In governance.sol
system's state transition logic, specifically in how proposals move through different states and interact with the timelock controller
. The core issue stems from a race condition and state inconsistency between the Governance.sol
and TimelockController
, where a proposal can become "stuck" due to improper state management.
When a proposal is queued directly through the timelock controller instead of following the proper state transition path through the governance contract, it creates a deadlock situation where:
The governance contract sees the proposal as "Succeeded" and tries to queue it
The timelock controller already has the proposal queued
The execution fails because the governance contract's state doesn't match the timelock's state
The proposal becomes permanently unexecutable
This vulnerability could be exploited to permanently freeze governance proposals, effectively allowing an attacker to disrupt the protocol's governance mechanism.
The vulnerability stems from a state synchronization issue between the Governance.sol
and TimelockController.sol
contracts. Here's the exact breakdown:
When a proposal passes voting, it enters the "Succeeded"
state in the Governance contract.
The Governance contract, directly calls TimelockController's scheduleBatch()
function to queue the proposal:
This direct call creates two simultaneous but conflicting states:
The proposal is now queued in TimelockController
The proposal's internal state in Governance remains as "Succeeded"
When state()
is called, it sees the proposal is pending in timelock and returns "Queued"
However, when execute()
is called, it:
Checks the internal state (still sees "Succeeded")
Tries to queue the proposal again
Fails because the proposal is already in the timelock
The proposal becomes permanently stuck because:
It can't be queued again (already in timelock)
It can't be executed (wrong internal state)
No mechanism exists to reconcile this state mismatch
First, in the state()
function, it automatically checks timelock status:
But in execute()
, it tries to queue again if it sees Succeeded:
The _queueProposal()
function then fails because it's already in timelock:
Proof of code:
Flow:
Initial State:
Direct Timelock Call by Governance:
. State Desynchronization:
This creates a situation where:
The proposal is queued in timelock
But governance's internal state hasn't tracked this change
When execute() is called, it tries to queue again
The operation fails because it's already in timelock
Result: The proposal becomes permanently unexecutable because:
Can't queue (already in timelock)
Can't execute (wrong internal state)
No way to resolve the state mismatch
All proposals become stuck and unexecutable.
ensure state Synchronization:
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.