Core Contracts

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

Missing active lock validation in veRAACToken::lock traps funds and result in voting power inflation

Description

The veRAACToken::lock function fails to validate existing active locks (minimum 1 year duration) before creating new positions. This allows users to overwrite previous locks, resulting in:

  1. Trapped RAAC tokens - Previous locked amounts remain inaccessible but count toward totalLocked

  2. Voting Power Duplication - New veRAAC tokens minted without burning existing balance

  3. Protocol Metric Corruption - LockState.totalLocked becomes artificially inflated

Proof of Concept

(1) Initial lock with maximum duration:

veRAACToken::lock(1000e18, 1460 days) // 4 years
-> _lockState.totalLocked = 1000e18
-> balanceOf(user) = 1000e18 (max duration = full multiplier)

(2) Second lock after 1 day with minimum duration:

veRAACToken::lock(2000e18, 365 days) // 1 year (MIN_LOCK_DURATION)
-> Overwrites lock.amount to 2000e18
-> _lockState.totalLocked = 3000e18 (1000+2000)
-> balanceOf(user) = 1000e18 + 500e18 (2000 * 1/4)

(3) After 1 year + 1 day:

veRAACToken::withdraw() returns 2000e18 RAAC
-> Contract retains original 1000e18 RAAC
-> totalLocked remains 3000e18 (1000e18 unaccounted)

Test case to demonstrate vulnerability:

In veRAACToken.test.js, add this test and run npx hardhat test --grep "traps funds when overwriting locks"

it("traps funds when overwriting locks", async () => {
const maxLock = ethers.parseEther("1000");
const minLock = ethers.parseEther("2000");
const maxDuration = 1460 * 24 * 3600;
const minDuration = 365 * 24 * 3600; // MIN_LOCK_DURATION
// Create max duration lock
await veRAACToken.connect(users[0]).lock(maxLock, maxDuration);
// Overwrite with min duration lock after 1 day
await time.increase(86400);
await raacToken.mint(users[0].address, minLock);
await veRAACToken.connect(users[0]).lock(minLock, minDuration);
// Verify state corruption
const finalLock = await veRAACToken.getLockPosition(users[0].address);
expect(finalLock.amount).to.equal(minLock); // previous lock overwritten
// Attempt withdrawal after min duration
await time.increase(minDuration + 1);
await veRAACToken.connect(users[0]).withdraw();
// Verify stranded funds
const contractBalance = await raacToken.balanceOf(
await veRAACToken.getAddress()
);
expect(contractBalance).to.equal(maxLock); // Original 1000e18 remains locked and trapped
await expect(
veRAACToken.connect(users[0]).withdraw()
).to.be.revertedWithCustomError(veRAACToken, "LockNotFound"); // lock is deleted
});

Impact

High Severity - Direct protocol impacts:

  • 🔒 Users permanently lose access to overwritten RAAC deposits

  • 📈 Inflated totalLocked misrepresents protocol TVL

  • ⚖️ Duplicate voting power distorts governance

  • 📉 Protocol accounting becomes irreparable

Recommendation

  • Prevent active lock overwrites:

contracts/core/tokens/veRAACToken.sol
function lock(...) external {
+ LockManager.Lock memory currentLock = _lockState.locks[msg.sender];
+ if (currentLock.exists && currentLock.end > block.timestamp) {
+ revert ActiveLockExists();
+ }
_lockState.createLock(msg.sender, amount, duration);
}
Updates

Lead Judging Commences

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

veRAACToken::lock called multiple times, by the same user, leads to loss of funds

Support

FAQs

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