Core Contracts

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

Overwriting Existing Locks Causes Loss of Tokens in veRAACToken Contract

Summary

The veRAACToken contract contains a vulnerability where creating a new lock position for RAAC tokens overwrites the existing lock, causing a loss of tokens. This issue arises because the lock function does not account for existing locks and simply replaces them with the new lock, leading to the loss of previously locked tokens.

Vulnerability Details

The vulnerability is found in the lock function of the veRAACToken contract. When a user creates a new lock, the function overwrites the existing lock position without preserving the previously locked tokens. This results in the loss of tokens that were previously locked by the user.

function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
if (duration < MIN_LOCK_DURATION || duration > MAX_LOCK_DURATION)
revert InvalidLockDuration();
// Do the transfer first - this will revert with ERC20InsufficientBalance if user doesn't have enough tokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
// Calculate unlock time
uint256 unlockTime = block.timestamp + duration;
// Create lock position
_lockState.createLock(msg.sender, amount, duration);
// audit voting power state calculating before new amount is added. This new amount is not used anywhere
// its reliant on the voting state shouldnt it be after calculate voting power
_updateBoostState(msg.sender, amount);
// Calculate initial voting power
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
amount,
unlockTime
);
// Update checkpoints
uint256 newPower = uint256(uint128(bias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Mint veTokens
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}

Steps to Reproduce

Create Initial Lock: A user locks a certain amount of RAAC tokens for a specified duration.
Create New Lock: The same user creates a new lock with a different amount and/or duration.
Observe Token Loss: The user's previous lock is overwritten, and the previously locked tokens are lost.

Example

// User creates an initial lock
veRAACToken.lock(1000 * 1e18, 365 days);
// User creates a new lock
veRAACToken.lock(500 * 1e18, 730 days);
// The user's previous lock of 1000 RAAC tokens is overwritten, causing a loss of tokens

Impact

The impact of this vulnerability is significant as it causes users to lose their previously locked tokens when they create a new lock. This can lead to substantial financial losses for users and erode trust in the protocol.

Tools Used

Solidity: The programming language used to write the smart contracts.
Manual Code Review: Reviewing the code to identify potential issues.
Foundry Tests: For POC

Recommendations

To fix this vulnerability, the lock function should be modified to account for existing locks and preserve the previously locked tokens. Here are some recommendations:

Check for Existing Locks: Before creating a new lock, check if the user already has an existing lock and handle it appropriately.
Combine Locks: If the user has an existing lock, combine the new lock with the existing lock to preserve the previously locked tokens.

LockManager.Lock memory existingLock = _lockState.getLock(msg.sender);
if(!existingLock) revert LockAlreadyCreated()

POC

Commands to run in the terminal

mkdir ../raacFoundry
cd ../raacFoundry
forge init
rm src/Counter.sol test/Counter.t.sol script/Counter.s.sol
cp -r ../2025-02-raac/contracts src
touch test/Test.sol
npm install @openzeppelin/contracts
npm install @openzeppelin/contracts
npm install @openzeppelin/contracts-upgradeable
npm install @chainlink/contracts
forge test

Add this to the forge.toml file

[profile.default]
src = "src"
out = "out"
libs = ['lib', 'node_modules']
remappings = [
"@openzeppelin/=node_modules/@openzeppelin/",
"@chainlink/=node_modules/@chainlink/"
]

Add this to the test/Test.sol file

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/contracts/core/tokens/RAACToken.sol";
import "../src/contracts/core/tokens/veRAACToken.sol";
import "../src/contracts/core/collectors/FeeCollector.sol";
contract FeeCollectorTest is Test {
RAACToken raacToken;
veRAACToken veRaacToken;
FeeCollector feeCollector;
address owner;
address user1;
address user2;
address treasury;
address newTreasury;
address repairFund;
address emergencyAdmin;
uint256 constant BASIS_POINTS = 10000;
uint256 constant WEEK = 7 * 24 * 3600;
uint256 constant ONE_YEAR = 365 * 24 * 3600;
uint256 constant INITIAL_MINT = 10000;
uint256 constant SWAP_TAX_RATE = 100; // 1%
uint256 constant BURN_TAX_RATE = 50; // 0.5%
struct FeeType {
uint256 veRAACShare;
uint256 burnShare;
uint256 repairShare;
uint256 treasuryShare;
}
function setUp() public {
owner = address(this);
user1 = address(1);
user2 = address(2);
treasury = address(3);
newTreasury = address(4);
repairFund = address(5);
emergencyAdmin = address(6);
raacToken = new RAACToken(owner, SWAP_TAX_RATE, BURN_TAX_RATE);
veRaacToken = new veRAACToken(address(raacToken));
feeCollector = new FeeCollector(address(raacToken), address(veRaacToken), treasury, repairFund, owner);
raacToken.setFeeCollector(address(feeCollector));
raacToken.manageWhitelist(address(feeCollector), true);
raacToken.manageWhitelist(address(veRaacToken), true);
raacToken.setMinter(owner);
veRaacToken.setMinter(owner);
feeCollector.grantRole(feeCollector.FEE_MANAGER_ROLE(), owner);
feeCollector.grantRole(feeCollector.EMERGENCY_ROLE(), emergencyAdmin);
feeCollector.grantRole(feeCollector.DISTRIBUTOR_ROLE(), owner);
raacToken.mint(owner, INITIAL_MINT * 100);
raacToken.approve(address(feeCollector), INITIAL_MINT * 100);
raacToken.mint(user1, INITIAL_MINT);
raacToken.mint(user2, INITIAL_MINT);
raacToken.mint(address(feeCollector), INITIAL_MINT * 100); // Mint tokens to the FeeCollector
vm.prank(user1);
raacToken.approve(address(feeCollector), type(uint256).max);
vm.prank(user2);
raacToken.approve(address(feeCollector), type(uint256).max);
//FeeType defaultFeeType = ;
for (uint8 i = 0; i < 8; i++) {
feeCollector.updateFeeType(i, IFeeCollector.FeeType(5000, 1000, 1000, 3000));
}
vm.prank(user1);
raacToken.approve(address(veRaacToken), 10000);
}
function testOverwriteLock() public {
uint256 startingRaacBalance = raacToken.balanceOf(user1);
console.log("Starting RAAC balance:", startingRaacBalance);
// User1 locks 100 RAAC tokens for 365 days
vm.prank(user1);
raacToken.approve(address(veRaacToken), 100);
vm.prank(user1);
veRaacToken.lock(100, 365 days);
// Check the balance of veRAAC tokens for user1
uint256 initialVeRaacBalance = veRaacToken.balanceOf(user1);
console.log("Initial veRAAC balance after first lock:", initialVeRaacBalance);
// User1 locks another 50 RAAC tokens for 365 days, overwriting the previous lock
vm.prank(user1);
raacToken.approve(address(veRaacToken), 50);
vm.prank(user1);
veRaacToken.lock(50, 365 days);
// Check the balance of veRAAC tokens for user1 after overwriting the lock
uint256 finalVeRaacBalance = veRaacToken.balanceOf(user1);
console.log("Final veRAAC balance after overwriting lock:", finalVeRaacBalance);
// User1 withdraws the locked tokens after the lock period
forwardTime(365 days);
vm.prank(user1);
veRaacToken.withdraw();
// Check the RAAC balance of user1 after withdrawal
uint256 raacBalanceAfterWithdrawal = raacToken.balanceOf(user1);
console.log("RAAC balance after withdrawal:", raacBalanceAfterWithdrawal);
// Log the difference in RAAC balance before and after withdrawal
uint256 raacBalanceDiff =startingRaacBalance - raacBalanceAfterWithdrawal;
console.log("Difference in RAAC balance after withdrawal:", raacBalanceDiff);
// Log the total lost amount
uint256 totalLost = (100 + 50) - raacBalanceDiff;
console.log("Total lost amount:", totalLost);
}
uint256 currentTime = block.timestamp;
function forwardTime(uint256 addTime) internal {
currentTime += addTime;
vm.warp(currentTime);
}
Updates

Lead Judging Commences

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

veRAACToken::lock called multiple times, by the same user, leads to loss of funds

Support

FAQs

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

Give us feedback!