Core Contracts

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

Missing Check That `newDuration` Is Greater Than Current Lock Duration

PoC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../contracts/veRAACToken.sol"; // Path to the veRAACToken contract
contract MockRAACToken is ERC20 {
constructor() ERC20("Mock RAAC", "MRAAC") {
// For testing, mint a large supply
\_mint(msg.sender, 1\_000\_000\_000e18);
}
}
contract veRAACShortExtendTest is Test {
veRAACToken public veToken;
MockRAACToken public raacToken;
address user = address(0xBEEF);
function setUp() public {
raacToken = new MockRAACToken();
veToken = new veRAACToken(address(raacToken));
// Give the user enough RAAC and approve the veToken for locking
raacToken.transfer(user, 100_000e18);
vm.prank(user);
raacToken.approve(address(veToken), type(uint256).max);
}
function testShortExtendLock() public {
// 1) User locks 100 tokens for 365 days
vm.prank(user);
veToken.lock(100e18, 365 days);
// Suppose there's no check to ensure newDuration > existingRemaining
// 2) The user calls extend(...) with 200 days (less than 365)
// Expecting a revert if properly enforced. But if there's no check:
vm.warp(block.timestamp + 100 days); // 100 days pass, 265 left
// Now user tries to "extend" to newDuration=200, which is < 265 left.
vm.startPrank(user);
try veToken.extend(200 days) {
// If no revert => proof the contract did not check for a real extension
console2.log("Short extension accepted, bug confirmed!");
} catch {
// If it reverts with something like "CannotShortenLock()",
// the contract does have a check, so no bug.
console2.log("Extension revert => the bug is fixed or doesn't exist");
}
vm.stopPrank();
}
}

Overview

When a user calls extend(uint256 newDuration), the contract defers to:

uint256 newUnlockTime = _lockState.extendLock(msg.sender, newDuration);
...
(int128 newBias, int128 newSlope) = _votingState.calculateAndUpdatePower(
msg.sender,
userLock.amount,
newUnlockTime
);
...

The snippet never verifies that newDuration is strictly larger than the user’s current lock. If the external LockManager library also omits this check, then:

  1. A user with, say, 300 days left in their lock could pass a 100‑day newDuration.

  2. The code calculates newUnlockTime = block.timestamp + 100 days—shorter than the original end, but if the library logic is flawed, it may still proceed to recalculate the user’s voting power as if the lock had been extended (or partially unaffected).

Depending on how the library handles the new duration, a user might:

  • Lower the actual lock time while still retaining or even boosting minted veRAAC if the function miscomputes newBias or slope under the assumption that any “extend” is valid.

  • Or keep the same end time (no net extension) but still get an updated bias or slope if the library or code is incorrectly recalculating voting power.

Attack Path / Proof of Concept

  1. User has 300 days left: The user previously locked tokens for 1 year, with 300 days remaining.

  2. Call extend(100):

    • The code calls _lockState.extendLock(msg.sender, 100 days).

    • If the library does not revert or check that 100 < 300 is invalid, the new unlock time might become block.timestamp + 100 days.

  3. Recalculate Voting Power:

    1. (newBias, newSlope) = _votingState.calculateAndUpdatePower(...) is computed with the new unlock time. If the function incorrectly treats it as an actual extension, the user’s minted tokens might remain the same or even increase.

  4. Shorter Lock with Full Power:

    • The user effectively gets a shortened or unchanged end date while retaining minted tokens, circumventing the time‑weighted lock concept.

Impact

Lock Duration Integrity: The entire premise of a time‑weighted escrow is that “extending” must strictly lengthen the lock. Failing to enforce this lets a user shorten or keep the same end date while the function believes it’s extended, potentially awarding more voting power or not penalizing them for a shorter lock.

Governance / Reward Distortions: The user could hold minted veRAAC with a lesser time commitment than the contract intended, undermining the linear “time = power” model.

Recommendations

  1. Enforce Strictly Larger Duration
    In extend(...) or the library call, confirm newDuration > userLock.remainingTime. For example:

    LockManager.Lock memory userLock = _lockState.locks[msg.sender];
    uint256 currentEndTime = userLock.end;
    uint256 remaining = currentEndTime > block.timestamp ? (currentEndTime - block.timestamp) : 0;
    // Then
    if (newDuration <= remaining) revert CannotShortenLock();
  2. Library Validation
    If the LockManager library is assumed to handle it, ensure it reverts if newDuration <= the user’s current remaining time. Confirm that the library does indeed do so.

  3. Remove Ambiguity
    If partial short‑term modifications are part of the design, clarify the effect on minted tokens so that the user’s veRAAC supply is adjusted accordingly (e.g., forcibly burning tokens if the user “shortens” their lock).

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!