Technical

Governance Freeze PostMortem

An incorrect constructor argument in the Veto Council deployment during the initial Glow V1 guarded-launch caused Governance to reference the wrong contract. When the first proposal to modify council membership executed, the call reverted, blocking and freezing the entire Governance pipeline.

Governance Freeze PostMortem

Code Dive

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 (NUMWEEKSTOVOTEONMOSTPOPULARPROPOSAL) 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 VETOCOUNCILELECTIONORSLASH. 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

  • Governance execution frozen. No on-chain grants, parameter changes, or council elections can complete.
  • Grants moved off-chain. The Foundation will aid in an off-chain grant process manually until V2 is operational.
  • No funds at risk. Contract balances and state remain intact; the issue is a logic lock, not a security breach.

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.

  • Strongly-typed constructors in new code. All new contracts receive explicit interface parameters; misuse fails at compile time.
    • Deployment scripts pass interface-typed objects instead of plain addresses, eliminating class-mismatch errors.
  • AI-assisted static analysis. New codebases run an LLM ruleset that flags duplicated or suspicious constructor arguments before merge.
  • Comprehensive deploy-script tests. Each script forks mainnet, executes the deployment end-to-end, and asserts post-deploy invariants, blocking any transaction that does not wire contracts correctly.
  • Mandatory multi-party review and canary execution. Every mainnet deploy requires dual-signature review of calldata and a post-deploy smoke test on a fork, preventing mis-configured contracts from reaching production.

Author: Glow Team

NEWSLETTER

Stay in the Loop

Get the latest updates on solar energy innovations, protocol developments, and impact stories delivered to your inbox.

We respect your privacy. Unsubscribe at any time. Unsubscribe here