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: 35/84
Findings: 2
Award: $87.74
🌟 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
7.1828 USDC - $7.18
Most functions of LiquidInfrastructureERC20
could potentially be susceptible to a DoS (Denial of Service) attack if the size of holders
experiences a significant increase.
When the distribution is not in progress (LockedForDistribution
is false
), approved holders of LiquidInfrastructureERC20
can freely transfer tokens among themselves without restriction.
The recipient will be added to the holders list if their previous balance was zero:
142: bool exists = (this.balanceOf(to) != 0); 143: if (!exists) { 144: holders.push(to); 145: }
All holders listed in holders
are eligible to receive future reward distributions.
However, it doesn't check if the recipient has been added to holders
before, a malicious user can increase the size of holders
significantly by calling LiquidInfrastructureERC20#transfer()
multi times to transfer zero amount of tokens to an approved holder with a zero balance.
Functions that iterate over holders
may encounter "Out of Gas" errors and revert:
Let's add a function in LiquidInfrastructureERC20
to retrive the size of holders
:
function lengthOfHolders() public view returns (uint256) { return holders.length; }
Then copy below codes to liquidERC20.ts and run npx hardhat test
:
it("Increase holders", async function () { const { infraERC20, holder1, holder2, badSigner } =await liquidErc20Fixture(); await infraERC20.approveHolder(holder1.address); await infraERC20.approveHolder(holder2.address); expect(await infraERC20.lengthOfHolders()).to.equal(0); for (let i=0; i<256; i++) { await infraERC20.connect(holder1).transfer(holder2.address, 0); } expect(await infraERC20.lengthOfHolders()).to.equal(256); });
The size of holders
can be easily increased to 256 through 256 times of zero token transferring.
Manual review
EnumerableSet.AddressSet
provided by openzeppelin to manage holders
.to
can only be added into holders
when pervious balance is zero and amount
is not zero.DoS
#0 - c4-pre-sort
2024-02-22T06:47:24Z
0xRobocop marked the issue as duplicate of #77
#1 - c4-judge
2024-03-04T13:23:44Z
0xA5DF marked the issue as satisfactory
#2 - c4-judge
2024-03-08T15:08:02Z
0xA5DF changed the severity to 3 (High Risk)
🌟 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
The distribution reward an approved holder received might be less than expected when some of other holders are disapproved.
Whenever the distribution is ready, any one can distribute token revenue to the eligible Liquid Infrastructure ERC20 holders by calling LiquidInfrastructureERC20#distribute()
.
For a specific token, the reward per token will be calculated firstly:
269: // Calculate the entitlement per token held 270: uint256 supply = this.totalSupply(); 271: for (uint i = 0; i < distributableERC20s.length; i++) { 272: uint256 balance = IERC20(distributableERC20s[i]).balanceOf( 273: address(this) 274: ); 275: uint256 entitlement = balance / supply; 276: erc20EntitlementPerUnit.push(entitlement); 277: }
Then the total reward will be calculated and distributed to the Liquid Infrastructure ERC20 holders:
214: for (i = nextDistributionRecipient; i < limit; i++) { 215: address recipient = holders[i]; 216: if (isApprovedHolder(recipient)) { 217: uint256[] memory receipts = new uint256[]( 218: distributableERC20s.length 219: ); 220: for (uint j = 0; j < distributableERC20s.length; j++) { 221: IERC20 toDistribute = IERC20(distributableERC20s[j]); 222:@> uint256 entitlement = erc20EntitlementPerUnit[j] * 223:@> this.balanceOf(recipient); 224:@> if (toDistribute.transfer(recipient, entitlement)) { 225: receipts[j] = entitlement; 226: } 227: } 228: 229: emit Distribution(recipient, distributableERC20s, receipts); 230: } 231: }
If a holder is disapproved, they are not eligible to receive any rewards no matter how many Liquid Infrastructure ERC20 tokens they holds.
However, the token balances of disapproved holders are not excluded from supply
when calculating entitlement
. The calculated entitlement
will be diluted and the reward an approved holder received is less than expected. Below is an example:
200
Liquid Infrastructure tokens400
100
USDCdistributeToAllHolders()
to distribute USDC reward
entitlement
is 0.25
USDC (100 / 400
)50
USDC (0.25 * 200
)50
USDC is still locked in the contractCopy below codes to liquidERC20.ts and run npx hardhat test
:
it("The distribution reward is diluted", async function () { const { infraERC20, erc20Owner, testERC20A, testERC20B, testERC20C, nftAccount1, nftAccount2, nftAccount3, holder1, holder2, holder3, holder4, } = await liquidErc20Fixture(); const holders = [holder1, holder2, holder3, holder4]; for (let holder of holders) { await infraERC20.approveHolder(holder.address); await infraERC20.mint(holder.address, 100); } const nft = await deployLiquidNFT(nftAccount1); const erc20s: ERC20[] = [testERC20A]; await nft.setThresholds(erc20s, erc20s.map(() => 0)); await nft.transferFrom(nftAccount1.address, await infraERC20.getAddress(), (await nft.AccountId())); await testERC20A.transfer(await nft.getAddress(), 30000); expect(await testERC20A.balanceOf(nft.getAddress())).to.equal(30000); await infraERC20.addManagedNFT(nft.getAddress()); await infraERC20.withdrawFromAllManagedNFTs(); expect(await testERC20A.balanceOf(nft.getAddress())).to.equal(0); expect(await testERC20A.balanceOf(infraERC20.getAddress())).to.equal(30000); const holder1BalanceBeforeDistribution = await testERC20A.balanceOf(holder1.getAddress()); const holder2BalanceBeforeDistribution = await testERC20A.balanceOf(holder2.getAddress()); const holder3BalanceBeforeDistribution = await testERC20A.balanceOf(holder3.getAddress()); const holder4BalanceBeforeDistribution = await testERC20A.balanceOf(holder4.getAddress()); //@audit-info disapprove holder4 before distribution await infraERC20.disapproveHolder(holder4.address); mine(500); await infraERC20.distributeToAllHolders(); const holder1BalanceAfterDistribution = await testERC20A.balanceOf(holder1.getAddress()); const holder2BalanceAfterDistribution = await testERC20A.balanceOf(holder2.getAddress()); const holder3BalanceAfterDistribution = await testERC20A.balanceOf(holder3.getAddress()); const holder4BalanceAfterDistribution = await testERC20A.balanceOf(holder4.getAddress()); //@audit-info all approved holders only received 7500 of reward expect(holder1BalanceAfterDistribution - holder1BalanceBeforeDistribution).to.equal(7500); expect(holder2BalanceAfterDistribution - holder2BalanceBeforeDistribution).to.equal(7500); expect(holder3BalanceAfterDistribution - holder3BalanceBeforeDistribution).to.equal(7500); //@audit-info 7500 of reward is locked in infraERC20 expect(holder4BalanceAfterDistribution - holder4BalanceBeforeDistribution).to.equal(0); expect(await testERC20A.balanceOf(infraERC20.getAddress())).to.equal(7500); });
Manual review
All Liquid Infrastructure ERC20 token balances of disapproved holders should be excluded from supply
when calculating entitlement
.
Math
#0 - c4-pre-sort
2024-02-20T05:25:32Z
0xRobocop marked the issue as duplicate of #703
#1 - c4-judge
2024-03-04T14:37:00Z
0xA5DF marked the issue as satisfactory