Summary
When a user delegates boost to a pool, delegation amount is not updated correctly. Due to this, their boost amount cannot be updated due to underflow.
Vulnerability Details
When user delegates boost to the pool, delegation amount is not updated correctly
function delegateBoost(
address to,
uint256 amount,
uint256 duration
) external override nonReentrant {
if (paused()) revert EmergencyPaused();
if (to == address(0)) revert InvalidPool();
if (amount == 0) revert InvalidBoostAmount();
if (duration < MIN_DELEGATION_DURATION || duration > MAX_DELEGATION_DURATION)
revert InvalidDelegationDuration();
uint256 userBalance = IERC20(address(veToken)).balanceOf(msg.sender);
if (userBalance < amount) revert InsufficientVeBalance();
UserBoost storage delegation = userBoosts[msg.sender][to];
if (delegation.amount > 0) revert BoostAlreadyDelegated();
@> delegation.amount = amount;
delegation.expiry = block.timestamp + duration;
delegation.delegatedTo = to;
delegation.lastUpdateTime = block.timestamp;
emit BoostDelegated(msg.sender, to, amount, duration);
}
Because as we see in updateUserBoost, amount is user's boost, not veToken amount:
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
if (paused()) revert EmergencyPaused();
if (user == address(0)) revert InvalidPool();
if (!supportedPools[pool]) revert PoolNotSupported();
UserBoost storage userBoost = userBoosts[user][pool];
PoolBoost storage poolBoost = poolBoosts[pool];
uint256 oldBoost = userBoost.amount;
uint256 newBoost = _calculateBoost(user, pool, 10000);
@> userBoost.amount = newBoost;
userBoost.lastUpdateTime = block.timestamp;
if (newBoost >= oldBoost) {
poolBoost.totalBoost = poolBoost.totalBoost + (newBoost - oldBoost);
} else {
poolBoost.totalBoost = poolBoost.totalBoost - (oldBoost - newBoost);
}
poolBoost.workingSupply = newBoost;
poolBoost.lastUpdateTime = block.timestamp;
emit BoostUpdated(user, pool, newBoost);
emit PoolBoostUpdated(pool, poolBoost.totalBoost, poolBoost.workingSupply);
}
Moreover, we can see that poolBoostparameters are not updated correctly on user's delegation
poolBoost.workingSupplyis not increased by user boost
poolBoost.totalBoostis not increased by user boost
poolBoost.workingSuplplyis set to newBoostwhen updating user boost
Due to above reasons, updateUserBoostwill revert with underflow.
POC
pragma solidity ^0.8.19;
import "../lib/forge-std/src/Test.sol";
import {BoostController} from "../contracts/core/governance/boost/BoostController.sol";
import {veRAACToken} from "../contracts/core/tokens/veRAACToken.sol";
import {RAACToken} from "../contracts/core/tokens/RAACToken.sol";
import {MockPool} from "../contracts/mocks/core/pools/MockPool.sol";
contract BoostControllerTest is Test {
veRAACToken veToken;
RAACToken raacToken;
BoostController boostController;
address pool;
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address eve = makeAddr("eve");
function setUp() external {
raacToken = new RAACToken(address(this), 0, 0);
raacToken.setMinter(address(this));
veToken = new veRAACToken(address(raacToken));
boostController = new BoostController(address(veToken));
pool = address(new MockPool());
boostController.modifySupportedPool(pool, true);
vm.label(pool, "pool");
}
function testDelegationRemovalFailure() external {
_dealVeToken(alice, 1000e18);
vm.startPrank(alice);
boostController.delegateBoost(pool, 100e18, 7 days);
vm.stopPrank();
skip(7 days);
vm.startPrank(pool);
vm.expectRevert(stdError.arithmeticError);
boostController.updateUserBoost(alice, pool);
vm.stopPrank();
}
function _dealVeToken(address account, uint256 amount) internal {
if (amount == 0) {
return;
}
deal(address(raacToken), account, amount);
vm.startPrank(account);
raacToken.approve(address(veToken), amount);
veToken.lock(amount, veToken.MAX_LOCK_DURATION());
vm.stopPrank();
}
}
Impact
Tools Used
Manual Review, Foundry
Recommendations
Delegation amount should be set to user's boost amount