Summary
The delegateBoost function lets users give their boost power to someone else, but the contract doesn’t actually let the recipient use it. The delegated boost gets saved, but it’s ignored when calculating or applying boosts, making the delegation feature useless.
Vulnerability Details
The delegateBoost function lets one user (the delegator) give boost power to another user (the recipient).
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);
}
-
It checks if the delegator (msg.sender) has enough veTokens and saves the delegation in userBoosts[msg.sender][to] with:
-
But that’s it, it doesn’t tell the rest of the contract to use this boost for the recipient.
The _calculateBoost function decides how much boost a user gets:
function _calculateBoost(
address user,
address pool,
uint256 amount
) internal view returns (uint256) {
if (amount == 0) revert InvalidBoostAmount();
if (!supportedPools[pool]) revert PoolNotSupported();
uint256 userBalance = IERC20(address(veToken)).balanceOf(user);
uint256 totalSupply = IERC20(address(veToken)).totalSupply();
if (userBalance == 0 || totalSupply == 0) {
return amount;
}
}
-
It only looks at userBalance—the veTokens the user owns themselves. It doesn’t check userBoosts[someone][user] to see if anyone delegated boost to this user.
-
Same thing in calculateBoost, which uses veToken.getVotingPower(user, block.timestamp)—still just the user’s own power.
-
The updateUserBoost function puts the boost into a pool:
function updateUserBoost(address user, address pool) external override nonReentrant whenNotPaused {
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;
}
newBoost comes from _calculateBoost, which ignores delegations.
It updates userBoosts[user][pool], but doesn’t look at userBoosts[delegator][user] to add any delegated boost.
The delegated boost gets stuck in userBoosts[msg.sender][to] and never reaches the recipient’s actual boost power. The contract has a `removeBoostDelegation function that subtracts the delegated amount from the recipient’s pool when it expires
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;
}
}
This shows the delegated boost was meant to help the recipient’s pool—but since it’s never added anywhere, this subtraction either does nothing (if totals are too low) or messes up the pool unfairly.
Impact
Users can delegate boost, but it doesn’t help the recipient get more rewards or power in pools. The whole point of delegation fails.
People might delegate expecting it to work (since the contract lets them), then get upset when it does nothing.
When delegations expire, removeBoostDelegation tries to subtract the boost from the recipient’s pool totals. Since it was never added, this could unfairly lower totals or just be ignored, depending on the numbers.
Tools Used
Manual Review
Recommendations
When delegateBoost runs, add the amount to the recipient’s userBoosts[to][pool] for a specific pool (needs a pool parameter)
function delegateBoost(address to, uint256 amount, uint256 duration, address pool) external {
UserBoost storage delegation = userBoosts[msg.sender][to];
delegation.amount = amount;
delegation.expiry = block.timestamp + duration;
delegation.delegatedTo = to;
delegation.lastUpdateTime = block.timestamp;
UserBoost storage recipientBoost = userBoosts[to][pool];
recipientBoost.amount += amount;
PoolBoost storage poolBoost = poolBoosts[pool];
poolBoost.totalBoost += amount;
poolBoost.workingSupply += amount;
}