Core Contracts

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

`locks` mapping in `veRAACToken` contract is not updated in key functions

Summary

Whenever users lock tokens, increase their locked tokens amount, or extend their lock duration, it is expected that these details are updated in the locks mapping.

/**
* @notice Mapping of user addresses to their lock positions
*/
mapping(address => Lock) public locks;

But, this mapping is never updated.

The veRAACToken updates user details into the private _lockState mapping instead. Which is a library state.

/**
* @notice State for managing lock positions
*/
LockManager.LockState private _lockState;

Impact

Reading from the veRAACToken to verify user details will always return wrong details and results.

This is particular harmful because, if an external contract needs to verify user details by calling getter functions, it will get back wrong details.

/**
* @notice Gets the amount of RAAC tokens locked by an account
* @dev Returns the raw locked token amount without time-weighting
* @param account The address to check
* @return The amount of RAAC tokens locked by the account
*/
function getLockedBalance(address account) external view returns (uint256) {
return locks[account].amount;
}
/**
* @notice Gets the lock end time for an account
* @dev Returns the timestamp when the lock expires
* @param account The address to check
* @return The unix timestamp when the lock expires
*/
function getLockEndTime(address account) external view returns (uint256) {
return locks[account].end;
}

PoC

Here is a test to prove this finding:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console2} from "forge-std/Test.sol";
import {veRAACToken} from "../contracts/core/tokens/veRAACToken.sol";
import {RAACMockERC20} from "../contracts/mocks/core/tokens/RAACMockERC20.sol";
contract TestVeRAACToken is Test {
veRAACToken vrc;
RAACMockERC20 mrt; // mock RAACERC20 token
address owner = address(0x1);
address user = address(0x2);
function setUp() public {
vm.startPrank(owner);
mrt = new RAACMockERC20(owner);
vrc = new veRAACToken(address(mrt));
mrt.mintTo(user, 10_000e18); // mint 10k mrt to user
vm.stopPrank();
}
function testUserDetailsNotUpdated() public {
uint256 amountToLock = 1_000e18;
uint256 timeToLock = 31_536_000 seconds; // 1 year
// user locks tokens
vm.startPrank(user);
mrt.approve(address(vrc), amountToLock);
vrc.lock(amountToLock, timeToLock);
vm.stopPrank();
uint256 userLockedAmount = vrc.getLockedBalance(user);
uint256 userLockedTime = vrc.getLockEndTime(user);
console2.log("This is the locked balance of the user: ", userLockedAmount);
console2.log("This is the duration the user locked their funds for: ", userLockedTime);
assert(amountToLock == userLockedAmount);
assert((timeToLock + block.timestamp) == userLockedTime);
}
}

And these are the traces:

[509948] TestVeRAACToken::testUserDetailsNotUpdated()
├─ [0] VM::startPrank(SHA-256: [0x0000000000000000000000000000000000000002])
│ └─ ← [Return]
├─ [25296] RAACMockERC20::approve(veRAACToken: [0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1], 1000000000000000000000 [1e21])
│ ├─ emit Approval(owner: SHA-256: [0x0000000000000000000000000000000000000002], spender: veRAACToken: [0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1], value: 1000000000000000000000 [1e21])
│ └─ ← [Return] true
├─ [455613] veRAACToken::lock(1000000000000000000000 [1e21], 31536000 [3.153e7])
│ ├─ [31614] RAACMockERC20::transferFrom(SHA-256: [0x0000000000000000000000000000000000000002], veRAACToken: [0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1], 1000000000000000000000 [1e21])
│ │ ├─ emit Transfer(from: SHA-256: [0x0000000000000000000000000000000000000002], to: veRAACToken: [0x535B3D7A252fa034Ed71F0C53ec0C6F784cB64E1], value: 1000000000000000000000 [1e21])
│ │ └─ ← [Return] true
│ ├─ emit LockCreated(user: SHA-256: [0x0000000000000000000000000000000000000002], amount: 1000000000000000000000 [1e21], unlockTime: 31536001 [3.153e7])
│ ├─ emit PeriodCreated(startTime: 1, duration: 604800 [6.048e5], initialValue: 0)
│ ├─ emit VotingPowerUpdated(user: SHA-256: [0x0000000000000000000000000000000000000002], oldPower: 0, newPower: 250000000000000000000 [2.5e20])
│ ├─ emit CheckpointCreated(user: SHA-256: [0x0000000000000000000000000000000000000002], blockNumber: 1, power: 250000000000000000000 [2.5e20])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SHA-256: [0x0000000000000000000000000000000000000002], value: 250000000000000000000 [2.5e20])
│ ├─ emit LockCreated(user: SHA-256: [0x0000000000000000000000000000000000000002], amount: 1000000000000000000000 [1e21], unlockTime: 31536001 [3.153e7])
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [2989] veRAACToken::getLockedBalance(SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
│ └─ ← [Return] 0
├─ [2947] veRAACToken::getLockEndTime(SHA-256: [0x0000000000000000000000000000000000000002]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("This is the locked balance of the user: ", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("This is the duration the user locked their funds for: ", 0) [staticcall]
│ └─ ← [Stop]
└─ ← [Revert] panic: assertion failed (0x01)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 6.74ms (2.49ms CPU time)

Tools Used

  • Foundry

Recommendations

There are two approaches to be considered for mitigation:

  • Read directly from the library mapping _lockState to get user lock details, or

/**
* @notice Gets the amount of RAAC tokens locked by an account
* @dev Returns the raw locked token amount without time-weighting
* @param account The address to check
* @return The amount of RAAC tokens locked by the account
*/
function getLockedBalance(address account) external view returns (uint256) {
- return locks[account].amount;
+ LockManager.Lock memory userLock = _lockState.locks[account]; // read directly from _lockState mapping
+ return userLock.amount;
}
/**
* @notice Gets the lock end time for an account
* @dev Returns the timestamp when the lock expires
* @param account The address to check
* @return The unix timestamp when the lock expires
*/
function getLockEndTime(address account) external view returns (uint256) {
- return locks[account].end;
+ LockManager.Lock memory userLock = _lockState.locks[account]; // read directly from _lockState mapping
+ return userLock.end;
}
  • Update veRAACToken locks mapping in ::lock(), ::increase(), and ::extend() functions

/**
* @notice Creates a new lock position for RAAC tokens
* @dev Locks RAAC tokens for a specified duration and mints veRAAC tokens representing voting power
* @param amount The amount of RAAC tokens to lock
* @param duration The duration to lock tokens for, in seconds
*/
function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
...
...
// Calculate unlock time
uint256 unlockTime = block.timestamp + duration;
// Create lock position
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
+ locks[msg.sender] = Lock({amount: amount, end: unlockTime}); // Update the locks mapping in the veRAACToken contract
...
...
}
/**
* @notice Increases the amount of locked RAAC tokens
* @dev Adds more tokens to an existing lock without changing the unlock time
* @param amount The additional amount of RAAC tokens to lock
*/
function increase(uint256 amount) external nonReentrant whenNotPaused {
...
...
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
+ locks[msg.sender].amount = userLock.amount; // Update locks mapping in the veRAACToken contract
...
...
}
/**
* @notice Extends the duration of an existing lock
* @dev Increases the lock duration which results in updated voting power
* @param newDuration The new total duration extension for the lock, in seconds
*/
function extend(uint256 newDuration) external nonReentrant whenNotPaused {
// Extend lock using LockManager
uint256 newUnlockTime = _lockState.extendLock(msg.sender, newDuration);
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
+ locks[msg.sender].end = newUnlockTime; // Update locks mapping in the veRAACToken contract
...
...
}

Either of these approaches will mitigate this issue and ensure that user details are correctly read from the veRAACToken contract.

Updates

Lead Judging Commences

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

veRAACToken::increase uses locks[msg.sender] instead of _lockState.locks[msg.sender] inside _updateBoostState call

Support

FAQs

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

Give us feedback!