Summary
delegateBoost::BoostController.sol allows a user (msg.sender) to delegate a portion of their veToken balance (amount) to a pool (to) for a specified duration, recording this in userBoosts[msg.sender][to]. However, it does not update the corresponding PoolBoost storage (poolBoosts[to]), which tracks the total boost and working supply for the pool. In contrast, removeBoostDelegation updates poolBoosts[msg.sender] (where msg.sender is the pool) by subtracting the delegated amount when a delegation is removed. This inconsistency means that delegateBoost fails to reflect the delegated boost in the pool’s aggregate state, potentially breaking the protocol’s boost tracking and application logic.
Vulnerability Details
delegateBoost::BoostController.sol
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);
}
removeBoostDelegation::BoostController.sol
function removeBoostDelegation(address from) external override nonReentrant {
UserBoost storage delegation = userBoosts[from][msg.sender];
if (delegation.delegatedTo != msg.sender) revert DelegationNotFound();
if (delegation.expiry > block.timestamp) revert InvalidDelegationDuration();
>> PoolBoost storage poolBoost = poolBoosts[msg.sender];
if (poolBoost.totalBoost >= delegation.amount) {
poolBoost.totalBoost -= delegation.amount;
}
if (poolBoost.workingSupply >= delegation.amount) {
poolBoost.workingSupply -= delegation.amount;
}
poolBoost.lastUpdateTime = block.timestamp;
emit DelegationRemoved(from, msg.sender, delegation.amount);
delete userBoosts[from][msg.sender];
}
Root Cause
Omitted Update: delegateBoost records the delegation in userBoosts[msg.sender][to] but does not increment poolBoosts[to].totalBoost or poolBoosts[to].workingSupply, leaving the pool’s aggregate boost state unchanged.
Inconsistent Design: removeBoostDelegation updates poolBoosts[msg.sender] to reflect the removal of a delegation, implying PoolBoost tracks active boosts. The absence of a corresponding update in delegateBoost breaks this symmetry.
Impact
Stale Pool Boost State
Inconsistency with Removal: removeBoostDelegation deducts from poolBoosts[msg.sender], but without prior increments in delegateBoost, these values may already be inaccurate or zero, leading to underflow checks passing trivially or state desync
Tools Used
Manual
Recommendations
Update PoolBoost in delegateBoost:
Add poolBoosts[to] updates to reflect the delegated amount:
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;
+ PoolBoost storage poolBoost = poolBoosts[to];
+ poolBoost.totalBoost += amount;
+ poolBoost.workingSupply += amount; // Adjust based on protocol logic
+ poolBoost.lastUpdateTime = block.timestamp;
emit BoostDelegated(msg.sender, to, amount, duration);
}