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: 18/84
Findings: 2
Award: $267.84
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
Token distribution is non-atomic, erc20EntitlementPerUnit
is calculated first and the cached value is always used during token distribution. If setDistributableERC20s
is called during the distribution, old erc20EntitlementPerUnit
data will cause the wrong number of tokens to be distributed.
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.12; import {Test, console2} from "forge-std/Test.sol"; import {LiquidInfrastructureERC20, ERC20} from "../contracts/LiquidInfrastructureERC20.sol"; contract ERC20Test is Test { LiquidInfrastructureERC20 public erc20; uint256 constant MIN_PERIOD = 10; address constant HOLDER2 = address(0xdead); function setUp() public { address[] memory managedNFTs = new address[](0); address[] memory approvedHolders = new address[](2); approvedHolders[0] = address(this); approvedHolders[1] = HOLDER2; address[] memory distributableErc20s = new address[](0); erc20 = new LiquidInfrastructureERC20( "ERC20", "E", managedNFTs, approvedHolders, MIN_PERIOD, distributableErc20s ); erc20.mint(address(this), 1e18); erc20.mint(HOLDER2, 1e18); } function testWrongUnitAfterSetDistributableERC20s() public { vm.roll(block.number + MIN_PERIOD); ERC20 token0 = new ERC20("Token0", "T0"); ERC20 token1 = new ERC20("Token1", "T1"); address[] memory distributableErc20s1 = new address[](2); distributableErc20s1[0] = address(token0); distributableErc20s1[1] = address(token1); uint256 firstDistribute = 10e18; deal(address(token0), address(erc20), firstDistribute); deal(address(token1), address(erc20), firstDistribute * 2); erc20.setDistributableERC20s(distributableErc20s1); erc20.distribute(1); assertEq(token0.balanceOf(address(this)), firstDistribute / 2); assertEq(token1.balanceOf(address(this)), firstDistribute); vm.roll(block.number + MIN_PERIOD); ERC20 token2 = new ERC20("Token2", "T2"); address[] memory distributableErc20s2 = new address[](1); distributableErc20s2[0] = address(token2); deal(address(token2), address(erc20), firstDistribute * 100); erc20.setDistributableERC20s(distributableErc20s2); erc20.distribute(1); // @audit should be firstDistribute / 50, not firstDistribute / 2 assertEq(token2.balanceOf(HOLDER2), firstDistribute / 2); } }
Foundry
erc20EntitlementPerUnit
should be reset when call setDistributableERC20s
Context
#0 - c4-pre-sort
2024-02-20T05:45:53Z
0xRobocop marked the issue as duplicate of #260
#1 - c4-judge
2024-03-04T15:28:07Z
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)
🌟 Selected for report: SpicyMeatball
Also found by: BowTiedOriole, Breeje, CaeraDenoir, JohnSmith, Krace, Meera, PumpkingWok, SovaSlava, SpicyMeatball, d3e4, juancito, kutugu, nuthan2x, rokinot, rouhsamad, web3pwn
242.1143 USDC - $242.11
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/2b26c04f0448505635210416bef9d3ce96391b16/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L366-L371 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/2b26c04f0448505635210416bef9d3ce96391b16/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L425-L426
releaseManagedNFT
will decrease ManagedNFTs.length
, and withdrawFromManagedNFTs
caches nextWithdrawal
, which depends on ManagedNFTs.length
.
This causes nextWithdrawal
to be stale and DOS withdrawFromManagedNFTs
.
// withdrawFromManagedNFTs uint256 limit = Math.min( numWithdrawals + nextWithdrawal, ManagedNFTs.length ); uint256 i; for (i = nextWithdrawal; i < limit; i++) { // ... } // releaseManagedNFT ManagedNFTs[i] = ManagedNFTs[ManagedNFTs.length - 1];
If the nextWithdrawal
of withdrawFromManagedNFT
caches ManagedNFTs.length
before releaseManagedNFT
, after releaseManagedNFT
is called, nextWithdrawal
may be lower than the latest ManagedNFTs.length
, causing the withdrawFromManagedNFTs
function to be unavailable.
Another issue is that because releaseManagedNFT
will exchange array index, the execution order of withdrawFromManagedNFTs
will be disordered.
The following is a POC for DOS:
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.12; import {Test, console2} from "forge-std/Test.sol"; import {LiquidInfrastructureERC20, ERC20} from "../contracts/LiquidInfrastructureERC20.sol"; import {LiquidInfrastructureNFT} from "../contracts/LiquidInfrastructureNFT.sol"; contract ERC20Test is Test { LiquidInfrastructureERC20 public erc20; uint256 constant MIN_PERIOD = 10; address constant HOLDER2 = address(0xdead); function setUp() public { address[] memory managedNFTs = new address[](0); address[] memory approvedHolders = new address[](2); approvedHolders[0] = address(this); approvedHolders[1] = HOLDER2; address[] memory distributableErc20s = new address[](0); erc20 = new LiquidInfrastructureERC20( "ERC20", "E", managedNFTs, approvedHolders, MIN_PERIOD, distributableErc20s ); erc20.mint(address(this), 1e18); erc20.mint(HOLDER2, 1e18); } function testReleaseManagedNFTDOSWithdrawFromManagedNFTs() public { vm.startPrank(address(erc20)); LiquidInfrastructureNFT nft1 = new LiquidInfrastructureNFT("NFT1"); LiquidInfrastructureNFT nft2 = new LiquidInfrastructureNFT("NFT2"); LiquidInfrastructureNFT nft3 = new LiquidInfrastructureNFT("NFT3"); LiquidInfrastructureNFT nft4 = new LiquidInfrastructureNFT("NFT4"); LiquidInfrastructureNFT nft5 = new LiquidInfrastructureNFT("NFT5"); vm.stopPrank(); erc20.addManagedNFT(address(nft1)); erc20.addManagedNFT(address(nft2)); erc20.addManagedNFT(address(nft3)); erc20.addManagedNFT(address(nft4)); erc20.addManagedNFT(address(nft5)); erc20.withdrawFromManagedNFTs(4); assertEq(erc20.nextWithdrawal(), 4); erc20.releaseManagedNFT(address(nft3), address(this)); erc20.releaseManagedNFT(address(nft4), address(this)); erc20.withdrawFromAllManagedNFTs(); // @audit no more changes assertEq(erc20.nextWithdrawal(), 4); } }
Foundry
releaseManagedNFT
should reset nextWithdrawal
if nextWithdrawal >= ManagedNFTs.length
releaseManagedNFT
should not swap array indexContext
#0 - c4-pre-sort
2024-02-22T05:15:45Z
0xRobocop marked the issue as duplicate of #130
#1 - c4-judge
2024-03-03T13:01:29Z
0xA5DF marked the issue as satisfactory