The Governance::cancel function allows a proposal to be cancelled. However, if the proposal has already been queued in the TimelockController, calling Governance::cancel only updates the proposal's state within the Governance contract. It does not cancel the corresponding scheduled operation within the TimelockController. This creates an inconsistent state where the Governance contract reflects the proposal as cancelled, but the TimelockController still has the operation scheduled for execution.
The vulnerability arises from the lack of coordination between the Governance contract and the TimelockController during proposal cancellation. The Governance::cancel function focuses solely on updating the proposal's state within its own storage, specifically by setting the canceled flag in the _proposals mapping. It does not interact with the TimelockController to remove the corresponding operation from the timelock queue. This disconnect allows a proposal to be marked as cancelled in the Governance contract while the associated operation remains scheduled for execution in the TimelockController. This can lead to several undesirable outcomes, such as if a cancelled proposal's actions are still executed despite cancellation.
Inconsistent State: The state of the proposal is inconsistent between the Governance contract and the TimelockController. This can lead to confusion and make it difficult to determine the true status of a proposal..
Unexpected Execution: The system might behave unexpectedly if the cancelled proposal's actions are still executed. This could have significant consequences depending on the nature of the proposal.
Proposer create a proposal in the Governance contract. Ensure it gathers sufficient votes to pass quorum.
Once the quorum is reached, call the Governance::execute function to queue the proposal in the TimelockController. This will schedule the operation for execution after the timelock delay.
Later, proposer cancels the proposal by calling the Governance::cancel function.
After cancelling, call the Governance::state function for the proposal. It will return ProposalState.Canceled.
But,calling the TimelockController::isOperationPending function with the operation ID (obtainable by hashing the proposal details). It will return true, indicating the operation is still scheduled.
As the cancelled proposal is still scheduled, the operation in the TimelockController will be still executable despite the Governance contract showing the proposal as cancelled, leading to unexpected execution.
Use this guide to intergrate foundry into your project: foundry
Create a new file FortisAudits.t.sol in the test directory.
Add the following gist code to the file: Gist Code
Run the test using forge test --mt test_FortisAudits_InconsistentProposalCancellation -vvvv.
The Governance::cancel function should call the TimelockController::cancel function to remove the corresponding operation from the timelock queue. This ensures that the proposal is consistently cancelled across both contracts and prevents unexpected execution of the proposal's actions.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.