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.