Core Contracts

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

Incomplete Storage Clearance Allows Residual Data to Persist, Impacting Voting Power, GAS Cost storage bloat, and Boost Calculation

Summary

The veRAACToken contract includes two functions, withdraw and emergencyWithdraw, which allow users to retrieve their locked tokens and clear storage upon successful withdrawal. While these functions correctly delete a user’s lock record from _lockState.locks[msg.sender], they fail to clear other critical storage mappings. Specifically, data related to voting power, checkpoints, and boost calculations remain in storage, even after a user's lock has been removed.

This oversight results in stale, inconsistent data persisting in storage, which can distort calculations related to governance voting power, historical snapshots, and reward distribution. If malicious actors exploit this, it can lead to unfair governance advantages, incorrect quorum requirements, and unintended consequences in the protocol’s boost mechanism.

Vulnerability Details

1. Storage Clearances That Are Missing

Upon withdrawal or emergency withdrawal, the contract only deletes the user's lock record but fails to remove related records in other mappings. The following data remains in storage:

  • Voting Power Records (_votingState.points[msg.sender])

    • Impact: User’s historical voting power remains stored, which could be used incorrectly in future calculations.

    • Fix: Should be cleared upon withdrawal.

  • Checkpoint Data (_votingState.checkpoints[msg.sender] and _checkpointState.userCheckpoints[msg.sender])

    • Impact: Old voting power records persist, which might artificially inflate governance weight or distort historical data.

    • Fix: Clear the checkpoint data upon withdrawal.

  • User-Specific Boost Periods (_boostState.userPeriods[msg.sender])

    • Impact: User-specific boost factors may remain, leading to incorrect calculations in rewards and governance participation.

    • Fix: Remove boost periods upon withdrawal.

2. Technical Breakdown

In the withdraw and emergencyWithdraw functions, we see the following deletion logic:

// Clear lock data
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender]; // Voting power data cleared, but not history!

However, the following key storage mappings are not cleared, leading to residual data that can persist indefinitely:

// Not cleared but should be:
delete _votingState.checkpoints[msg.sender]; // Voting history (Checkpoints)
delete _boostState.userPeriods[msg.sender]; // Boost data
delete _checkpointState.userCheckpoints[msg.sender]; // Voting power checkpoints

Proof of Concept

To demonstrate this vulnerability, the following Proof of Concept (PoC) is provided. The PoC is written using the Foundry tool.

  1. Step 1: Create a Foundry project and place all the contracts in the src directory.

  2. Step 2: Create a test directory and a mocks folder within the src directory (or use an existing mocks folder).

  3. Step 3: Create all necessary mock contracts, if required.

  4. Step 4: Create a test file (with any name) in the test directory.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {veRAACToken} from "../src/core/tokens/veRAACToken.sol";
import {RAACToken} from "../src/core/tokens/RAACToken.sol";
import {TimeWeightedAverage} from "../src/libraries/math/TimeWeightedAverage.sol";
import {LockManager} from "../src/libraries/governance/LockManager.sol";
import {IveRAACToken} from "../src/interfaces/core/tokens/IveRAACToken.sol";
contract VeRAACTokenTest is Test {
veRAACToken veRaacToken;
RAACToken raacToken;
address RAAC_OWNER = makeAddr("RAAC_OWNER");
address RAAC_MINTER = makeAddr("RAAC_MINTER");
uint256 initialRaacSwapTaxRateInBps = 200; // 2%, 10000 - 100%
uint256 initialRaacBurnTaxRateInBps = 150; // 1.5%, 10000 - 100%
address VE_RAAC_OWNER = makeAddr("VE_RAAC_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address DEVIL = makeAddr("DEVIL");
function setUp() public {
raacToken = new RAACToken(RAAC_OWNER, initialRaacSwapTaxRateInBps, initialRaacBurnTaxRateInBps);
vm.startPrank(VE_RAAC_OWNER);
veRaacToken = new veRAACToken(address(raacToken));
vm.stopPrank();
}
}
  1. Step 5: Add the following test PoC in the test file, after the setUp function.

function testResidualStorageAfterWithdraw() public {
uint256 LOCK_AMOUNT = 10_000_000e18;
uint256 LOCK_DURATION = 365 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, LOCK_AMOUNT);
vm.stopPrank();
uint256 veRaacRaacTokenBalance = raacToken.balanceOf(address(veRaacToken));
// Step 1: Alice approves and locks tokens.
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(LOCK_AMOUNT / 2, LOCK_DURATION);
vm.stopPrank();
veRaacRaacTokenBalance = raacToken.balanceOf(address(veRaacToken));
// roll to let boost calculator have a chance to update user balance.
vm.warp(block.timestamp + 1 days);
// Step 2: ALICE increases their lock to update internal state (including checkpoints and boost state).
vm.startPrank(ALICE);
veRaacToken.increase(LOCK_AMOUNT / 2);
vm.stopPrank();
vm.warp(block.timestamp + 366 days);
vm.roll(block.number + 1000);
// Step 3: ALICE withdraws tokens. The vulnerable withdraw function clears only some storage,
// leaving residual checkpoint and boost data intact.
veRaacRaacTokenBalance = raacToken.balanceOf(address(veRaacToken));
vm.startPrank(ALICE);
veRaacToken.withdraw();
vm.stopPrank();
// Verify residual storage remains:
// Check that the boost state time period for the ALICE still exists.
TimeWeightedAverage.Period memory aliceBoostTimePeriod = veRaacToken.getUserBoostTimePeriods(ALICE);
console.log("Alice's data persists even after withdrawal...");
console.log("startTime: ", aliceBoostTimePeriod.startTime);
console.log("endTime: ", aliceBoostTimePeriod.endTime);
console.log("lastUpdateTime: ", aliceBoostTimePeriod.lastUpdateTime);
console.log("value: ", aliceBoostTimePeriod.value);
console.log("weightedSum: ", aliceBoostTimePeriod.weightedSum);
console.log("totalDuration: ", aliceBoostTimePeriod.totalDuration);
console.log("weight: ", aliceBoostTimePeriod.weight);
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testResidualStorageAfterWithdraw -vv
  1. Step 7: Review the output. The expected output should indicate that user data still persist after withdrawal and cleanup.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[PASS] testResidualStorageAfterWithdraw() (gas: 758686)
Logs:
Alice's data persists even after withdrawal...
startTime: 2
endTime: 604802
lastUpdateTime: 86401
value: 0
weightedSum: 431995000000000000000000000000
totalDuration: 691199
weight: 1000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.77ms (1.03ms CPU time)
Ran 1 test suite in 11.40ms (3.77ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that the user data persist in storage even after withdrawal and storage cleanup.

Notice: Some modifications have been made to address additional issues.

veRAACToken::withdraw: (modified chunk)

+ // there's a issue in withdrawal because raacToken take a cut in form of taxes
- raacToken.safeTransfer(msg.sender, amount);
+ raacToken.safeTransfer(msg.sender, balanceOf(address(this)));

veRAACToken::getUserBoostTimePeriods: (newly introduced getter for ease)

// * @dev temporary not exist in actual
function getUserBoostTimePeriods(address user) external view returns (TimeWeightedAverage.Period memory) {
return _boostState.userPeriods[user];
}

veRAACToken::_updateBoostState (modified) Although it's not necessary to modify therefore we can test for withdrawal just after the lock.

function _updateBoostState(address user, uint256 newAmount) internal {
// Update boost calculator state
_boostState.votingPower = _votingState.calculatePowerAtTimestamp(user, block.timestamp);
_boostState.totalVotingPower = totalSupply();
_boostState.totalWeight = _lockState.totalLocked;
_boostState.updateBoostPeriod();
+ _boostState.updateUserBalance(user, newAmount);
}

Impact

  • Distorted Governance Quorum & Voting Power:

    • Users may still be counted in governance calculations even after they have withdrawn their tokens.

    • This could lead to incorrect quorum requirements and potential governance manipulation.

  • Stale Boost Factors Affecting Rewards:

    • Since BoostState.userPeriods[msg.sender] is not cleared, a user may continue to receive rewards based on an old boost period even after withdrawing.

    • This introduces an unfair advantage for some users and affects overall fairness in reward distribution.

  • Incorrect Historical Data & Voting Checkpoints:

    • Stale checkpoint records could affect historical governance analysis and mislead users into thinking past votes are still valid.

    • The protocol may continue referencing outdated checkpoints even when the user is no longer participating in governance.

  • Potential Exploitation by Malicious Users:

    • Users could withdraw, re-lock tokens under a new account, and still retain governance weight from the previous account.

    • This artificial inflation of governance power could be used to manipulate proposals unfairly.

Tools Used

  • Manual Review

  • Foundry

  • AI (ChatGpt, Phind)

  • SRC Mutation

Recommendations

1. Ensure Complete Storage Clearance

Modify the withdraw and emergencyWithdraw functions to clear all related storage mappings to prevent stale data from persisting. Update the functions to include:

// Ensure complete clearance of user's data upon withdrawal
delete _lockState.locks[msg.sender]; // Clear lock data
delete _votingState.points[msg.sender]; // Clear voting power
delete _votingState.checkpoints[msg.sender]; // Clear voting history
delete _boostState.userPeriods[msg.sender]; // Remove boost periods
delete _checkpointState.userCheckpoints[msg.sender]; // Remove voting power history

3. Introduce Events for Storage Clearance Monitoring

  • Emit events when clearing storage, such as:

emit StorageCleared(msg.sender, "Voting Power Cleared");
emit StorageCleared(msg.sender, "Boost State Cleared");
  • This allows external auditors and governance participants to track proper state updates.

4. Add a View Function to Check Residual Data

  • Implement a helper function that verifies whether a user's data has been completely cleared after withdrawal.

  • Example:

function hasResidualData(address user) external view returns (bool) {
return _votingState.points[user] != 0 ||
_boostState.userPeriods[user].start != 0 ||
_checkpointState.userCheckpoints[user].length > 0;
}
Updates

Lead Judging Commences

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

Appeal created

theirrationalone Submitter
4 months ago
inallhonesty Lead Judge
3 months ago
inallhonesty Lead Judge 3 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.