Governance Freeze PostMortem | Glow Blog

CodeDive

In Glow V1, the happy path for governance follows a five step process, outlined below:

1. Proposal Creation

• Users call one of the create*Proposal entry points (createGrantsProposal, createGCACouncilElectionOrSlashProposal, etc.).
• Each function:

  1. Calculates the nomination cost (costForNewProposalAndUpdateLastExpiredProposalId).
  2. Burns the caller's nominations via _spendNominations.
  3. Stores a Proposal struct in _proposals and, if its vote count tops the leaderboard, records it in mostPopularProposalOfWeek.

2. Collecting Votes

• Additional nominations can later be attached with useNominationsOnProposal.
• Once the popularity week ends, long-staked GLW holders record ratify/reject votes through ratifyOrReject.
• Votes accumulate in _proposalLongStakerVotes.

3. Eligibility Window

A proposal becomes executable when: • Its popularity week has finished and
• The four-week ratify/reject & veto window (_NUM_WEEKS_TO_VOTE_ON_MOST_POPULAR_PROPOSAL) has elapsed.

4. Execution Engine (syncProposals)

Anyone can invoke syncProposals() to process the backlog sequentially—oldest first.

1function syncProposals() public {
2    ...
3    for (_nextWeekToExecute; _nextWeekToExecute < currentWeek; ++_nextWeekToExecute) {
4        uint256 proposalId = mostPopularProposalOfWeek[_nextWeekToExecute];
5        ...
6        handleProposalExecution(_nextWeekToExecute, proposalId, proposalType, proposal.data);
7    }
8}

If any execution reverts, the loop aborts and lastExecutedWeek remains unchanged, blocking every subsequent proposal.

5. Type-specific Dispatch (handleProposalExecution)

handleProposalExecution decodes the payload and forwards the action to the correct subsystem:

1if (proposalType == IGovernance.ProposalType.VETO_COUNCIL_ELECTION_OR_SLASH) {
2    (address oldMember, address newMember, bool slashOldMember) = abi.decode(data,(address,address,bool));
3    success = IVetoCouncil(VETO_COUNCIL).addAndRemoveCouncilMember(oldMember, newMember, slashOldMember);
4}

Other branches cover Grants payouts, GCA requirement changes, RFC logging, etc.
Success or failure is stamped via _setProposalStatus, and an event is emitted.

Where It Broke

During week 76, the queued proposal was a VETO_COUNCIL_ELECTION_OR_SLASH.
When syncProposals() hit the dispatch above, the call to VetoCouncil.addAndRemoveCouncilMember() reverted with CallerNotGovernance because the Veto Council contract's immutable governance field had been mis-wired to the GLW token address during deployment:

1// DeployGuardedLaunch.s.sol
2vetoCouncilContract = new VetoCouncil(address(glow), address(glow), startingVetoCouncilAgents);
3//                          ^^^^^^^^^^^^^^^^^^^^^  (should be Governance)

The revert bubbled up, terminated the for loop, and left lastExecutedWeek stuck.
Because syncProposals() insists on synchronous, chronological execution (week == lastExecutedWeek + 1), the entire Governance pipeline froze—no subsequent proposals (including grant payouts) can progress until a redeploy wires the Veto Council to the correct Governance address.

Impact

Preventing These Issues

Ultimately, the value of any post-mortem lies in the concrete safeguards it inspires. The four mitigations below - type-safe constructors, AI-assisted static analysis, end-to-end deploy-script tests, and dual-review canary deployments - turn the lessons of this incident into hard guardrails baked into our tooling and release flow. Together they make a repeat of the mis-wiring error mechanically unlikely and surface any similar faults long before they can reach mainnet.

Author: Glow Team