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: 23/84
Findings: 3
Award: $167.78
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: BowTiedOriole
Also found by: 0x0bserver, 0xAadi, 0xJoyBoy03, 0xlamide, 0xlemon, 0xpiken, Babylen, Breeje, Brenzee, CodeWasp, DanielArmstrong, DarkTower, Fassi_Security, Fitro, Honour, JohnSmith, Krace, MrPotatoMagic, Myrault, ReadyPlayer2, SovaSlava, SpicyMeatball, TheSavageTeddy, Tigerfrake, atoko, cryptphi, csanuragjain, d3e4, gesha17, kinda_very_good, krikolkk, matejdb, max10afternoon, miaowu, n0kto, nuthan2x, parlayan_yildizlar_takimi, peanuts, petro_1912, pontifex, psb01, pynschon, rouhsamad, shaflow2, slippopz, spark, turvy_fuzz, web3pwn, zhaojohnson
3.5914 USDC - $3.59
Judge has assessed an item in Issue #303 as 3 risk. The relevant finding follows:
When burning tokens, the to
address will be address(0). This to
address will be appended to the holders
array during burn()
as _beforeTokenTransfer()
is invoked, which is
if (from == address(0) || to == address(0)) { _beforeMintOrBurn(); } bool exists = (this.balanceOf(to) != 0); if (!exists) { > holders.push(to); }
Have a clause that disallows the burn address to be part of the holders array.
#0 - c4-judge
2024-03-08T13:52:19Z
0xA5DF marked the issue as duplicate of #77
#1 - c4-judge
2024-03-08T13:52:23Z
0xA5DF marked the issue as satisfactory
#2 - c4-judge
2024-03-08T13:52:27Z
0xA5DF marked the issue as partial-50
🌟 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
Reward tokens will be stuck in the contract. Disapproved token holders will not be able to claim their reward tokens and approved token holders will get less rewards.
For each LiquidInfrastructure token a holder has, it is entitled to some rewards. This is calculated through entitlement, which takes the total balance of the reward token and divide it by the supply of the LiquidInfrastructure token.
uint256 entitlement = balance / supply;
For example, if there are 1000e18 reward tokens, and 1000e18 LiquidInfrastructure token, then 1 LiquidInfrastructure token gets 1 reward token.
A user can be disapproved by the owner through the disapproveHolder()
function.
function disapproveHolder(address holder) public onlyOwner { require(isApprovedHolder(holder), "holder not approved"); HolderAllowlist[holder] = false; }
If the user is disapproved, then he is unable to receive any reward tokens.
function distribute(uint256 numDistributions) public nonReentrant { ... > if (isApprovedHolder(recipient)) { uint256[] memory receipts = new uint256[]( distributableERC20s.length ); for (uint j = 0; j < distributableERC20s.length; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); > uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient); > if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement;
However, the calculation of the distribution still assume that the holder of the LiquidInfrastructure token is getting the reward tokens.
Since the disapproved holder cannot get their reward tokens, the reward tokens will be stuck in the contract.
For example, there are 1000 LiquidInfrastructure tokens. Alice has 500 tokens. For some reason, Alice got disapproved as a holder. She is not eligible for the rewards. There are 1000e18 reward tokens, and 500e18 reward tokens are distributed to the holders of the remaining 500 LiquidInfrastructure tokens. The other 500e18 reward tokens are left in the contract.
Manual Review
The totalSupply() should reflect the current approved holder supply only, and remove all the disapproveHolder supply.
This might be harsh but if a user is disapproved, recommend burning all their LiquidInfrastructure token to prevent reward tokens from getting stuck and allowing the approved holders to get more reward tokens.
function disapproveHolder(address holder) public onlyOwner { require(isApprovedHolder(holder), "holder not approved"); + _burn(holder , this.balanceOf(holder)) HolderAllowlist[holder] = false; }
Could have an internal account as well so that if the holder ever gets re-approved, the owner will mint the amount of tokens that was burned previously.
Context
#0 - c4-pre-sort
2024-02-22T07:34:34Z
0xRobocop marked the issue as duplicate of #703
#1 - c4-judge
2024-03-04T14:44:54Z
0xA5DF marked the issue as satisfactory
🌟 Selected for report: ZanyBonzy
Also found by: 0xSmartContractSamurai, DarkTower, MrPotatoMagic, SpicyMeatball, TheSavageTeddy, jesjupyter, lsaudit, peanuts, zhaojohnson
83.634 USDC - $83.63
When burning tokens, the to
address will be address(0). This to
address will be appended to the holders
array during burn()
as _beforeTokenTransfer()
is invoked, which is
if (from == address(0) || to == address(0)) { _beforeMintOrBurn(); } bool exists = (this.balanceOf(to) != 0); if (!exists) { > holders.push(to); }
Have a clause that disallows the burn address to be part of the holders array.
MinDistributionPeriod
cannot be changedOnce MinDistributionPeriod
is set in the constructor, it cannot be changed by anyone. This is an issue because the block.number of different chains may differ in the future due to upgrades, and to accommodate to the changes, MinDistributionPeriod
should be flexible.
constructor( string memory _name, string memory _symbol, address[] memory _managedNFTs, address[] memory _approvedHolders, uint256 _minDistributionPeriod, address[] memory _distributableErc20s ) ERC20(_name, _symbol) Ownable() { ManagedNFTs = _managedNFTs; ... > MinDistributionPeriod = _minDistributionPeriod;
Create a function that allows the owner to change MinDistributionPeriod
.
MinDistributionPeriod
is set at 0, then minting and burning can happen before distribution.The README states:
mint() and burn() should not work when the minimum distribution period has elapsed.
If there is no minimum distribution period, then mint()
and burn()
can work when the minimum distribution period has elapsed.
Enforce that MinDistributionPeriod cannot be a zero value in the constructor.
for (uint i = 0; i < _approvedHolders.length; i++) { HolderAllowlist[_approvedHolders[i]] = true; } require(MinDistributionPeriod != 0, "MinDistributionPeriod is set at 0"); MinDistributionPeriod = _minDistributionPeriod; distributableERC20s = _distributableErc20s;
The _beforeTokenTransfer()
function is overriden from the base ERC20 contract, and so if a user transfers to another user via a transferFrom()
function, the transfer will not work. The approvee needs to be an approved holder as well.
function _beforeTokenTransfer( address from, address to, uint256 amount ) internal virtual override { require(!LockedForDistribution, "distribution in progress"); if (!(to == address(0))) { > require( isApprovedHolder(to), "receiver not approved to hold the token" ); } if (from == address(0) || to == address(0)) { _beforeMintOrBurn(); } bool exists = (this.balanceOf(to) != 0); if (!exists) { holders.push(to); } }
Since the transfer does not work, only the burnFrom()
function works. I'm not sure about the exact purpose of burning the token since all it does is create extra attack vectors. As such, the approval in ERC20 should be overridden completely and should not be in use.
distribute()
may reach out of gas error if the length of the arrays of approved holders and approved ERC20s is too largeIf there is a lot of approved holders and a lot of ERC20 type tokens to distribute, then the function distribute()
will continuously face out of gas errors. The main problem is the nested loop structure, which will consume a lot of gas.
uint i; for (i = nextDistributionRecipient; i < limit; i++) { address recipient = holders[i]; if (isApprovedHolder(recipient)) { uint256[] memory receipts = new uint256[]( distributableERC20s.length ); for (uint j = 0; j < distributableERC20s.length; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient); if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; } }
When the holder is disapproved by the owner, the HolderAllowlist[holder]
will be false.
function disapproveHolder(address holder) public onlyOwner { require(isApprovedHolder(holder), "holder not approved"); HolderAllowlist[holder] = false; }
The holder cannot transfer their tokens out as well, because of the overridden _transferFrom()
function. The Althea token will be stuck in the contract.
Consider burning the holder's tokens if the owner decides to disapprove their token.
It is known that the reward token for being a holder of LiquidInfrastructureERC20 token is USDC. Users that holds the LiquidInfrastructureERC20 token will get some USDC rewards when distribute()
is called.
); for (uint j = 0; j < distributableERC20s.length; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient); > if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; }
A malicious holder can grief the distribute function by blacklisting themselves so that they cannot receive USDC tokens. As a result, the whole function will fail since the transaction is atomic.
The whole distribution process will fail. Distribution caller will waste money on gas.
Note that some other ERC20s also have a blacklist option. Take note of this when accepting ERC20 tokens as reward. If possible, break the function apart so that is it not atomic and if someone tries to grief the transaction, it will not work.
When distributing tokens, the distribute function does not take into account that the distributed sum can be zero if there is no revenue generated from the NFTs.
); for (uint j = 0; j < distributableERC20s.length; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient); > if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; }
Some ERC20 tokens, eg LEND, will revert on zero value transfers. Take note of those tokens before adding it to the revenue.
https://github.com/d-xo/weird-erc20?tab=readme-ov-file#revert-on-zero-value-transfers
#0 - c4-pre-sort
2024-02-22T07:35:56Z
0xRobocop marked the issue as high quality report
#1 - ChristianBorst
2024-03-02T02:26:07Z
Confirmed: This is an issue which allows payout manipulation and a potential DoS vector
Disagree With Severity: I think it's a great suggestion to add this capability especially considering block times are highly irregular, however the way Althea-L1 will work means this is not a vulnerability. On other chains like Gnosis this may be a significant issue.
Confirmed.
Dispute Validity: _beforeTokenTransfer() does not require the approvee to be an approved holder, it requires the recipient of the LiquidInfrastructureERC20 tokens to be an approved holder, which is the desired behavior. I have successfully tested this situation by facilitating a transfer from one approved holder to another different approved holder via an unapproved holder like so:
holder1
and holder2
, but do not do so for nonHolder
approve()
function as holder1
, approving nonHolder
to use 500 wei of the LiquidInfrastructureERC20transferFrom()
function as nonHolder
, sending 500 wei from holder1
to holder2
This works without issue.
Acknowledged: This is valid, however we're expecting distributableERC20s to be only 1 or 2 tokens, so it is not of significant concern to us. We are more concerned with issues regarding a growing holders list.
Dispute Validity: There is no overridden _transferFrom() function. The overridden _beforeTokenTransfer() and _afterTokenTransfers() will allow a disapproved holder to send their tokens to another approved holder or to burn the tokens. Therefore this report is invalid.
Disagree With Severity: On Althea-L1 we will have bridged ERC20 representations which will not have blacklists, since a user could simply avoid the blacklist by bridging to a non-blacklisted address. This is a valid concern but not for our deployment, and the distributableERC20s will be vetted to not contain features like blacklists.
Disagree With Severity: Same as L-07 we will not have ERC20s with reverts on transfer like this (the amount from balanceOf() -> transfer() should not revert at least) so this is not of concern to our deployment.
#2 - c4-sponsor
2024-03-02T02:26:23Z
ChristianBorst (sponsor) acknowledged
#3 - 0xA5DF
2024-03-08T11:18:42Z
+L from #587 +L from #302
#4 - 0xA5DF
2024-03-08T13:55:12Z
H L R F L F L L
6L, 1R
#5 - c4-judge
2024-03-08T14:25:17Z
0xA5DF marked the issue as grade-a