Platform: Code4rena
Start Date: 23/06/2023
Pot Size: $60,500 USDC
Total HM: 31
Participants: 132
Period: 10 days
Judge: 0xean
Total Solo HM: 10
Id: 254
League: ETH
Rank: 43/132
Findings: 2
Award: $212.43
π Selected for report: 0
π Solo Findings: 0
π Selected for report: Kenshin
Also found by: 0xNightRaven, Breeje, totomanov
202.5014 USDC - $202.50
Denial of service to distributeRewards()
, potentially delaying reward distribution indefinitely.
Trade slippage in a Curve StableSwap pool increases with size (relative to liquidity) and decreases with the amplification factor A.
0.2% is practically too low to work under all market conditions. When
distributeRewards()
to be called on a part of the rewards.DoS
#0 - c4-pre-sort
2023-07-08T15:12:22Z
JeffCX marked the issue as duplicate of #841
#1 - c4-judge
2023-07-25T20:28:59Z
0xean changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-07-26T13:00:19Z
This previously downgraded issue has been upgraded by 0xean
#3 - c4-judge
2023-07-26T13:00:35Z
0xean marked the issue as duplicate of #794
#4 - c4-judge
2023-07-28T15:37:54Z
0xean marked the issue as satisfactory
π Selected for report: 0xnev
Also found by: 0xRobocop, 0xbrett8571, 0xkazim, 0xnacho, 3agle, 8olidity, ABAIKUNANBAEV, Bauchibred, Co0nan, CrypticShepherd, D_Auditor, DelerRH, HE1M, Iurii3, Kaysoft, MrPotatoMagic, RedOneN, RedTiger, Rolezn, SanketKogekar, Sathish9098, Timenov, Toshii, Vagner, bart1e, bytes032, codetilda, devival, halden, hals, kutugu, m_Rassska, naman1778, nonseodion, seth_lawson, solsaver, squeaky_cactus, totomanov, y51r, yudan, zaevlad
9.931 USDC - $9.93
https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/ProtocolRewardsPool.sol#L95 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/ProtocolRewardsPool.sol#L157 https://github.com/code-423n4/2023-06-lybra/blob/main/contracts/lybra/miner/ProtocolRewardsPool.sol#L163
Each unstaking operation results in an expected loss of 3.9M wei LBR for the user. The loss cumulates with every consecutive unstaking operations.
When a user calls unstake
, a quantity unstakeRatio
is calculated for the user, which represents the LBR unlocked per second, such that after exitCycle
(90 days or 7776000 seconds), the claimable LBR is 100% of the stake.
// ProtocolRewardsPool.sol#L95 unstakeRatio[msg.sender] = total / exitCycle;
Due to Solidity's integer division unstakeRatio
may have a remainder of up to 7775999, which will be unaccounted for. In practice this means that calculating the claimable LBR at any point in time between 0 and 90 days will on average make the user lose 3888000 wei
of dust.
This loss wiull cumulate if the user intermittently calls unstake
while waiting for another unstake
to finish. After n
intermittent calls a user will (roughly) lose on average n*3888000
wei.
All losses are irrecoverable for the user.
The following test file demonstrates that residuals can cumulate. In the test case an aggregate loss of 311,839,927 wei is reached after 79 consecutive unstaking operations.
// test/poc.test.js const { ethers } = require("hardhat"); const { time } = require("@nomicfoundation/hardhat-network-helpers"); const { parseEther, formatEther } = ethers.utils; const days = (n) => Math.floor(n * 24 * 60 * 60); it('ProtocolRewardsPool.sol residual stacking PoC', async function () { const [owner, user] = await ethers.getSigners(); // 1. Setup const dao = await ethers.deployContract("GovernanceTimelock", [1, [owner.address], [owner.address], owner.address]); const configurator = await ethers.deployContract("Configurator", [dao.address, ethers.constants.AddressZero]); const esLBR = await ethers.deployContract("esLBR", [configurator.address]); const LBR = await ethers.deployContract("LBR", [configurator.address, 18, ethers.constants.AddressZero]); const esLBRBoost = await ethers.deployContract("esLBRBoost", []); const pool = await ethers.deployContract("ProtocolRewardsPool", [configurator.address]); await pool.setTokenAddress(esLBR.address, LBR.address, esLBRBoost.address); await configurator.setProtocolRewardsPool(pool.address); await configurator.setTokenMiner([owner.address, pool.address], [true, true]); // 2. Stake 100 LBR locked for 30 days await LBR.mint(user.address, parseEther("100")); await esLBRBoost.connect(user).setLockStatus(0); await LBR.connect(user).approve(pool.address, ethers.constants.MaxUint256); await pool.connect(user).stake(parseEther("100")); await time.increase(days(30)); // 3. Stake and unstake lots of times const unstakeAmt = parseEther('0.001'); const timeStep = days(1); const repetitions = 79; for(let i = 0; i < repetitions; i++) { await pool.connect(user).unstake(unstakeAmt); await time.increase(timeStep); } await time.increase(days(90)); // 4. Withdraw all LBR and check loss await pool.withdraw(user.address); const expectedLBR = unstakeAmt.mul(repetitions); const actualLBR = await LBR.balanceOf(user.address); const absoluteLoss = expectedLBR.sub(actualLBR); // Absolute loss 311839927 after 79x unstake(0.001) console.log(`Absolute loss ${absoluteLoss.toString()} after ${repetitions}x unstake(${formatEther(unstakeAmt)})`); });
Hardhat, mocha
Increase the precision of unstakeRatio
by multiplying the numerator by 1e18, for example.
Math
#0 - c4-pre-sort
2023-07-10T12:58:23Z
JeffCX marked the issue as primary issue
#1 - c4-sponsor
2023-07-18T06:12:44Z
LybraFinance marked the issue as disagree with severity
#2 - LybraFinance
2023-07-18T06:12:50Z
Loss can be negligible.
#3 - c4-judge
2023-07-26T17:02:41Z
0xean changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-07-28T17:17:43Z
0xean marked the issue as grade-b
#5 - c4-sponsor
2023-07-29T11:25:41Z
LybraFinance marked the issue as sponsor acknowledged