Platform: Code4rena
Start Date: 13/02/2024
Pot Size: $24,500 USDC
Total HM: 5
Participants: 84
Period: 6 days
Judge: 0xA5DF
Id: 331
League: ETH
Rank: 33/84
Findings: 2
Award: $106.29
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xloscar01
Also found by: 0xAadi, 0xpiken, BowTiedOriole, Breeje, Fassi_Security, JohnSmith, Limbooo, SpicyMeatball, Tendency, Topmark, ZanyBonzy, aslanbek, atoko, jesjupyter, matejdb, max10afternoon, n0kto, peanuts, pkqs90, rouhsamad, thank_you, zhaojohnson
80.5583 USDC - $80.56
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L116-L119 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L216
The LiquidInfrastructureERC20
contract implements a mechanism to disapprove token holders from receiving further distributions. When the owner calls the disapproveHolder
function, disapproved holders lose their entitlement to future distributions. However, the existing tokens share held by these disapproved holders remain in the contract, leading to an accumulation of tokens over time.
The accumulation of tokens from disapproved holders can result in an imbalance in the distribution of rewards among remaining token holders. Disapproved holders' tokens are effectively locked in the contract, which may affect the fairness and effectiveness of the distribution mechanism. Moreover, the accumulation of unused tokens may also impact the overall token economics. The accumulated tokens can not be retrieved even by the owner.
require("@nomicfoundation/hardhat-foundry")
 in hardhat.config.js
 and run this to be able to run the PoC:npm install --save-dev @nomicfoundation/hardhat-foundry npx hardhat init-foundry
test/LiquidInfrastructureERC20Test.t.sol
 file containing the code below:// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.12; import {Test, console} from "forge-std/Test.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {LiquidInfrastructureERC20} from "../contracts/LiquidInfrastructureERC20.sol"; import {LiquidInfrastructureNFT} from "../contracts/LiquidInfrastructureNFT.sol"; contract LiquidInfrastructureERC20Test is Test { LiquidInfrastructureERC20 public liqInfraERC20; address erc20Owner = makeAddr("ERC20_Owner"); address alice = makeAddr("Alice"); address bob = makeAddr("Bob"); address[] erc20Addresses; address[] managedNFTs; address[] approvedHolders; function setUp() public { // Deploy ERC20 contract and LiquidInfrastructureERC20 contract erc20Addresses = new address[](1); erc20Addresses[0] = address(new MockERC20()); vm.startPrank(erc20Owner); liqInfraERC20 = new LiquidInfrastructureERC20( "Infra", "INFRA", managedNFTs, approvedHolders, 500, erc20Addresses ); // Approve holders and mint tokens, for Alice and Bob liqInfraERC20.approveHolder(alice); liqInfraERC20.mint(alice, 50); liqInfraERC20.approveHolder(bob); liqInfraERC20.mint(bob, 50); vm.stopPrank(); } function test_PoC_DisapproveHolderSharesAccumulateTokens() public { MockERC20 erc20 = MockERC20(erc20Addresses[0]); // Mint 100 tokens to the ERC20 contract held by LiquidInfrastructureERC20 erc20.mint(address(liqInfraERC20), 100); // Roll the block to trigger the distribution period vm.roll(501); // Ensure the ERC20 contract balance in LiquidInfrastructureERC20 is 100 assertEq(erc20.balanceOf(address(liqInfraERC20)), 100); // Ensure Alice and Bob have 0 balance before the distribution assertEq(erc20.balanceOf(alice), 0); assertEq(erc20.balanceOf(bob), 0); // Distribute rewards to all holders (Alice and Bob) liqInfraERC20.distributeToAllHolders(); // Ensure the ERC20 contract balance becomes 0 after distribution assertEq(erc20.balanceOf(address(liqInfraERC20)), 0); // Ensure Alice and Bob receive 50 tokens each assertEq(erc20.balanceOf(alice), 50); assertEq(erc20.balanceOf(bob), 50); // Disapprove Bob as a holder vm.startPrank(erc20Owner); liqInfraERC20.disapproveHolder(bob); vm.stopPrank(); // Mint another 100 tokens to the ERC20 contract held by LiquidInfrastructureERC20 erc20.mint(address(liqInfraERC20), 100); // Roll the block to trigger the next distribution period vm.roll(1001); // Distribute rewards to all holders (Alice) liqInfraERC20.distributeToAllHolders(); // The ERC20 contract balance dose not becomes 0 after distribution! assertNotEq(erc20.balanceOf(address(liqInfraERC20)), 0); // Alice has 100 tokens (before has 50 + receives only 50), // Bob has only 50 (did not receives anything since he is disapproved) assertEq(erc20.balanceOf(alice), 100); assertEq(erc20.balanceOf(bob), 50); // Mint 1000 more tokens to the ERC20 contract held by LiquidInfrastructureERC20 erc20.mint(address(liqInfraERC20), 1000); vm.roll(1501); // Distribute rewards to all holders (Alice) liqInfraERC20.distributeToAllHolders(); // The accumulation of tokens held by LiquidInfrastructureERC20 will get bigger now assertEq(erc20.balanceOf(address(liqInfraERC20)), 550); // Alice receives only 500 only while he is the only approved holder! assertEq(erc20.balanceOf(alice), 600); assertEq(erc20.balanceOf(bob), 50); } } contract MockERC20 is ERC20 { constructor() ERC20("ERC20", "ERC20") {} function mint(address to, uint256 value) public { _mint( to, value ); } }
forge test --mt test_PoC_DisapproveHolderSharesAccumulateTokens
 and see this test passing.Foundry
To mitigate the accumulation of tokens from disapproved holders consider one of the following:
Update the distribution mechanism to exclude disapproved holders from the entitlement calculation entirely. Modify the _beginDistribution()
function to calculate entitlements only for approved holders.
Implement token burning functionality within the disapproveHolder
function. Upon disapproval, burn the tokens held by the disapproved holder to remove them from circulation. This ensures that disapproved holders not only lose their entitlement to future distributions but also forfeit their existing tokens, preventing an accumulation of unused tokens in the contract.
Modify the disapproveHolder
function to include token burning functionality:
function disapproveHolder(address holder) public onlyOwner { require(isApprovedHolder(holder), "holder not approved"); // Burn the tokens held by the disapproved holder uint256 balanceToBurn = balanceOf(holder); _burn(holder, balanceToBurn); // Mark the holder as disapproved HolderAllowlist[holder] = false; }
With this modification, disapproved holders' tokens will be burned, ensuring a fairer distribution of rewards among remaining token holders and preventing an accumulation of unused tokens in the contract.
Other
#0 - c4-pre-sort
2024-02-20T04:07:43Z
0xRobocop marked the issue as duplicate of #703
#1 - c4-judge
2024-03-04T14:36:18Z
0xA5DF marked the issue as satisfactory
#2 - c4-judge
2024-03-08T15:15:58Z
0xA5DF changed the severity to 2 (Med Risk)
🌟 Selected for report: nuthan2x
Also found by: 0x0bserver, AM, CaeraDenoir, DanielArmstrong, JrNet, Kirkeelee, KmanOfficial, Krace, Limbooo, Meera, SovaSlava, SpicyMeatball, TheSavageTeddy, agadzhalov, aslanbek, atoko, csanuragjain, d3e4, imare, jesjupyter, juancito, kartik_giri_47538, kutugu, max10afternoon, offside0011, pkqs90, turvy_fuzz, xchen1130, zhaojohnson, ziyou-
25.7286 USDC - $25.73
In LiquidInfrastructureERC20 contract, The _beginDistribution
function locks the contract for distribution and calculates the entitlement to protocol-held ERC20s per unit (distributableERC20s
) of the LiquidInfrastructureERC20 held. Changing the distributableERC20s
array while the contract is locked for distribution can lead to incorrect distribution calculations, potential loss of funds, and unexpected behavior during reward distribution to token holders.
Consider the following scenarios:
distributableERC20s
array while a distribution is ongoing.erc20EntitlementPerUnit
array.distributableERC20s
array while a distribution is ongoing.Consider preventing modification during distribution, Implement a check within the setDistributableERC20s
function to ensure that it cannot be called while the contract is locked for distribution.
function setDistributableERC20s(address[] memory _distributableERC20s) public onlyOwner { require(!LockedForDistribution, "Cannot update while locked for distribution"); distributableERC20s = _distributableERC20s; }
Other
#0 - c4-pre-sort
2024-02-20T05:55:36Z
0xRobocop marked the issue as duplicate of #260
#1 - c4-judge
2024-03-04T15:30:07Z
0xA5DF marked the issue as satisfactory
#2 - c4-judge
2024-03-08T15:11:55Z
0xA5DF changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-08T15:26:19Z
0xA5DF changed the severity to 2 (Med Risk)