29190 - [SC - Insight] Permanent freezing of up to wei of yield each ...
Submitted on Mar 10th 2024 at 01:46:37 UTC by @nethoxa for Boost | ZeroLend
Report ID: #29190
Report type: Smart Contract
Report severity: Insight
Target: https://github.com/zerolend/governance
Impacts:
Permanent freezing of unclaimed yield
Description
Brief/Intro
Due to a rounding error when notifying a reward to LendingPoolGauge, up to 3 wei of the used token will be locked forever in the contract.
Vulnerability Details
It's well known Solidity rounds down on integer division. Because of that, in LendingPoolGauge::notifyRewardAmount, if the given amount is not divisible by 4, up to 3 wei of yield will be permanently locked in the contract as there is no way to take them back and the contract sends amount / 4 to the supplyGauge and amount / 4 * 3 to the borrowGauge:
functionnotifyRewardAmount(address token,uint256 amount ) externalreturns (bool) {IERC20(token).safeTransferFrom(msg.sender,address(this), amount);// send 1/4 to the supply sideIERC20(token).approve(address(supplyGauge), amount);bool a = supplyGauge.notifyRewardAmount(token, amount /4);// send 3/4th to the borrow sideIERC20(token).approve(address(borrowGauge), amount);bool b = borrowGauge.notifyRewardAmount(token, (amount /4) *3); // @audit rounding, yield lost foreverreturn a && b; }
It may not be a high amount for tokens with high decimals, but for other tokens like USDC (6 decimals) or a variant of EURO which I do not remember, but had 2 decimals, it can be a significant loss of yield.
Impact Details
Vanilla loss of yield, permanent as there is no way to take them back.
Proof of Concept
The runnable POC is the next one:
pragmasolidity 0.8.20;import {Test, console2} from"forge-std/Test.sol";import {StakingBonus} from"src/vesting/StakingBonus.sol";import {VestedZeroNFT} from"src/vesting/VestedZeroNFT.sol";import {LockerToken} from"src/locker/LockerToken.sol";import {LockerLP} from"src/locker/LockerLP.sol";import {OmnichainStaking} from"src/locker/OmnichainStaking.sol";import {PoolVoter} from"src/voter/PoolVoter.sol";import {LendingPoolGauge} from"src/voter/gauge/LendingPoolGauge.sol";import {RewardBase} from"src/voter/gauge/RewardBase.sol";import {ZeroLend} from"src/ZeroLendToken.sol";import {IVestedZeroNFT} from"src/interfaces/IVestedZeroNFT.sol";import {IERC20} from"openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";contractDummyGaugeisRewardBase {functioninit(address_zero,address_vesting) external {__RewardBase_init(_zero, _vesting); }functionrewardPerToken(IERC20 token) publicviewoverridereturns (uint256) {return0; }functionearned(IERC20 token,address account ) publicviewoverridereturns (uint256) {return0; }modifierupdateReward(IERC20 token,address account) override { _; }}contractPOCisTest {functiontest_POC() external {address bob =makeAddr("bob"); vm.startPrank(bob); StakingBonus bonus =newStakingBonus(); VestedZeroNFT vZero =newVestedZeroNFT(); LockerToken locker =newLockerToken(); LockerLP lockerLP =newLockerLP(); ZeroLend zero =newZeroLend(); OmnichainStaking staking =newOmnichainStaking(); PoolVoter voter =newPoolVoter(); DummyGauge dummyGauge1 =newDummyGauge(); DummyGauge dummyGauge2 =newDummyGauge(); LendingPoolGauge gauge =newLendingPoolGauge(address(dummyGauge1),address(dummyGauge2)); bonus.init(address(zero), address(locker), address(vZero), 5); // 5% bonus, as StakingBonus::calculateBonus does the maths /100 instead of /10000
vZero.init(address(zero),address(bonus)); locker.init(address(zero),address(staking),address(bonus)); staking.init(address(0),address(locker),address(lockerLP)); lockerLP.init(address(zero),address(staking),address(bonus)); voter.init(address(staking),address(zero)); dummyGauge1.init(address(zero),address(vZero)); dummyGauge2.init(address(zero),address(vZero)); zero.togglePause(false); zero.approve(address(gauge),1e18+3); // so that % 4 != 0 and the lost yield is 3 wei console2.log("\n\n"); console2.log("[\x1b[32m+\x1b[0m] ZERO balance of Bob before the notify =\x1b[31m", zero.balanceOf(bob), "\x1b[0m");
console2.log("[\x1b[32m+\x1b[0m] ZERO balance of LendingPoolGauge before the notify =\x1b[31m", zero.balanceOf(address(gauge)), "\x1b[0m");
console2.log(""); console2.log(" \x1b[33m-\x1b[0m Calling \x1b[32mLendingPoolGauge::notifyRewardAmount\x1b[0m with amount being \x1b[31m1e18 + 3\x1b[0m...");
gauge.notifyRewardAmount(address(zero),1e18+3); console2.log(""); console2.log("[\x1b[32m+\x1b[0m] ZERO balance of Bob after the notify =\x1b[31m", zero.balanceOf(bob), "\x1b[0m");
console2.log("[\x1b[32m+\x1b[0m] ZERO balance of LendingPoolGauge after the notify =\x1b[31m", zero.balanceOf(address(gauge)), "\x1b[0m");
console2.log("\n\n");require(zero.balanceOf(address(gauge)) !=0,"POC"); vm.stopPrank(); }}