Core Contracts

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

Improper Voting State Cleanup in Withdrawal Function

Summary

The veRAACToken contract fails to properly clean up voting power decay parameters (slope changes) when users withdraw funds via both normal and emergency withdrawal functions. This leaves corrupted voting power decay calculations in the system, leading to permanently inaccurate total voting power tracking.

Vulnerability Details

Problematic Code in Withdraw Functions

function withdraw() external nonReentrant {
// ... existing code ...
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender]; // Deletes user's voting points
// MISSING: Slope change cleanup
}
function emergencyWithdraw() external nonReentrant {
// ... existing code ...
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender]; // Same issue here
// MISSING: Slope change cleanup
}

Key Missing Components
Slope Change Tracking: The VotingPowerLib tracks slope changes at unlock times to calculate voting power decay:
mapping(uint256 => int128) slopeChanges; // Tracks slope changes per timestamp

  1. Uncleaned Slopes: When deleting a user's voting points, the contract fails to:
    Retrieve the user's existing slope value
    Subtract it from slopeChanges at their unlock time
    Update the global voting power decay calculations

Impact

  • Total voting power becomes inaccurate, potentially allowing malicious actors to exploit corrupted state

  • Residual slopes cause continuous voting power decay calculations to include withdrawn positions

Proof of concept

pragma solidity ^0.8.19;
import "../../core/tokens/veRAACToken.sol";
contract VeRAACTokenTestHelper is veRAACToken {
constructor(address _raacToken) veRAACToken(_raacToken) {}
// Expose slope change for testing by passing in the lock end time.
function getSlopeChange(uint256 unlockTime) external view returns (int128) {
// Return the slope change value from the VotingPowerLib state.
return _votingState.slopeChanges[unlockTime];
}
}
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("veRAACToken Emergency Withdraw Slope Cleanup PoC", function () {
let ERC20Mock, raacToken, VeRAACTokenTestHelper, veRAACToken;
let owner, user;
const lockAmount = ethers.utils.parseEther("1000");
const oneYear = 365 * 24 * 60 * 60;
const fourYears = 1460 * 24 * 60 * 60;
beforeEach(async function () {
[owner, user] = await ethers.getSigners();
// Deploy an ERC20 mock token for RAAC
ERC20Mock = await ethers.getContractFactory("ERC20Mock");
raacToken = await ERC20Mock.deploy("RAAC Token", "RAAC", owner.address, ethers.utils.parseEther("1000000"));
await raacToken.deployed();
// Transfer tokens to user and approve
await raacToken.transfer(user.address, ethers.utils.parseEther("10000"));
// Deploy the VeRAACTokenTestHelper
VeRAACTokenTestHelper = await ethers.getContractFactory("VeRAACTokenTestHelper");
veRAACToken = await VeRAACTokenTestHelper.connect(owner).deploy(raacToken.address);
await veRAACToken.deployed();
// User approves veRAACToken contract to spend RAAC tokens
await raacToken.connect(user).approve(veRAACToken.address, ethers.utils.parseEther("10000"));
});
it("should leave residual slope changes after emergencyWithdraw", async function () {
// user locks tokens for four years
await veRAACToken.connect(user).lock(lockAmount, fourYears);
// Get user's lock end time from the contract (using the test helper getLockPosition)
const lockPosition = await veRAACToken.getLockPosition(user.address);
const unlockTime = lockPosition.end;
// Check initial slope change is set (non-zero)
const initialSlope = await veRAACToken.getSlopeChange(unlockTime);
expect(initialSlope).to.be.gt(0);
// Schedule and enable emergency withdraw
const EMERGENCY_WITHDRAW_ACTION = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("enableEmergencyWithdraw"));
await veRAACToken.connect(owner).scheduleEmergencyAction(EMERGENCY_WITHDRAW_ACTION);
// Increase time by EMERGENCY_DELAY
await ethers.provider.send("evm_increaseTime", [3 * 24 * 60 * 60]);
await ethers.provider.send("evm_mine");
await veRAACToken.connect(owner).enableEmergencyWithdraw();
// Execute emergencyWithdraw as user
await veRAACToken.connect(user).emergencyWithdraw();
// After emergency withdraw, the lock and points cleanup occur
// However, the slope change cleanup is missing the subtraction – expect residual drift (non-zero)
const residualSlope = await veRAACToken.getSlopeChange(unlockTime);
expect(residualSlope).to.not.eq(0);
console.log("Initial slope:", initialSlope.toString());
console.log("Residual slope after emergencyWithdraw:", residualSlope.toString());
});
});

Recommendations

function withdraw() external nonReentrant {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
// Get existing slope before deletion
int128 oldSlope = _votingState.points[msg.sender].slope;
// Clean up slope changes
_votingState.slopeChanges[userLock.end] -= oldSlope;
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender];
// Update boost state
_boostState.updateBoostPeriod();
}
function emergencyWithdraw() external nonReentrant {
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
int128 oldSlope = _votingState.points[msg.sender].slope;
_votingState.slopeChanges[userLock.end] -= oldSlope;
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender];
_boostState.updateBoostPeriod();
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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