governance_execute.gno
9.84 Kb · 330 lines
1package governance
2
3import (
4 "chain"
5 "chain/runtime"
6 "errors"
7 "time"
8
9 ufmt "gno.land/p/nt/ufmt/v0"
10 "gno.land/r/gnoswap/access/v1"
11
12 "gno.land/p/gnoswap/utils/v1"
13
14 "gno.land/r/gnoswap/common"
15 "gno.land/r/gnoswap/emission"
16 "gno.land/r/gnoswap/halt/v1"
17
18 "gno.land/r/gnoswap/gov/governance"
19)
20
21// Execute executes an approved proposal.
22//
23// Processes and implements governance decisions after successful voting.
24// Enforces the configured execution delay and execution window.
25// Anyone can trigger execution to ensure decentralization.
26//
27// Parameters:
28// - _: Internal call discriminator; pass 0.
29// - rlm: Propagated realm context; must be current for authorized governance state updates and is crossed into execution-dependent calls.
30// - proposalID: ID of the proposal to execute
31//
32// Requirements:
33// - Proposal must have passed (quorum and strict yes-over-no majority)
34// - Quorum must be reached using the proposal's stored amount, derived from
35// the total xGNS supply and configured quorum percentage at creation
36// - Execution delay must have elapsed (1 day by default)
37// - Must be within the execution window (30 days by default)
38// - Proposal not already executed or cancelled
39//
40// Effects:
41// - Executes proposal actions (parameter changes, treasury transfers)
42// - Marks proposal as executed
43// - Emits execution event
44//
45// Returns:
46// - proposalID: ID of the executed proposal
47//
48// Callable by anyone once the proposal is executable.
49func (gv *governanceV1) Execute(_ int, rlm realm, proposalID int64) int64 {
50 access.AssertIsRlmCurrent(0, rlm)
51
52 // Check if execution is allowed (system not halted for execution).
53 // Halt recovery proposals remain executable while governance is halted.
54 assertIsNotHaltedGovernanceForProposal(gv, proposalID)
55
56 // Get caller information and current blockchain state
57 prev := rlm.Previous()
58 caller := prev.Address()
59 currentHeight := runtime.ChainHeight()
60 currentAt := time.Now().Unix()
61
62 // Mint and distribute GNS tokens as part of the execution process
63 emission.MintAndDistributeGns(cross(rlm))
64
65 // Attempt to execute the proposal with current context
66 proposal, err := gv.executeProposal(
67 0, rlm,
68 proposalID,
69 currentAt,
70 currentHeight,
71 caller,
72 )
73 if err != nil {
74 panic(err)
75 }
76 if err := gv.removeActiveProposal(0, rlm, proposal); err != nil {
77 panic(err)
78 }
79
80 // Emit execution event for tracking and auditing
81 chain.Emit(
82 "Execute",
83 "prevAddr", prev.Address().String(),
84 "prevRealm", prev.PkgPath(),
85 "proposalId", utils.FormatInt(proposalID),
86 )
87
88 return proposal.ID()
89}
90
91// executeProposal handles core logic of proposal execution.
92func (gv *governanceV1) executeProposal(
93 _ int, rlm realm,
94 proposalID int64,
95 executedAt int64,
96 executedHeight int64,
97 executedBy address,
98) (*governance.Proposal, error) {
99 // Retrieve the proposal from storage
100 proposal, ok := gv.getProposal(proposalID)
101 if !ok {
102 return nil, errors.New(errDataNotFound)
103 }
104
105 // Text proposals cannot be executed (they are informational only)
106 if proposal.IsTextType() {
107 return nil, errors.New(errTextProposalNotExecutable)
108 }
109
110 proposalResolver := NewProposalResolver(proposal)
111
112 // Verify proposal is in executable state (timing and voting requirements met)
113 if !proposalResolver.IsExecutable(executedAt) {
114 return nil, errors.New(errProposalNotExecutable)
115 }
116
117 // Mark proposal as executed in its status
118 err := proposalResolver.execute(executedAt, executedHeight, executedBy)
119 if err != nil {
120 return nil, err
121 }
122
123 // Execute proposal based on its type
124 switch proposal.Type() {
125 case governance.CommunityPoolSpend:
126 // Execute community pool spending (token transfers)
127 err = executeCommunityPoolSpend(0, rlm, proposal, globalParameterRegistry, executedAt, executedHeight, executedBy)
128 if err != nil {
129 return nil, err
130 }
131 case governance.ParameterChange:
132 // Execute parameter changes (governance configuration updates)
133 err = executeParameterChange(0, rlm, proposal, globalParameterRegistry, executedAt, executedHeight, executedBy)
134 if err != nil {
135 return nil, err
136 }
137 }
138
139 return proposal, nil
140}
141
142// Cancel cancels a proposal in upcoming status.
143//
144// Allows proposers to withdraw their proposals before voting begins.
145// Prevents accidental or malicious proposals from reaching vote.
146// Safety mechanism for proposal errors or changed circumstances.
147//
148// Parameters:
149// - _: Internal call discriminator; pass 0.
150// - rlm: Propagated realm context; must be current for authorized governance state updates and is crossed into reward-distribution calls.
151// - proposalID: ID of the proposal to cancel
152//
153// Requirements:
154// - Must be called by original proposer
155// - Proposal must be in "upcoming" status
156// - Voting must not have started yet
157// - Proposal not already cancelled or executed
158//
159// Effects:
160// - Sets proposal status to "cancelled"
161// - Prevents future voting or execution
162// - Emits cancellation event
163// - Frees up proposer's proposal slot
164//
165// Returns:
166// - proposalID: ID of the cancelled proposal
167//
168// Only callable by original proposer before voting begins.
169func (gv *governanceV1) Cancel(_ int, rlm realm, proposalID int64) int64 {
170 access.AssertIsRlmCurrent(0, rlm)
171
172 halt.AssertIsNotHaltedGovernance()
173
174 prev := rlm.Previous()
175 caller := prev.Address()
176 assertCallerIsProposer(gv, proposalID, caller)
177
178 // Get current blockchain state and caller information
179 currentHeight := runtime.ChainHeight()
180 currentAt := time.Now().Unix()
181
182 // Mint and distribute GNS tokens as part of the process
183 emission.MintAndDistributeGns(cross(rlm))
184
185 // Attempt to cancel the proposal
186 proposal, err := gv.cancel(proposalID, currentAt, currentHeight, caller)
187 if err != nil {
188 panic(err)
189 }
190 if err := gv.removeActiveProposal(0, rlm, proposal); err != nil {
191 panic(err)
192 }
193
194 // Emit cancellation event for tracking
195 chain.Emit(
196 "Cancel",
197 "prevAddr", prev.Address().String(),
198 "prevRealm", prev.PkgPath(),
199 "proposalId", utils.FormatInt(proposalID),
200 )
201
202 return proposal.ID()
203}
204
205// RemoveInactiveProposalFromIndex removes one inactive proposal from the
206// snapshot index without deleting its proposal or voting history.
207// Parameters:
208// - _: Internal call discriminator; pass 0.
209// - rlm: Propagated realm context used for the authorized snapshot-index update.
210// - proposalID: Identifier of the inactive proposal to remove from the snapshot index.
211func (gv *governanceV1) RemoveInactiveProposalFromIndex(_ int, rlm realm, proposalID int64) {
212 access.AssertIsRlmCurrent(0, rlm)
213
214 halt.AssertIsNotHaltedGovernance()
215
216 proposal, exists := gv.getProposal(proposalID)
217 if !exists {
218 panic(makeErrorWithDetails(errProposalNotFound, ufmt.Sprintf("proposalID: %d", proposalID)))
219 }
220 if NewProposalResolver(proposal).IsActive(time.Now().Unix()) {
221 panic(makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("proposal %d is still active", proposalID)))
222 }
223 if err := gv.removeActiveProposal(0, rlm, proposal); err != nil {
224 panic(err)
225 }
226
227 prev := rlm.Previous()
228 chain.Emit(
229 "RemoveInactiveProposalFromIndex",
230 "prevAddr", prev.Address().String(),
231 "prevRealm", prev.PkgPath(),
232 "proposalId", utils.FormatInt(proposalID),
233 )
234}
235
236// cancel handles core logic of proposal cancellation.
237// Validates proposal state and updates status to canceled.
238func (gv *governanceV1) cancel(
239 proposalID, canceledAt, canceledHeight int64,
240 canceledBy address,
241) (proposal *governance.Proposal, err error) {
242 // Retrieve the proposal from storage
243 proposal, ok := gv.getProposal(proposalID)
244 if !ok {
245 return nil, errors.New(errDataNotFound)
246 }
247
248 proposalResolver := NewProposalResolver(proposal)
249
250 // Attempt to cancel the proposal (this validates cancellation conditions)
251 err = proposalResolver.cancel(canceledAt, canceledHeight, canceledBy)
252 if err != nil {
253 return nil, err
254 }
255
256 return proposal, nil
257}
258
259// executeCommunityPoolSpend executes community pool spending proposals.
260// Handles token transfers from community pool to specified recipients.
261func executeCommunityPoolSpend(
262 _ int, rlm realm,
263 proposal *governance.Proposal,
264 parameterRegistry *ParameterRegistry,
265 executedAt int64,
266 executedHeight int64,
267 executedBy address,
268) error {
269 // Verify token registration for community pool spending
270 if proposal.IsCommunityPoolSpendType() {
271 common.MustRegistered(proposal.Data().CommunityPoolSpend().TokenPath())
272 }
273
274 // Execute all parameter changes defined in the proposal
275 dataResolver := NewProposalDataResolver(proposal.Data())
276 parameterChangesInfos, err := dataResolver.ParameterChangesInfos()
277 if err != nil {
278 return err
279 }
280 for _, parameterChangeInfo := range parameterChangesInfos {
281 // Get the appropriate handler for this parameter change
282 key := makeHandlerKey(parameterChangeInfo.PkgPath(), parameterChangeInfo.Function())
283 handler, err := parameterRegistry.Handler(key)
284 if err != nil {
285 return err
286 }
287
288 // Execute the parameter change with provided parameters
289 err = handler.Execute(0, rlm, parameterChangeInfo.Params())
290 if err != nil {
291 return err
292 }
293 }
294
295 return nil
296}
297
298// executeParameterChange executes parameter change proposals.
299// Handles governance configuration updates and system parameter modifications.
300func executeParameterChange(
301 _ int, rlm realm,
302 proposal *governance.Proposal,
303 parameterRegistry *ParameterRegistry,
304 executedAt int64,
305 executedHeight int64,
306 executedBy address,
307) error {
308 // Execute all parameter changes defined in the proposal
309 dataResolver := NewProposalDataResolver(proposal.Data())
310 parameterChangesInfos, err := dataResolver.ParameterChangesInfos()
311 if err != nil {
312 return err
313 }
314 for _, parameterChangeInfo := range parameterChangesInfos {
315 // Get the appropriate handler for this parameter change
316 key := makeHandlerKey(parameterChangeInfo.PkgPath(), parameterChangeInfo.Function())
317 handler, err := parameterRegistry.Handler(key)
318 if err != nil {
319 return err
320 }
321
322 // Execute the parameter change with provided parameters
323 err = handler.Execute(0, rlm, parameterChangeInfo.Params())
324 if err != nil {
325 return err
326 }
327 }
328
329 return nil
330}