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: 32/84
Findings: 2
Award: $106.29
🌟 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
Reward per share (entitlement) is calculated from rewardToken.balanceOf(this) / totalSupply()
:
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); }
Then, in distribute
, rewards are distributed only to approved holders:
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; } }
If a part of LiquidInfrastructureERC20's totalSupply
is held by disapproved holders, they will not receive any rewards. These rewards are not forwarded to approved holders, but instead are left in the contract.
Alice and Bob each have 50e18 LiquidInfrastructureERC20s;
Bob became disapproved;
100e18 DAI are distributed: entitlement = DAI.balanceOf(this) / totalSupply() = 100e18 / 100e18 = 1
;
Bob does not receive anything (as intended); Alice receives 50e18 * 1 = 50e18 DAI
; 50e18 DAI are left in the contract.
Instead of totalSupply()
, entitlement
calculation should use the total balance of approved accounts, which should be tracked in functions approveHolder
, disapproveHolder
, and _transfer
.
Other
#0 - c4-pre-sort
2024-02-20T05:00:15Z
0xRobocop marked the issue as duplicate of #703
#1 - c4-judge
2024-03-04T14:36:46Z
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
function setDistributableERC20s( address[] memory _distributableERC20s ) public onlyOwner { distributableERC20s = _distributableERC20s; }
setDistributableERC20s
can be called by the owner during a distribution, which would be reasonable to call an owner mistake. However, it is possible to damage the system even if setDistributableERC20s
is called outside of distribution:
Imagine there are 2 holders of LiERC20, both holding 0.5e18 tokens; the current array of distributable ERC20s is [DAI]
. The owner wants to change it to [USDC, USDT]
, so he makes sure all rewards are distributed, and then calls setDistributableERC20s
.
An attacker frontruns the owner, by donating 1e18 DAI and calling distribute(1)
, which sends 0.5e18 DAI to the first holder.
Then, the owner's transaction is mined: distributions and transfers become bricked, as the next distribute
will attempt to transfer 0.5e18 USDC to the next holder, but the contract does not have any USDC, so the distribution will revert.
To lift the DoS, the owner will need to set the array back to [DAI]
, then finish the distribution, and setDistributableERC20s
to [USDC, USDT]
again.
The budget for the attack will be quite different depending if the "precision loss in calculating entitlement" issue is fixed:
a) If not fixed, the attacker will have to donate the same amount of DAI as the total supply of LiquidInfrastructureERC20, which is unfeasible (otherwise entitlement would round down to zero, and distributions of 0 tokens will not revert).
b) However, if the issue is fixed in a reasonable way, the required budget would be a fraction of a cent.
function setDistributableERC20s( address[] memory _distributableERC20s ) public onlyOwner { + require(!LockedForDistribution, "distribution in progress"); distributableERC20s = _distributableERC20s; }
OR
function setDistributableERC20s( address[] memory _distributableERC20s ) public onlyOwner { + if (LockedForDistribution) distributeToAllHolders(); distributableERC20s = _distributableERC20s; }
DoS
#0 - c4-pre-sort
2024-02-20T05:22:43Z
0xRobocop marked the issue as duplicate of #260
#1 - c4-judge
2024-03-04T15:27:13Z
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:18Z
0xA5DF changed the severity to 2 (Med Risk)