Core Contracts

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

Storage Inconsistency in veRAACToken's getLockEndTime Function

Summary

The veRAACToken.sol contract contains a storage inconsistency between two separate state variables that track lock information. The contract uses _lockState (with the LockManager library) to manage and update lock data, but the getLockEndTime() function incorrectly reads from a different, non-updated mapping (locks), causing it to return incorrect values (specifically, returning 0 instead of the actual lock end time).

Vulnerability Details

The contract maintains two separate data structures for tracking lock information:

A direct mapping:

mapping(address => Lock) public locks;

A structured storage variable with the LockManager library:

LockManager.LockState private _lockState;

When a user creates a lock via the lock() function, the contract correctly updates the _lockState variable:

_lockState.createLock(msg.sender, amount, duration);

However, the direct locks mapping is never updated. This becomes problematic when the getLockEndTime() function is called:

function getLockEndTime(address account) external view returns (uint256) {
return locks[account].end;
}

This function reads from the locks mapping rather than from _lockState, resulting in it always returning 0 (the default value) instead of the actual lock end time.

Impact

This inconsistency leads to incorrect data being returned from the getLockEndTime() function, which could mislead users or integrated protocols that rely on this function to determine when locks expire. However, since the getLockPosition().end works correctly and provides the same information, and the getLockEndTime() function does not appear to be used in critical protocol logic, the practical impact is minimal. This is a low severity issue that represents a correctness bug rather than a security vulnerability.

Proof of Code

  1. Convert the project into a foundry project, ensuring test in foundry.toml points to a designated test directory.

  2. Comment out the forking object from the hardhat.congif.cjs file:

networks: {
hardhat: {
mining: {
auto: true,
interval: 0
},
//forking: {
// url: process.env.BASE_RPC_URL,
//},
  1. Copy the following code into the test folder:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
//Tokens
import "contracts/core/tokens/RToken.sol";
import "contracts/core/tokens/RAACToken.sol";
import "contracts/core/tokens/veRAACToken.sol";
//Governance
import "contracts/core/collectors/FeeCollector.sol";
import "contracts/core/governance/boost/BoostController.sol";
import "contracts/core/governance/gauges/BaseGauge.sol";
import "contracts/core/governance/gauges/GaugeController.sol";
import "contracts/core/governance/gauges/RAACGauge.sol";
import "contracts/core/governance/gauges/RWAGauge.sol";
import "contracts/core/governance/proposals/Governance.sol";
import "contracts/core/governance/proposals/TimelockController.sol";
//3rd Party
import {Test, console} from "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockERC20 is ERC20 {
constructor() ERC20("Mock", "MCK") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
contract MasterGovernanceTest is Test {
RToken public rToken;
RAACToken public raacToken;
veRAACToken public veRaacToken;
// Mock token
MockERC20 public mockCrvUSD;
// Admin actors
address public protocolOwner = makeAddr("ProtocolOwner");
address public rTokenMinter = makeAddr("rTokenMinter");
address public rTokenBurner = makeAddr("rTokenBurner");
address public raacTokenMinter = makeAddr("RAACTokenMinter");
address public veRaacTokenMinter = makeAddr("veRaacTokenMinter");
// Users
address public Alice = makeAddr("Alice");
address public Bob = makeAddr("Bob");
// SetUp
function setUp() public {
vm.startPrank(protocolOwner);
// Set up RToken
mockCrvUSD = new MockERC20();
rToken = new RToken(
"RToken",
"RTKN",
protocolOwner,
address(mockCrvUSD)
);
rToken.setMinter(rTokenMinter);
rToken.setBurner(rTokenBurner);
// Set up RAACToken
raacToken = new RAACToken(
protocolOwner, // initialOwner
100, // initialSwapTaxRate - 1%
50 // initialBurnTaxRate - 0.5%
);
raacToken.setMinter(raacTokenMinter);
// Set up veRAACToken
veRaacToken = new veRAACToken(address(raacToken));
veRaacToken.setMinter(veRaacTokenMinter);
// Whitelist veRAACToken address so that fees are not issued on transfers
raacToken.manageWhitelist(address(veRaacToken), true);
vm.stopPrank();
}
function testFail_GetLockEndTimeNotWorking() public {
// Set up RAAC tokens for Alice
vm.startPrank(raacTokenMinter);
raacToken.mint(Alice, 1000e18);
vm.stopPrank();
// Alice approves veRAACToken contract to spend her RAAC tokens
vm.startPrank(Alice);
raacToken.approve(address(veRaacToken), type(uint256).max);
// Alice creates initial lock - 500 RAAC for 1 year
uint256 initialLockAmount = 500e18;
uint256 initialLockDuration = 365 days;
veRaacToken.lock(initialLockAmount, initialLockDuration);
// Get Alice's lock end time
IveRAACToken.LockPosition memory firstLockPosition = veRaacToken.getLockPosition(Alice);
console.log("First lock - End time:", firstLockPosition.end);
uint256 getLockTimeReturn = veRaacToken.getLockEndTime(Alice);
// Assert
assertEq(getLockTimeReturn, firstLockPosition.end);
}
}
  1. run forge test -vvvv, NOTE: this is a testFail test so it will "pass"

  2. Output:

├─ [1530] veRAACToken::getLockPosition(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea]) [staticcall]
│ └─ ← [Return] LockPosition({ amount: 500000000000000000000 [5e20], end: 31536001 [3.153e7], power: 125000000000000000000 [1.25e20] })
├─ [0] console::log("First lock - End time:", 31536001 [3.153e7]) [staticcall]
│ └─ ← [Stop]
├─ [2655] veRAACToken::getLockEndTime(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::assertEq(0, 31536001 [3.153e7]) [staticcall]
│ └─ ← [Revert] assertion failed: 0 != 31536001
└─ ← [Revert] assertion failed: 0 != 31536001

Recommendations

Modify the getLockEndTime function to read from _lockState:

function getLockEndTime(address account) external view returns (uint256) {
return _lockState.locks[account].end;
}
Updates

Lead Judging Commences

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

veRAACToken::getLockEndTime and getLockedBalance returns 0 by reading from unused locks mapping instead of _lockState, making lock expiry times unavailable to clients

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

veRAACToken::getLockEndTime and getLockedBalance returns 0 by reading from unused locks mapping instead of _lockState, making lock expiry times unavailable to clients

Support

FAQs

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