Core Contracts

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

Users can be DoSed locking their funds in the veRAACToken contract

Summary

Users can be DoSed locking their funds using the veRAACToken::lock function due to an incorrect check of totalSupply limit.

Vunerability Details

When locking funds using the veRAACToken::lock function, there is a check on the limit of the totalSupply of veRAACToken to prevent voting power to be more than the desired value. But this check is incorrectly done since it uses the amount of RAAC tokens to be locked instead of the amount of the amount veRAACToken (power) to mint in exchange of RAAC tokens.

File: contracts/core/tokens/veRAACToken.sol#L212-L244
function lock(uint256 amount, uint256 duration) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (amount > MAX_LOCK_AMOUNT) revert AmountExceedsLimit();
// @audit `totalSupply` and `MAX_TOTAL_SUPPLY` are in veRAACToken, but `amount` is in RAACToken
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));
_checkpointState.writeCheckpoint(msg.sender, newPower);
// Mint veTokens
// @audit newPower is the real change in totalSupply and is deducted from `bias` which is computed in VotingPowerLIb::calculateAndUpdatePower()
_mint(msg.sender, newPower);
emit LockCreated(msg.sender, amount, unlockTime);
}

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 compute the voting power that 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);
}

As one can see, the calculated power will always be lower than the locked amount.

Impact

As a result, users might be prevented from locking funds if the amount to lock is > MAX_TOTAL_SUPPLY - totalSupply() even if it won't break any invariant of the contract.
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 will always be lower than the locked amount.

Proof of Concept

The following PoC proves the above statement.

it.only("should user's balance difference be lower than lock amount", 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.

Tools Used

Manual review.

Recommendations

In veRAACToken::lock(), verify the totalSupply limit with newPower instead of amount.

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 7 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.

Give us feedback!