Core Contracts

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

Incorrect Voting Power Reporting in `veRAACToken.sol::getLockPosition` Function

Summary

The veRAACToken.sol::getLockPosition() function incorrectly reports a user's voting power by returning their raw veRAAC token balance instead of calculating the time-decayed voting power. This creates a discrepancy between the reported voting power and the actual voting power used in governance operations.

Vulnerability Details

The veRAAC token system implements a Curve-style voting mechanism where voting power decays linearly over time until the lock expiry. The getLockPosition() function returns balanceOf(account) as the power value, which doesn't account for the time-decay calculation that governs actual voting influence in the system.

function getLockPosition(address account) external view override returns (LockPosition memory) {
LockManager.Lock memory userLock = _lockState.getLock(account);
return LockPosition({
amount: userLock.amount,
end: userLock.end,
@> power: balanceOf(account) // Incorrect: returns raw token balance instead of time-decayed power
});
}

Impact

Low - This inconsistency primarily affects UI/frontend applications and users monitoring their voting power through this function. It could cause users to see incorrect information about their current voting influence, but doesn't affect the actual governance mechanisms since those use the proper time-decayed calculations directly.

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 test 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_IncorrectPowerReturn() 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);
// Fast forward some time (6 months)
vm.warp(block.timestamp + 180 days);
// getLockPosition.power
veRAACToken.LockPosition memory aliceLockPosition = veRaacToken.getLockPosition(Alice);
console.log("Alice power after 180 days (getLockPosition().power): ", aliceLockPosition.power);
// get actual voting power
uint256 actualVotingPower = veRaacToken.getVotingPower(Alice, block.timestamp);
console.log("Alice power after 180 days (getVotingPower): ", actualVotingPower);
assertGt(aliceLockPosition.power, actualVotingPower);
}
  1. Run forge test -vv

  2. Output:

Logs:
Alice power after 180 days (getLockPosition().power): 250000000000000000000
Alice power after 180 days (getVotingPower): 126712328767125568000

Recommendations

For consistency and clarity, modify the getLockPosition function to correctly return the time-decayed voting power:

function getLockPosition(address account) external view override returns (LockPosition memory) {
LockManager.Lock memory userLock = _lockState.getLock(account);
return LockPosition({
amount: userLock.amount,
end: userLock.end,
@> power: _votingState.getCurrentPower(account, block.timestamp)
});
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 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 4 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.