Summary
The BoostController contract allows users to delegate more boost than their actual veToken balance by not tracking the total amount of boost delegated across all recipients. This enables users to perform multiple delegations of their full balance to different addresses, effectively creating unlimited voting power from a limited veToken balance.
Vulnerability Details
The vulnerability exists in the delegateBoost function of the BoostController contract. While the function checks if a user has sufficient veToken balance for each individual delegation and prevents double delegation to the same recipient, it fails to track the cumulative amount of boost delegated across all recipients.
Here's the vulnerable code:
function delegateBoost(address to, uint256 amount, uint256 duration) external override nonReentrant {
...
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;
...
}
The issue can be exploited as follows:
User has 100 veTokens
User delegates 100 boost to recipient
A User delegates another 100 boost to recipient B
This process can be repeated for unlimited recipients
This was confirmed by the following test in governance/boost/BoostController.test.js:
it("should allow double-delegation by not tracking total available boost", async function () {
const { boostController, veToken, user1, user2, owner } = await loadFixture(deployFixture);
const boostAmount = ethers.parseEther("100");
await veToken.mint(user1.address, boostAmount);
await boostController.connect(user1).delegateBoost(user2.address, boostAmount, 7 * 24 * 3600);
await boostController.connect(user1).delegateBoost(owner.address, boostAmount, 7 * 24 * 3600);
const delegationToUser2 = await boostController.getUserBoost(user1.address, user2.address);
const delegationToOwner = await boostController.getUserBoost(user1.address, owner.address);
expect(delegationToUser2.amount).to.equal(boostAmount);
expect(delegationToOwner.amount).to.equal(boostAmount);
});
Impact
Critical. This vulnerability allows:
Infinite multiplication of voting power
Manipulation of governance decisions
Unfair advantages in boost-based rewards or incentives
Undermining of the entire veToken-based governance system
Tools Used
Manual code review
Unit tests with Hardhat
Recommendations
Add tracking of total delegated boost per user:
contract BoostController {
mapping(address => uint256) public totalDelegatedBoost;
function delegateBoost(address to, uint256 amount, uint256 duration) external {
uint256 userBalance = IERC20(address(veToken)).balanceOf(msg.sender);
if (totalDelegatedBoost[msg.sender] + amount > userBalance)
revert InsufficientVeBalance();
UserBoost storage delegation = userBoosts[msg.sender][to];
if (delegation.amount > 0) revert BoostAlreadyDelegated();
totalDelegatedBoost[msg.sender] += amount;
delegation.amount = amount;
delegation.expiry = block.timestamp + duration;
}
function undelegateBoost(address from) external {
totalDelegatedBoost[msg.sender] -= delegation.amount;
}
}