function cancel(uint256 proposalId) external override {
ProposalCore storage proposal = _proposals[proposalId];
if (proposal.startTime == 0) revert ProposalDoesNotExist(proposalId);
ProposalState currentState = state(proposalId);
if (currentState == ProposalState.Executed) {
revert InvalidProposalState(proposalId, currentState, ProposalState.Active, "Cannot cancel executed proposal");
}
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;
emit ProposalCanceled(proposalId, msg.sender, "Proposal canceled by proposer");
}
pragma solidity ^0.8.19;
import {Test} from "forge-std/Test.sol";
import {Governance} from "../../contracts/core/governance/proposals/Governance.sol";
import {TimelockController} from "../../contracts/core/governance/proposals/TimelockController.sol";
import {RAACToken} from "../../contracts/core/tokens/RAACToken.sol";
import {veRAACToken} from "../../contracts/core/tokens/veRAACToken.sol";
import {IGovernance} from "../../contracts/interfaces/core/governance/proposals/IGovernance.sol";
import {ITimelockController} from "../../contracts/interfaces/core/governance/proposals/ITimelockController.sol";
import "forge-std/console2.sol";
contract FoundryTest is Test {
Governance public governance;
TimelockController public timelock;
RAACToken public raacToken;
veRAACToken public veToken;
address public admin = address(this);
address public proposer = makeAddr("proposer");
address public voter = makeAddr("voter");
address[] public targets;
uint256[] public values;
bytes[] public calldatas;
string public description = "Test Proposal";
uint256 public constant PROPOSAL_THRESHOLD = 100_000e18;
uint256 public constant VOTING_POWER = 1_000_000e18;
function setUp() public {
uint256 initialSwapTaxRate = 100;
uint256 initialBurnTaxRate = 50;
raacToken = new RAACToken(admin, initialSwapTaxRate, initialBurnTaxRate);
raacToken.setMinter(admin);
veToken = new veRAACToken(address(raacToken));
address[] memory proposers = new address[](1);
proposers[0] = address(0);
address[] memory executors = new address[](1);
executors[0] = address(0);
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));
timelock.grantRole(timelock.EXECUTOR_ROLE(), admin);
timelock.grantRole(timelock.CANCELLER_ROLE(), address(governance));
targets.push(address(raacToken));
values.push(0);
calldatas.push(abi.encodeWithSignature("approve(address,uint256)", address(1), 100));
}
function test_setupGovernanceTest() public {
assertEq(timelock.getRoleAdmin(timelock.PROPOSER_ROLE()), timelock.DEFAULT_ADMIN_ROLE());
assertEq(timelock.getRoleAdmin(timelock.EXECUTOR_ROLE()), timelock.DEFAULT_ADMIN_ROLE());
assertEq(timelock.getRoleAdmin(timelock.CANCELLER_ROLE()), timelock.DEFAULT_ADMIN_ROLE());
assertEq(timelock.hasRole(timelock.PROPOSER_ROLE(), address(governance)), true);
assertEq(timelock.hasRole(timelock.EXECUTOR_ROLE(), address(governance)), true);
assertEq(timelock.hasRole(timelock.CANCELLER_ROLE(), address(governance)), true);
}
function test_CancelQueuedProposal() public {
raacToken.mint(proposer, VOTING_POWER);
vm.startPrank(proposer);
raacToken.approve(address(veToken), VOTING_POWER);
veToken.lock(VOTING_POWER, 366 days);
uint256 proposalId = governance.propose(
targets,
values,
calldatas,
description,
IGovernance.ProposalType.ParameterChange
);
vm.warp(block.timestamp + governance.votingDelay() + 1);
governance.castVote(proposalId, true);
vm.warp(block.timestamp + governance.votingPeriod() + 1);
governance.execute(proposalId);
governance.cancel(proposalId);
vm.stopPrank();
assertEq(uint256(governance.state(proposalId)), uint256(IGovernance.ProposalState.Canceled));
bytes32 id = timelock.hashOperationBatch(targets, values, calldatas, bytes32(0), keccak256(bytes(description)));
assertEq(timelock.isOperationPending(id), true);
vm.warp(20 days);
vm.expectEmit(true, true, true, true);
emit ITimelockController.OperationExecuted(
id,
targets,
values,
calldatas,
bytes32(0),
keccak256(bytes(description))
);
timelock.executeBatch(targets, values, calldatas, bytes32(0), keccak256(bytes(description)));
}
}
Modify the cancel() function in the Governance contract to also cancel the operation in the TimelockController when a queued proposal is cancelled and make sure the Governance contract has the CANCELLER_ROLE
. This ensures that cancellation is atomic across both contracts and maintains consistent state: