Core Contracts

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

Voting Power Decay Enabling Unauthorized Proposal Cancellation in `Governance::cancel()`

Summary

The vulnerability lies in the fact that during the 7-day voting period, any proposal can be canceled if the proposer's voting power falls below the required threshold. This means that even if a proposal was valid at creation, it becomes vulnerable to cancellation within that period if the decaying voting power causes it to drop below the threshold.

Vulnerability Details

Proposal Creation

When a proposal is created, the contract checks that the caller’s current voting power meets proposalThreshold:

Governance::propose()

uint256 proposerVotes = _veToken.getVotingPower(msg.sender);
if (proposerVotes < proposalThreshold) {
revert InsufficientProposerVotes(msg.sender, proposerVotes, proposalThreshold, "Below threshold");
}
  • Requirement: The proposer must have at least proposalThreshold (e.g., 100,000 veRAAC tokens).

Cancellation Logic

Later, when attempting to cancel a proposal, the contract includes the following logic:

if (msg.sender != proposal.proposer &&
_veToken.getVotingPower(proposal.proposer) >= proposalThreshold) {
revert InsufficientProposerVotes(proposal.proposer,
_veToken.getVotingPower(proposal.proposer), proposalThreshold, "Proposer lost required voting power");
}
proposal.canceled = true;
  • Cancellation Condition: Anyone (other than the proposer) can cancel the proposal if the proposer's current voting power is below the proposalThreshold.

Voting Power Decay Mechanism veRAACToken

The voting power is calculated based on a point system with a bias (initial power) and a slope (rate of decay):

function getVotingPower(address account) public view returns (uint256) {
return _votingState.getCurrentPower(account, block.timestamp);
}
function getCurrentPower(
VotingPowerState storage state,
address account,
uint256 timestamp
) internal view returns (uint256) {
RAACVoting.Point memory point = state.points[account];
if (point.timestamp == 0) return 0;
uint256 timeDelta = timestamp - point.timestamp;
int128 adjustedBias = point.bias;
if (timeDelta > 0) {
int128 decay = (point.slope * int128(int256(timeDelta))) / int128(int256(1));
adjustedBias = point.bias - decay;
}
return adjustedBias > 0 ? uint256(uint128(adjustedBias)) : 0;
}
  • Key Math:

    • Initial Setup:
      A user mints 100,000e18 tokens for 4 years.
      The voting power (bias) is calculated as:

      initialPower = (amount * duration) / MAX_LOCK_DURATION
      bias = initialPower // here, 100,000e18
      slope = initialPower / duration // ≈ 792744799594114
    • Time Decay Effect:
      Consider the scenario where:

      • The proposal is created with block.timestamp = T.

      • The proposal have 7 days Duration of voting period

      • The tokens were minted at T (i.e., point.timestamp = T seconds).

      • The voting power at the moment of cancellation (at time T + 7) is computed as:

        • timeDelta = 604800

        • decay = slope * timeDelta (≈ 792744799594114 * 604800)

        • adjustedBias = bias - decay

      With these values, even though the proposer initially had exactly 100,000e18 voting power, by the time of cancellation, the decayed voting power drops to a value less than the threshold (e.g., approximately 99,520e18).

Exploitation Scenario (Proof-of-Concept)

  1. Initial Setup:
    A user locks 100,000e18 tokens for 4 years. At the moment of minting, the user’s voting power equals the threshold (100,000e18).

  2. Proposal Creation:
    The user creates a proposal. The check in the propose function passes because the user’s voting power meets the threshold.

  3. Time Decay:
    Due to the linear decay mechanism, after a period (e.g., after 7 days—the duration used in the calculation), the voting power decays by an amount proportional to the time elapsed.

  4. Unauthorized Cancellation:
    At a later time, when a third party (not the proposal creator) calls the cancel function:

    • The contract re-checks the proposer's voting power using the decayed value.

    • Since the decayed voting power is now below proposalThreshold, the check:

      if (msg.sender != proposal.proposer &&
      _veToken.getVotingPower(proposal.proposer) >= proposalThreshold)

      is bypassed.

    • Result: The proposal is canceled even though it was valid at creation.

Impact

  • Valid proposals can be canceled by anyone once the proposer’s voting power decays below the threshold, undermining the governance process.

  • Proposers might be discouraged from participating if their proposals can be canceled due to natural decay, even if they initially met the requirements.

Tools Used

Manual Review

Recommendations

Consider recording the proposer’s voting power at the time of proposal creation and use that static value for later checks (e.g., in cancellation logic) rather than recalculating it with a decay factor.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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