Skip to content

Commit a27fc66

Browse files
committed
refactor, test: updateProposalsState
1 parent bbf5811 commit a27fc66

File tree

2 files changed

+270
-55
lines changed

2 files changed

+270
-55
lines changed

gov/governance/proposal.gno

Lines changed: 101 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ var (
2121
latestProposalByProposer = make(map[std.Address]uint64) // proposer -> proposalId
2222
)
2323

24+
const (
25+
GOV_SPLIT = "*GOV*"
26+
EXE_SPLIT = "*EXE*"
27+
)
28+
2429
// ProposeText creates a new text proposal with the given data
2530
// It checks if the proposer is eligible to create a proposal and if they don't have an active proposal.
2631
// Returns the proposal ID
@@ -187,7 +192,7 @@ func ProposeParameterChange(
187192
}
188193

189194
// check if numToExecute is a valid number
190-
splitGov := strings.Split(executions, "*GOV*")
195+
splitGov := strings.Split(executions, GOV_SPLIT)
191196
if uint64(len(splitGov)) != numToExecute {
192197
panic(addDetailToError(
193198
errInvalidInput,
@@ -197,7 +202,7 @@ func ProposeParameterChange(
197202

198203
// check if each execution is valid
199204
for _, gov := range splitGov {
200-
splitExe := strings.Split(gov, "*EXE*")
205+
splitExe := strings.Split(gov, EXE_SPLIT)
201206
if len(splitExe) != 3 {
202207
panic(addDetailToError(
203208
errInvalidInput,
@@ -259,63 +264,104 @@ func ProposeParameterChange(
259264

260265
func updateProposalsState() {
261266
now := uint64(time.Now().Unix())
267+
262268
for id, proposal := range proposals {
263-
config := GetConfigVersion(proposal.ConfigVersion)
264-
265-
// check if proposal is in a state that needs to be updated
266-
// - created
267-
// - not canceled
268-
// - not executed
269-
270-
// proposal is in voting period
271-
if proposal.ExecutionState.Created &&
272-
!proposal.ExecutionState.Canceled &&
273-
!proposal.ExecutionState.Executed {
274-
275-
if proposal.ExecutionState.Upcoming && // was upcoming
276-
now >= (proposal.ExecutionState.CreatedAt+config.VotingStartDelay) && // voting started
277-
now <= (proposal.ExecutionState.CreatedAt+config.VotingStartDelay+config.VotingPeriod) { // voting not ended
278-
proposal.ExecutionState.Upcoming = false
279-
proposal.ExecutionState.Active = true
280-
}
281-
}
269+
updater := newProposalStateUpdater(&proposal, now)
282270

283-
// proposal voting ended, check if passed or rejected
284-
if now > (proposal.ExecutionState.CreatedAt+config.VotingStartDelay+config.VotingPeriod) &&
285-
(proposal.ExecutionState.Passed == false && proposal.ExecutionState.Rejected == false && proposal.ExecutionState.Canceled == false) {
286-
yeaUint := proposal.Yea.Uint64()
287-
nayUint := proposal.Nay.Uint64()
288-
quorumUint := proposal.QuorumAmount
289-
290-
if yeaUint >= quorumUint && yeaUint > nayUint {
291-
proposal.ExecutionState.Passed = true
292-
proposal.ExecutionState.PassedAt = now
293-
} else {
294-
proposal.ExecutionState.Rejected = true
295-
proposal.ExecutionState.RejectedAt = now
296-
}
297-
proposal.ExecutionState.Upcoming = false
298-
proposal.ExecutionState.Active = false
299-
}
271+
updater.updateVotingState()
272+
updater.updateVotingResult()
273+
updater.updateExecutionState()
300274

301-
// TODO (@notJoon): refactor this.
302-
// (non text) proposal passed but not executed until executing window ends
303-
if proposal.ProposalType != Text && // isn't text type ≈ can be executed
304-
proposal.ExecutionState.Passed && // passed
305-
!proposal.ExecutionState.Executed && // not executed
306-
!proposal.ExecutionState.Expired { // not expired
307-
308-
votingEnd := proposal.ExecutionState.CreatedAt + config.VotingStartDelay + config.VotingPeriod
309-
windowStart := votingEnd + config.ExecutionDelay
310-
windowEnd := windowStart + config.ExecutionWindow
311-
312-
if now >= windowEnd { // execution window ended
313-
proposal.ExecutionState.Expired = true
314-
proposal.ExecutionState.ExpiredAt = now
315-
}
316-
}
275+
proposals[id] = *updater.proposal
276+
}
277+
}
278+
279+
type proposalStateUpdater struct {
280+
proposal *ProposalInfo
281+
config Config
282+
now uint64
283+
}
284+
285+
func newProposalStateUpdater(proposal *ProposalInfo, now uint64) *proposalStateUpdater {
286+
return &proposalStateUpdater{
287+
proposal: proposal,
288+
config: GetConfigVersion(proposal.ConfigVersion),
289+
now: now,
290+
}
291+
}
292+
293+
func (u *proposalStateUpdater) shouldUpdate() bool {
294+
return u.proposal.ExecutionState.Created &&
295+
!u.proposal.ExecutionState.Canceled &&
296+
!u.proposal.ExecutionState.Executed
297+
}
298+
299+
func (u *proposalStateUpdater) getVotingTimes() (start, end uint64) {
300+
start = u.proposal.ExecutionState.CreatedAt + u.config.VotingStartDelay
301+
end = start + u.config.VotingPeriod
302+
return
303+
}
304+
305+
func (u *proposalStateUpdater) getExecutionTimes() (start, end uint64) {
306+
_, votingEnd := u.getVotingTimes()
307+
start = votingEnd + u.config.ExecutionDelay
308+
end = start + u.config.ExecutionWindow
309+
return
310+
}
311+
312+
func (u *proposalStateUpdater) updateVotingState() {
313+
if !u.shouldUpdate() {
314+
return
315+
}
316+
317+
votingStart, votingEnd := u.getVotingTimes()
318+
isVotingPeriod := u.now >= votingStart && u.now <= votingEnd
319+
320+
if u.proposal.ExecutionState.Upcoming && isVotingPeriod {
321+
u.proposal.ExecutionState.Upcoming = false
322+
u.proposal.ExecutionState.Active = true
323+
}
324+
}
325+
326+
func (u *proposalStateUpdater) updateVotingResult() {
327+
_, votingEnd := u.getVotingTimes()
328+
329+
hasNoResult := !u.proposal.ExecutionState.Passed &&
330+
!u.proposal.ExecutionState.Rejected &&
331+
!u.proposal.ExecutionState.Canceled
332+
333+
if u.now <= votingEnd || !hasNoResult {
334+
return
335+
}
336+
337+
yeaUint := u.proposal.Yea.Uint64()
338+
nayUint := u.proposal.Nay.Uint64()
339+
340+
if yeaUint >= u.proposal.QuorumAmount && yeaUint > nayUint {
341+
u.proposal.ExecutionState.Passed = true
342+
u.proposal.ExecutionState.PassedAt = u.now
343+
} else {
344+
u.proposal.ExecutionState.Rejected = true
345+
u.proposal.ExecutionState.RejectedAt = u.now
346+
}
347+
348+
u.proposal.ExecutionState.Upcoming = false
349+
u.proposal.ExecutionState.Active = false
350+
}
351+
352+
func (u *proposalStateUpdater) updateExecutionState() {
353+
if u.proposal.ProposalType == Text ||
354+
!u.proposal.ExecutionState.Passed ||
355+
u.proposal.ExecutionState.Executed ||
356+
u.proposal.ExecutionState.Expired {
357+
return
358+
}
359+
360+
_, executionEnd := u.getExecutionTimes()
317361

318-
proposals[id] = proposal
362+
if u.now >= executionEnd {
363+
u.proposal.ExecutionState.Expired = true
364+
u.proposal.ExecutionState.ExpiredAt = u.now
319365
}
320366
}
321367

gov/governance/proposal_test.gno

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package governance
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
u256 "gno.land/p/gnoswap/uint256"
8+
)
9+
10+
func TestUpdateProposalsState(t *testing.T) {
11+
baseTime := uint64(1000)
12+
configVersions = map[uint64]Config{
13+
1: {
14+
VotingStartDelay: 50,
15+
VotingPeriod: 100,
16+
ExecutionDelay: 50,
17+
ExecutionWindow: 100,
18+
},
19+
}
20+
21+
testCases := []struct {
22+
name string
23+
currentTime uint64
24+
setupProposal func(uint64) ProposalInfo
25+
validate func(*testing.T, ProposalInfo)
26+
}{
27+
{
28+
name: "Should reject proposal when voting ends with insufficient votes",
29+
currentTime: baseTime + 200,
30+
setupProposal: func(now uint64) ProposalInfo {
31+
return ProposalInfo{
32+
ConfigVersion: 1,
33+
Yea: u256.NewUint(100),
34+
Nay: u256.NewUint(200),
35+
QuorumAmount: 300,
36+
ExecutionState: ExecutionState{
37+
Created: true,
38+
CreatedAt: baseTime,
39+
Active: true,
40+
},
41+
}
42+
},
43+
validate: func(t *testing.T, proposal ProposalInfo) {
44+
if !proposal.ExecutionState.Rejected {
45+
t.Error("Proposal should be rejected")
46+
}
47+
if proposal.ExecutionState.Active {
48+
t.Error("Proposal should not be active anymore")
49+
}
50+
if proposal.ExecutionState.Upcoming {
51+
t.Error("Proposal should not be upcoming")
52+
}
53+
if proposal.ExecutionState.RejectedAt == 0 {
54+
t.Error("RejectedAt timestamp should be set")
55+
}
56+
},
57+
},
58+
{
59+
name: "Should pass proposal when voting ends with sufficient votes",
60+
currentTime: baseTime + 200,
61+
setupProposal: func(now uint64) ProposalInfo {
62+
return ProposalInfo{
63+
ConfigVersion: 1,
64+
Yea: u256.NewUint(400),
65+
Nay: u256.NewUint(200),
66+
QuorumAmount: 300,
67+
ExecutionState: ExecutionState{
68+
Created: true,
69+
CreatedAt: baseTime,
70+
Active: true,
71+
},
72+
}
73+
},
74+
validate: func(t *testing.T, proposal ProposalInfo) {
75+
if !proposal.ExecutionState.Passed {
76+
t.Error("Proposal should be passed")
77+
}
78+
if proposal.ExecutionState.Active {
79+
t.Error("Proposal should not be active anymore")
80+
}
81+
if proposal.ExecutionState.PassedAt == 0 {
82+
t.Error("PassedAt timestamp should be set")
83+
}
84+
},
85+
},
86+
{
87+
name: "Should expire non-text proposal when execution window ends",
88+
currentTime: baseTime + 400,
89+
setupProposal: func(now uint64) ProposalInfo {
90+
return ProposalInfo{
91+
ConfigVersion: 1,
92+
ProposalType: ParameterChange,
93+
ExecutionState: ExecutionState{
94+
Created: true,
95+
CreatedAt: baseTime,
96+
Passed: true,
97+
PassedAt: baseTime + 200,
98+
},
99+
}
100+
},
101+
validate: func(t *testing.T, proposal ProposalInfo) {
102+
if !proposal.ExecutionState.Expired {
103+
t.Error("Proposal should be expired")
104+
}
105+
if proposal.ExecutionState.ExpiredAt == 0 {
106+
t.Error("ExpiredAt timestamp should be set")
107+
}
108+
},
109+
},
110+
{
111+
name: "Should not update canceled proposal",
112+
currentTime: baseTime + 60,
113+
setupProposal: func(now uint64) ProposalInfo {
114+
return ProposalInfo{
115+
ConfigVersion: 1,
116+
ExecutionState: ExecutionState{
117+
Created: true,
118+
CreatedAt: baseTime,
119+
Canceled: true,
120+
Upcoming: true,
121+
},
122+
}
123+
},
124+
validate: func(t *testing.T, proposal ProposalInfo) {
125+
if !proposal.ExecutionState.Canceled {
126+
t.Error("Proposal should remain canceled")
127+
}
128+
if !proposal.ExecutionState.Upcoming {
129+
t.Error("Proposal state should not change when canceled")
130+
}
131+
},
132+
},
133+
{
134+
name: "Should not expire text proposal",
135+
currentTime: baseTime + 400,
136+
setupProposal: func(now uint64) ProposalInfo {
137+
return ProposalInfo{
138+
ConfigVersion: 1,
139+
ProposalType: Text,
140+
ExecutionState: ExecutionState{
141+
Created: true,
142+
CreatedAt: baseTime,
143+
Passed: true,
144+
PassedAt: baseTime + 200,
145+
},
146+
}
147+
},
148+
validate: func(t *testing.T, proposal ProposalInfo) {
149+
if proposal.ExecutionState.Expired {
150+
t.Error("Text proposal should not expire")
151+
}
152+
},
153+
},
154+
}
155+
156+
for _, tc := range testCases {
157+
t.Run(tc.name, func(t *testing.T) {
158+
proposals = make(map[uint64]ProposalInfo)
159+
160+
proposal := tc.setupProposal(tc.currentTime)
161+
proposals[1] = proposal
162+
163+
updateProposalsState()
164+
165+
updatedProposal := proposals[1]
166+
tc.validate(t, updatedProposal)
167+
})
168+
}
169+
}

0 commit comments

Comments
 (0)