Core Contracts

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

Wrong constraint verification in `veRAACToken::lock` function will results in users unable to lock RAAC tokens much ealier than expected.

Summary

Wrong constraint verification in veRAACToken::lock function will results in users unable to lock RAAC tokens much ealier than expected.

Relevant links

https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/tokens/veRAACToken.sol#L215

Vulnerability Details

in veRAACToken::lock, during a lock, we verify if 3 conditions are met. Amongst which the max total verification

if (totalSupply() + amount > MAX\_TOTAL\_SUPPLY) revert TotalSupplyLimitExceeded();

  • totalSupply() represents the value of veRAACTokens that are already minted.

  • amount represents the amount of RAAC tokens to be locked.

  • MAX_TOTAL_SUPPLY represents the amaximum amount of veRAACTokens to ever be minted.

It is wrong to mix these various amounts as though they represent the same thing especially given that, the actual amount of veRAACToken to be minted will always be less than the amount of RAAC token locked.

This is because, the newPower minted is deducted from bias which is calculated using the VotingPowerLib::calculateAndUpdatePower function.

File: contracts/libraries/governance/VotingPowerLib.sol#L75-L107
function calculateAndUpdatePower(
VotingPowerState storage state,
address user,
uint256 amount,
uint256 unlockTime
) internal returns (int128 bias, int128 slope) {
if (amount == 0 || unlockTime <= block.timestamp) revert InvalidPowerParameters();
uint256 MAX_LOCK_DURATION = 1460 days; // 4 years
// FIXME: Get me to uncomment me when able
// bias = RAACVoting.calculateBias(amount, unlockTime, block.timestamp);
// slope = RAACVoting.calculateSlope(amount);
// Calculate initial voting power that will decay linearly to 0 at unlock time
uint256 duration = unlockTime - block.timestamp;
// @audit calculate the voting power that is to be passed on to `bias`
uint256 initialPower = (amount * duration) / MAX_LOCK_DURATION; // Normalize by max duration
bias = int128(int256(initialPower));
slope = int128(int256(initialPower / duration)); // Power per second decay
uint256 oldPower = getCurrentPower(state, user, block.timestamp);
state.points[user] = RAACVoting.Point({
bias: bias,
slope: slope,
timestamp: block.timestamp
});
_updateSlopeChanges(state, unlockTime, 0, slope);
emit VotingPowerUpdated(user, oldPower, uint256(uint128(bias)));
return (bias, slope);
}

We can observe that, the calculated power will always be lesser than the amount of RAAC locked.

POC

it("User's veRAACToken valance is always lower than locked amount of RAAC token", async () => {
const amount = ethers.parseEther("1000"); // 1000 tokens
await raacToken.mint(users[0].address, amount);
const duration = 365 * 24 * 3600; // 1 year
const userBalanceBefore = await veRAACToken.balanceOf(users[0]);
await veRAACToken.connect(users[0]).lock(amount, duration);
const userBalanceAfter = await veRAACToken.balanceOf(users[0]);
expect(userBalanceAfter - userBalanceBefore).to.be.lt(amount);
});

Copy the above code in test/unit/core/tokens/veRAACToken.test.js and run npx hardhat test test/unit/core/tokens/veRAACToken.test.js in the terminal.

Impact

Users will be prevented from locking funds if the amount to be locked is > MAX_TOTAL_SUPPLY - totalSupply() even if it won't break any invariant of the contract. Thus, users will not be able to create new lock positions well ahead of when the protocol intended for them to be unable to. This means that, early comers will gain an unfaire voting advantage over the late voters as the majority of the voting power will be owned by them ( due to them locking their votes early on ) and the late comers will have no means of gaining any (new) voting power given that, the false check on MAX_TOTAL_SUPPLY will prevent them to.

Also, MAX_TOTAL_SUPPLY will never be reached since it is the amount to be lock that is used to perform the verification and the calculated power is always be lower than the locked amount per the above explanation.

I should note that, given that one can extend his lock duration indefinitely, it's possible for those who locked their positions early on to hoard all the voting power for themselves.

Tools Used

Manual review.

Recommendations

function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
- if (totalSupply() + amount > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
if (duration < MIN_LOCK_DURATION || duration > MAX_LOCK_DURATION)
revert InvalidLockDuration();
// Do the transfer first - this will revert with ERC20InsufficientBalance if user doesn't have enough tokens
raacToken.safeTransferFrom(msg.sender, address(this), amount);
// Calculate unlock time
uint256 unlockTime = block.timestamp + duration;
// Create lock position
_lockState.createLock(msg.sender, amount, duration);
_updateBoostState(msg.sender, amount);
// Calculate initial voting power
(int128 bias, int128 slope) = _votingState.calculateAndUpdatePower(
msg.sender,
amount,
unlockTime
);
// Update checkpoints
uint256 newPower = uint256(uint128(bias));
+ if (totalSupply() + newPower > MAX_TOTAL_SUPPLY) revert TotalSupplyLimitExceeded();
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Mint veTokens
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}
Updates

Lead Judging Commences

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

Incorrect `MAX_TOTAL_SUPPLY` check in the `veRAACToken::lock/extend` function of `veRAACToken` could harm locking functionality

Support

FAQs

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