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: 28/84
Findings: 3
Award: $117.64
🌟 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#L106-L109 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L216 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L270-L276
If a holder is disapproved
by the owner using disapproveHolder
, his will not be eligible in distribute
due to the isApprovedHolder
check. However, in _beginDistribution
, his share is still taken into account when calculating erc20EntitlementPerUnit
. This will lead to incorrect distribution, since other eligible users may receive fewer rewards, and some rewards are still left in the contract for the next round of distribution.
The owner can use disapproveHolder to prevent some users from receiving any more of the underlying ERC20. This could work for someone who is already possessing some underlying ERC20 tokens.
function disapproveHolder(address holder) public onlyOwner { require(isApprovedHolder(holder), "holder not approved"); HolderAllowlist[holder] = false; }
In the distribute function, holder can't receive rewards since he is not eligible.
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; } } emit Distribution(recipient, distributableERC20s, receipts); }
However, when the distribution starts, the erc20EntitlementPerUnit
is calculated using entitlement = balance / supply
in _beginDistribution.
uint256 supply = this.totalSupply(); for (uint i = 0; i < distributableERC20s.length; i++) { uint256 balance = IERC20(distributableERC20s[i]).balanceOf( address(this) ); uint256 entitlement = balance / supply; erc20EntitlementPerUnit.push(entitlement); }
Consider the following situation:
A
, B
, C
both have 1 LiquidInfrastructureERC20
.USDC
in the contract to distribute.A
is disapproved and will not participate in distribute
.erc20EntitlementPerUnit
for USDC
will be 10 USDC per LiquidInfrastructureERC20 token
since A
's share is still counted in the calculation.distribute
, B
and C
will only receive 10 USDC
instead of 15 USDC
. And 10 USDC
is left in the contract to be distributed in the next round.VSCode
Add a variable frozen
account to record the share of LiquidInfrastructureERC20
being frozen due to disapproval
. And use the substrated value supply-frozen
to calculate entitlement
.
uint frozen = 0; function approveHolder(address holder) public onlyOwner { require(!isApprovedHolder(holder), "holder already approved"); HolderAllowlist[holder] = true; frozen -= this.balanceOf(holder); // <= reduce when the holder is approved } function disapproveHolder(address holder) public onlyOwner { require(isApprovedHolder(holder), "holder not approved"); HolderAllowlist[holder] = false; frozen += this.balanceOf(holder); // <= increase when the holder is disapproved } function _beginDistribution() internal { ... // Calculate the entitlement per token held uint256 supply = this.totalSupply(); require(supply > frozen, "no user to distribute"); // <= add here for (uint i = 0; i < distributableERC20s.length; i++) { uint256 balance = IERC20(distributableERC20s[i]).balanceOf( address(this) ); uint256 entitlement = balance / (supply-frozen); // <= modify code here erc20EntitlementPerUnit.push(entitlement); } nextDistributionRecipient = 0; emit DistributionStarted(); }
ERC20
#0 - c4-pre-sort
2024-02-22T05:24:12Z
0xRobocop marked the issue as duplicate of #703
#1 - c4-judge
2024-03-04T14:41:50Z
0xA5DF marked the issue as satisfactory
🌟 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#L106-L109 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
approveHolder
and disapproveHolder
should not be called during distribution. If a holder is disapproved
, some rewards will be left in the contract and will not be used in this round of distribution
.
There is no check of LockedForDistribution
in the function approveHolder
and disapproveHolder. This means that a user can be disapproved during the distribution process.
function disapproveHolder(address holder) public onlyOwner { require(isApprovedHolder(holder), "holder not approved"); HolderAllowlist[holder] = false; }
Consider the following scenario:
A
, B
, and C
is ready for distribution
.A
is distributed.B
is disapproved
, so he will not get his share of rewards.C
is distributed, some rewards are still left in the contract, leading to incorrect distribution.Manual
Add require(!LockedForDistribution," cannot disapprove/approve when already locked");
in the function approveHolder
and disapproveHolder
.
ERC20
#0 - c4-pre-sort
2024-02-22T05:27:52Z
0xRobocop marked the issue as duplicate of #126
#1 - c4-judge
2024-03-05T12:18:47Z
0xA5DF marked the issue as duplicate of #703
#2 - c4-judge
2024-03-05T12:21:27Z
0xA5DF marked the issue as partial-25
🌟 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
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L441-L445 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L272-L276 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L221-L225
In the setDistributableERC20s
function, the distributableERC20s
is reset by the owner without any check on LockedForDistribution
. If the function is called after the distribution has begun, this would cause inconsistency between distributableERC20s
and erc20EntitlementPerUnit
, leading to Denial of Service or incorrect rewards distribution.
In the setDistributableERC20s function, the distributableERC20s
is reset by the owner. Thus tokens and length may all change during the function call.
function setDistributableERC20s( address[] memory _distributableERC20s ) public onlyOwner { distributableERC20s = _distributableERC20s; }
However, since there is no check on LockedForDistribution
, the function setDistributableERC20s
could be called during the distribution
process.
In _beginDistribution however, erc20EntitlementPerUnit
will be created for each token in distributableERC20s
.
for (uint i = 0; i < distributableERC20s.length; i++) { uint256 balance = IERC20(distributableERC20s[i]).balanceOf( address(this) ); uint256 entitlement = balance / supply; erc20EntitlementPerUnit.push(entitlement); }
If setDistributableERC20s
after _beginDistribution
, this would cause inconsistency in tokens and length. And would cause unintended behavior in distribute since erc20EntitlementPerUnit[j]
is inconsistent with distributableERC20s[j]
.
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; } }
In this scenario, the transaction would revert if erc20EntitlementPerUnit.length < distributableERC20s.length or there is not enough balance to transfer for the new token. Otherwise, the distribution would be completely incorrect(For example, 3 USDT
is distributed, but should be 3 WETH
instead).
Manual, VSCode
Add require(!LockedForDistribution," cannot set distributableERC20s when already locked");
in the function setDistributableERC20s
.
DoS
#0 - c4-pre-sort
2024-02-20T04:20:27Z
0xRobocop marked the issue as duplicate of #151
#1 - c4-pre-sort
2024-02-20T04:38:32Z
0xRobocop marked the issue as duplicate of #260
#2 - c4-judge
2024-03-04T15:16:41Z
0xA5DF marked the issue as satisfactory
#3 - c4-judge
2024-03-08T15:13:03Z
0xA5DF changed the severity to 3 (High Risk)
#4 - c4-judge
2024-03-08T15:26:19Z
0xA5DF changed the severity to 2 (Med Risk)
🌟 Selected for report: ZanyBonzy
Also found by: 0xSmartContractSamurai, DarkTower, MrPotatoMagic, SpicyMeatball, TheSavageTeddy, jesjupyter, lsaudit, peanuts, zhaojohnson
11.348 USDC - $11.35
The redundant requirement will never revert and should be removed.
The requirement require(true, "unable to find released NFT in ManagedNFTs");
is useless in the code since it will never revert.
Manual
Remove the requirement.
Other
#0 - c4-pre-sort
2024-02-21T05:25:54Z
0xRobocop marked the issue as duplicate of #184
#1 - c4-judge
2024-03-04T12:56:39Z
0xA5DF changed the severity to QA (Quality Assurance)
#2 - 0xA5DF
2024-03-08T11:35:42Z
+3L - #582, #290, #524 = 4L
#3 - c4-judge
2024-03-09T08:40:18Z
0xA5DF marked the issue as grade-b