Core Contracts

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

Locked Funds Due to Token Burn Mechanism

Summary

The veRAACToken::withdraw() function fails when attempting to return locked tokens due to the RAAC token's burn and fee mechanism, which reduces the actual amount stored in the contract.

Vulnerability Details

When a user locks their RAAC tokens, the contract records the full amount deposited. However, the RAAC token has a 1.5% burn/fee mechanism applied on every transfer. This means that the contract only receives 98.5% of the intended deposit. When the user later attempts to withdraw, the contract tries to send back 100% of the locked amount, but it only holds 98.5%, leading to a failed transaction.

The issue arises from the veRAACToken::withdraw() function:

function withdraw() external nonReentrant {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert LockNotFound();
if (block.timestamp < userLock.end) revert LockNotExpired();
uint256 amount = userLock.amount;
uint256 currentPower = balanceOf(msg.sender);
// Clear lock data
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender];
// Update checkpoints
_checkpointState.writeCheckpoint(msg.sender, 0);
// Burn veTokens and transfer RAAC
_burn(msg.sender, currentPower);
raacToken.safeTransfer(msg.sender, amount); // ❌ This fails due to insufficient balance
emit Withdrawn(msg.sender, amount);
}

Since raacToken.safeTransfer(msg.sender, amount) attempts to send back the full deposit but the contract holds less due to the burn mechanism, this call fails.

Impact

Users are permanently unable to withdraw their locked funds, effectively locking their RAAC tokens in the contract forever.

Tools Used

  • Manual code review

  • Foundry for testing

Proof of Concept

contract BoostExploitTest is Test {
RAACToken raacToken;
veRAACToken veRAACTokenContract;
MockPool pool;
address owner;
BoostController boostContract;
address attacker = address(0xBEEF);
address public victim;
uint256 constant SWAP_TAX_RATE = 100; // 1%
uint256 constant BURN_TAX_RATE = 50; // 0.5%
function setUp() public {
owner = makeAddr("owner");
victim = makeAddr("vitcim");
vm.startPrank(owner);
raacToken = new RAACToken(owner, SWAP_TAX_RATE, BURN_TAX_RATE);
veRAACTokenContract = new veRAACToken(address(raacToken));
// Deploy BoostExploitPoC contract
boostContract = new BoostController(address(veRAACTokenContract));
pool = new MockPool();
// Add the pool to supported pools
raacToken.setMinter(owner);
boostContract.modifySupportedPool(address(pool), true);
raacToken.mint(attacker, 100 ether);
vm.stopPrank();
}
function testExploit_CannotWithdraw() public {
vm.prank(attacker);
raacToken.approve(address(veRAACTokenContract), 100 ether);
vm.prank(attacker);
veRAACTokenContract.lock(100 ether, 365 * 24 * 3600);
// Simulate passage of time: veTokens expire
vm.warp(block.timestamp + 365 days);
vm.prank(attacker);
vm.expectRevert();
veRAACTokenContract.withdraw();
}
}

Recommendations

  • Modify the veRAACToken::lock() function to account for the actual balance received after the burn fee.

  • Consider using an ERC20 token without a burn/fee mechanism.

  • If the burn mechanism must be kept, explicitly warn users that they will receive less than the initially locked amount.

Updates

Lead Judging Commences

inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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