Core Contracts

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

Storage Inconsistency in veRAACToken's getLockedBalance Function

Summary

The veRAACToken.sol contract exhibits a storage inconsistency in the getLockedBalance() function. This function incorrectly reads lock data from a mapping that is never updated during lock operations, causing it to return 0 instead of the actual locked token amount. This creates a discrepancy where getLockPosition() returns the correct locked amount while getLockedBalance() always returns 0.

Vulnerability Details

The veRAACToken.sol contract maintains two separate storage structures for tracking lock information:

  1. A direct mapping defined in the contract:

mapping(address => Lock) public locks;
  1. A structured storage variable that uses the LockManager library:

LockManager.LockState private _lockState;

The issue is that when a user creates a lock through the lock() function, only the _lockState variable is updated:

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

The getLockedBalance() function incorrectly reads from the direct locks mapping:

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

Since this mapping is never updated during lock creation, the function always returns 0 regardless of the actual locked amount.

Impact

This inconsistency means that any user or protocol relying on the getLockedBalance() function will receive incorrect information (0) about locked token amounts. Since getLockPosition() provides correct information, this is unlikely to cause financial loss, but it could lead to confusion, inaccurate display of locked token amounts in UIs, or integration issues with protocols that rely on getLockedBalance(). As the function does not appear to be used in critical protocol logic, this is assessed as a low severity issue representing a correctness bug rather than a security vulnerability.

Proof of Concept

  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");
// Constants
uint256 constant MIN_LOCK_DURATION = 365 days;
uint256 constant MAX_LOCK_DURATION = 1460 days; // 4 years
uint256 constant LOCK_AMOUNT = 1000 ether;
// 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);
// Mint RAAC tokens to Alice for testing
vm.startPrank(raacTokenMinter);
raacToken.mint(Alice, LOCK_AMOUNT);
vm.stopPrank();
vm.stopPrank();
}
function test_IncorrectBalanceReturn() public {
// Setup: Alice creates an initial lock for 1 year
uint256 initialLockDuration = 365 days;
vm.startPrank(Alice);
// Approve veRaacToken to spend Alice's RAAC tokens
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
// Create initial lock
veRaacToken.lock(LOCK_AMOUNT, initialLockDuration);
// getLockedPBalance
uint256 aliceGetLockBalane = veRaacToken.getLockedBalance(Alice);
console.log("Alice Balance (getLockBalance()): ", aliceGetLockBalane);
// get actual balance
veRAACToken.LockPosition memory aliceLockPosition = veRaacToken.getLockPosition(Alice);
console.log("Alice Balance (getLockPosition().amount): ", aliceLockPosition.amount);
}
}
  1. Run forge test -vv

  2. Output:

Logs:
Alice Balance (getLockBalance()): 0
Alice Balance (getLockPosition().amount): 1000000000000000000000

Recommendations

Modify the getLockedBalance function to read from _lockState:

function getLockedBalance(address account) external view returns (uint256) {
return _lockState.getLock(account).amount;
}
Updates

Lead Judging Commences

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

veRAACToken::getLockPosition incorrectly reports user voting power by returning raw token balance instead of time-decayed value, causing UI/frontend display inconsistencies

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

veRAACToken::getLockPosition incorrectly reports user voting power by returning raw token balance instead of time-decayed value, causing UI/frontend display inconsistencies

Support

FAQs

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