Core Contracts

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

Missing Update to `_lockState.totalLocked` in `veRAACToken::emergencyWithdraw()`

Summary

The veRAACToken::emergencyWithdraw() function does not update _lockState.totalLocked, causing an unchecked increase in its value. Over time, this leads to an overflow, preventing new tokens from being locked and potentially disrupting contract functionality. However, given that emergency withdrawals may indicate the contract is being deprecated or abandoned, this issue is classified as informational unless emergency withdrawals are expected to maintain normal functionality.

Vulnerability Details

The functions veRAACToken::lock() and veRAACToken::increase() correctly update _lockState.totalLocked, ensuring that the total locked balance remains accurate. However, veRAACToken::emergencyWithdraw() only clears user-specific data without adjusting _lockState.totalLocked, leading to a continuously increasing value that could eventually overflow.

function emergencyWithdraw() external nonReentrant {
if (emergencyWithdrawDelay == 0 || block.timestamp < emergencyWithdrawDelay)
revert EmergencyWithdrawNotEnabled();
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert NoTokensLocked();
uint256 amount = userLock.amount;
uint256 currentPower = balanceOf(msg.sender);
@> delete _lockState.locks[msg.sender];
@> delete _votingState.points[msg.sender];
_burn(msg.sender, currentPower);
raacToken.safeTransfer(msg.sender, amount);
emit EmergencyWithdrawn(msg.sender, amount);
}

Since _lockState.totalLocked is never updated upon withdrawal, its value continues to increase indefinitely.

Comparison with withdraw():
In contrast, the standard withdrawal mechanism ensures _lockState.totalLocked is updated correctly. If emergencyWithdraw() is meant to replace withdraw(), its omission of _lockState.totalLocked updates suggests that emergency withdrawals may be intended as a final measure before contract abandonment.

Poc

Add the following test to test/unit/core/tokens/veRAACToken.test.js and execute it:

describe("emergencyWithdraw fails to update totalLocked", () => {
it("Poc", async () => {
const amount = ethers.parseEther("2000");
const duration = 365 * 24 * 3600 * 4 ; // 4 year
// User[0] locks 2000e18
await veRAACToken.connect(users[0]).lock(amount, duration);
// User[1] locks 2000e18
await veRAACToken.connect(users[1]).lock(amount, duration);
// User[2] locks 2000e18
await veRAACToken.connect(users[2]).lock(amount, duration);
// Enable emergency withdrawal
const EMERGENCY_DELAY = 3 * 24 * 3600; // 3 days in seconds
const EMERGENCY_WITHDRAW_ACTION = ethers.keccak256(
ethers.toUtf8Bytes("enableEmergencyWithdraw")
);
await veRAACToken.connect(owner).scheduleEmergencyAction(EMERGENCY_WITHDRAW_ACTION);
await time.increase(EMERGENCY_DELAY);
await veRAACToken.connect(owner).enableEmergencyWithdraw();
await time.increase(EMERGENCY_DELAY);
await network.provider.send("evm_mine");
// Users call emergencyWithdraw()
await veRAACToken.connect(users[0]).emergencyWithdraw();
await veRAACToken.connect(users[1]).emergencyWithdraw();
await veRAACToken.connect(users[2]).emergencyWithdraw();
const boostState = await veRAACToken.getBoostState();
console.log("_lockState.totalLocked:",boostState.totalWeight);
console.log("total voting power:", await veRAACToken.getTotalVotingPower());
});
});

output:

veRAACToken
emergencyWithdraw fails to update totalLocked
_lockState.totalLocked: 6000000000000000000000n
total voting power: 0n

The output confirms that _lockState.totalLocked continues increasing without a corresponding decrease, diverging from the expected total voting power. Over time, this unchecked growth can lead to an overflow, rendering the contract unusable.

Impact

If emergency withdrawals are meant to preserve contract functionality, the unchecked growth of _lockState.totalLocked could lead to an overflow, preventing new tokens from being locked and disrupting normal operations. However, if emergency withdrawals indicate the contract is being deprecated, then this behavior may be intentional.

Tools Used

Manual Review

Recommendations

If emergency withdrawals are intended to allow continued contract operation, modify emergencyWithdraw() to properly update _lockState.totalLocked by subtracting the withdrawn amount,For example:

function emergencyWithdraw() external nonReentrant {
if (emergencyWithdrawDelay == 0 || block.timestamp < emergencyWithdrawDelay)
revert EmergencyWithdrawNotEnabled();
LockManager.Lock memory userLock = _lockState.locks[msg.sender];
if (userLock.amount == 0) revert NoTokensLocked();
uint256 amount = userLock.amount;
uint256 currentPower = balanceOf(msg.sender);
// Update totalLocked
+ _lockState.totalLocked -= amount;
delete _lockState.locks[msg.sender];
delete _votingState.points[msg.sender];
_burn(msg.sender, currentPower);
raacToken.safeTransfer(msg.sender, amount);
emit EmergencyWithdrawn(msg.sender, amount);
}
Updates

Lead Judging Commences

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

veRAACToken::withdraw / emergencyWithdraw doesn't substract the `_lockState.totalLocked`

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

veRAACToken::withdraw / emergencyWithdraw doesn't substract the `_lockState.totalLocked`

Support

FAQs

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

Give us feedback!