Skip to content

Commit

Permalink
refactor, test: updateProposalsState
Browse files Browse the repository at this point in the history
  • Loading branch information
notJoon committed Dec 24, 2024
1 parent bbf5811 commit a27fc66
Show file tree
Hide file tree
Showing 2 changed files with 270 additions and 55 deletions.
156 changes: 101 additions & 55 deletions gov/governance/proposal.gno
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ var (
latestProposalByProposer = make(map[std.Address]uint64) // proposer -> proposalId
)

const (
GOV_SPLIT = "*GOV*"
EXE_SPLIT = "*EXE*"
)

// ProposeText creates a new text proposal with the given data
// It checks if the proposer is eligible to create a proposal and if they don't have an active proposal.
// Returns the proposal ID
Expand Down Expand Up @@ -187,7 +192,7 @@ func ProposeParameterChange(
}

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

// check if each execution is valid
for _, gov := range splitGov {
splitExe := strings.Split(gov, "*EXE*")
splitExe := strings.Split(gov, EXE_SPLIT)
if len(splitExe) != 3 {
panic(addDetailToError(
errInvalidInput,
Expand Down Expand Up @@ -259,63 +264,104 @@ func ProposeParameterChange(

func updateProposalsState() {
now := uint64(time.Now().Unix())

for id, proposal := range proposals {
config := GetConfigVersion(proposal.ConfigVersion)

// check if proposal is in a state that needs to be updated
// - created
// - not canceled
// - not executed

// proposal is in voting period
if proposal.ExecutionState.Created &&
!proposal.ExecutionState.Canceled &&
!proposal.ExecutionState.Executed {

if proposal.ExecutionState.Upcoming && // was upcoming
now >= (proposal.ExecutionState.CreatedAt+config.VotingStartDelay) && // voting started
now <= (proposal.ExecutionState.CreatedAt+config.VotingStartDelay+config.VotingPeriod) { // voting not ended
proposal.ExecutionState.Upcoming = false
proposal.ExecutionState.Active = true
}
}
updater := newProposalStateUpdater(&proposal, now)

// proposal voting ended, check if passed or rejected
if now > (proposal.ExecutionState.CreatedAt+config.VotingStartDelay+config.VotingPeriod) &&
(proposal.ExecutionState.Passed == false && proposal.ExecutionState.Rejected == false && proposal.ExecutionState.Canceled == false) {
yeaUint := proposal.Yea.Uint64()
nayUint := proposal.Nay.Uint64()
quorumUint := proposal.QuorumAmount

if yeaUint >= quorumUint && yeaUint > nayUint {
proposal.ExecutionState.Passed = true
proposal.ExecutionState.PassedAt = now
} else {
proposal.ExecutionState.Rejected = true
proposal.ExecutionState.RejectedAt = now
}
proposal.ExecutionState.Upcoming = false
proposal.ExecutionState.Active = false
}
updater.updateVotingState()
updater.updateVotingResult()
updater.updateExecutionState()

// TODO (@notJoon): refactor this.
// (non text) proposal passed but not executed until executing window ends
if proposal.ProposalType != Text && // isn't text type ≈ can be executed
proposal.ExecutionState.Passed && // passed
!proposal.ExecutionState.Executed && // not executed
!proposal.ExecutionState.Expired { // not expired

votingEnd := proposal.ExecutionState.CreatedAt + config.VotingStartDelay + config.VotingPeriod
windowStart := votingEnd + config.ExecutionDelay
windowEnd := windowStart + config.ExecutionWindow

if now >= windowEnd { // execution window ended
proposal.ExecutionState.Expired = true
proposal.ExecutionState.ExpiredAt = now
}
}
proposals[id] = *updater.proposal
}
}

type proposalStateUpdater struct {
proposal *ProposalInfo
config Config
now uint64
}

func newProposalStateUpdater(proposal *ProposalInfo, now uint64) *proposalStateUpdater {
return &proposalStateUpdater{
proposal: proposal,
config: GetConfigVersion(proposal.ConfigVersion),
now: now,
}
}

func (u *proposalStateUpdater) shouldUpdate() bool {
return u.proposal.ExecutionState.Created &&
!u.proposal.ExecutionState.Canceled &&
!u.proposal.ExecutionState.Executed
}

func (u *proposalStateUpdater) getVotingTimes() (start, end uint64) {
start = u.proposal.ExecutionState.CreatedAt + u.config.VotingStartDelay
end = start + u.config.VotingPeriod
return
}

func (u *proposalStateUpdater) getExecutionTimes() (start, end uint64) {
_, votingEnd := u.getVotingTimes()
start = votingEnd + u.config.ExecutionDelay
end = start + u.config.ExecutionWindow
return
}

func (u *proposalStateUpdater) updateVotingState() {
if !u.shouldUpdate() {
return
}

votingStart, votingEnd := u.getVotingTimes()
isVotingPeriod := u.now >= votingStart && u.now <= votingEnd

if u.proposal.ExecutionState.Upcoming && isVotingPeriod {
u.proposal.ExecutionState.Upcoming = false
u.proposal.ExecutionState.Active = true
}
}

func (u *proposalStateUpdater) updateVotingResult() {
_, votingEnd := u.getVotingTimes()

hasNoResult := !u.proposal.ExecutionState.Passed &&
!u.proposal.ExecutionState.Rejected &&
!u.proposal.ExecutionState.Canceled

if u.now <= votingEnd || !hasNoResult {
return
}

yeaUint := u.proposal.Yea.Uint64()
nayUint := u.proposal.Nay.Uint64()

if yeaUint >= u.proposal.QuorumAmount && yeaUint > nayUint {
u.proposal.ExecutionState.Passed = true
u.proposal.ExecutionState.PassedAt = u.now
} else {
u.proposal.ExecutionState.Rejected = true
u.proposal.ExecutionState.RejectedAt = u.now
}

u.proposal.ExecutionState.Upcoming = false
u.proposal.ExecutionState.Active = false
}

func (u *proposalStateUpdater) updateExecutionState() {
if u.proposal.ProposalType == Text ||
!u.proposal.ExecutionState.Passed ||
u.proposal.ExecutionState.Executed ||
u.proposal.ExecutionState.Expired {
return
}

_, executionEnd := u.getExecutionTimes()

proposals[id] = proposal
if u.now >= executionEnd {
u.proposal.ExecutionState.Expired = true
u.proposal.ExecutionState.ExpiredAt = u.now
}
}

Expand Down
169 changes: 169 additions & 0 deletions gov/governance/proposal_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package governance

import (
"testing"
"time"

u256 "gno.land/p/gnoswap/uint256"
)

func TestUpdateProposalsState(t *testing.T) {
baseTime := uint64(1000)
configVersions = map[uint64]Config{
1: {
VotingStartDelay: 50,
VotingPeriod: 100,
ExecutionDelay: 50,
ExecutionWindow: 100,
},
}

testCases := []struct {
name string
currentTime uint64
setupProposal func(uint64) ProposalInfo
validate func(*testing.T, ProposalInfo)
}{
{
name: "Should reject proposal when voting ends with insufficient votes",
currentTime: baseTime + 200,
setupProposal: func(now uint64) ProposalInfo {
return ProposalInfo{
ConfigVersion: 1,
Yea: u256.NewUint(100),
Nay: u256.NewUint(200),
QuorumAmount: 300,
ExecutionState: ExecutionState{
Created: true,
CreatedAt: baseTime,
Active: true,
},
}
},
validate: func(t *testing.T, proposal ProposalInfo) {
if !proposal.ExecutionState.Rejected {
t.Error("Proposal should be rejected")
}
if proposal.ExecutionState.Active {
t.Error("Proposal should not be active anymore")
}
if proposal.ExecutionState.Upcoming {
t.Error("Proposal should not be upcoming")
}
if proposal.ExecutionState.RejectedAt == 0 {
t.Error("RejectedAt timestamp should be set")
}
},
},
{
name: "Should pass proposal when voting ends with sufficient votes",
currentTime: baseTime + 200,
setupProposal: func(now uint64) ProposalInfo {
return ProposalInfo{
ConfigVersion: 1,
Yea: u256.NewUint(400),
Nay: u256.NewUint(200),
QuorumAmount: 300,
ExecutionState: ExecutionState{
Created: true,
CreatedAt: baseTime,
Active: true,
},
}
},
validate: func(t *testing.T, proposal ProposalInfo) {
if !proposal.ExecutionState.Passed {
t.Error("Proposal should be passed")
}
if proposal.ExecutionState.Active {
t.Error("Proposal should not be active anymore")
}
if proposal.ExecutionState.PassedAt == 0 {
t.Error("PassedAt timestamp should be set")
}
},
},
{
name: "Should expire non-text proposal when execution window ends",
currentTime: baseTime + 400,
setupProposal: func(now uint64) ProposalInfo {
return ProposalInfo{
ConfigVersion: 1,
ProposalType: ParameterChange,
ExecutionState: ExecutionState{
Created: true,
CreatedAt: baseTime,
Passed: true,
PassedAt: baseTime + 200,
},
}
},
validate: func(t *testing.T, proposal ProposalInfo) {
if !proposal.ExecutionState.Expired {
t.Error("Proposal should be expired")
}
if proposal.ExecutionState.ExpiredAt == 0 {
t.Error("ExpiredAt timestamp should be set")
}
},
},
{
name: "Should not update canceled proposal",
currentTime: baseTime + 60,
setupProposal: func(now uint64) ProposalInfo {
return ProposalInfo{
ConfigVersion: 1,
ExecutionState: ExecutionState{
Created: true,
CreatedAt: baseTime,
Canceled: true,
Upcoming: true,
},
}
},
validate: func(t *testing.T, proposal ProposalInfo) {
if !proposal.ExecutionState.Canceled {
t.Error("Proposal should remain canceled")
}
if !proposal.ExecutionState.Upcoming {
t.Error("Proposal state should not change when canceled")
}
},
},
{
name: "Should not expire text proposal",
currentTime: baseTime + 400,
setupProposal: func(now uint64) ProposalInfo {
return ProposalInfo{
ConfigVersion: 1,
ProposalType: Text,
ExecutionState: ExecutionState{
Created: true,
CreatedAt: baseTime,
Passed: true,
PassedAt: baseTime + 200,
},
}
},
validate: func(t *testing.T, proposal ProposalInfo) {
if proposal.ExecutionState.Expired {
t.Error("Text proposal should not expire")
}
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
proposals = make(map[uint64]ProposalInfo)

proposal := tc.setupProposal(tc.currentTime)
proposals[1] = proposal

updateProposalsState()

updatedProposal := proposals[1]
tc.validate(t, updatedProposal)
})
}
}

0 comments on commit a27fc66

Please sign in to comment.