Transaction
8C2A8C2FE8B9A3…6600BF4E4606
Block 77,269 · index 0 · indexed
Summary
- Hash
- 8C2A8C2FE8B9A34B2991E57B1D464050233381D1B728A24946646600BF4E4606
- Block
- 77,269
- Size
- 147145 bytes
- Gas used
- 182,956,847 / 219,548,157
- Fee
- 219549 ugnot
- Status
- success
Messages
Arguments · 24
- #1governance
- #2README.md
- #3# Governance Decentralized protocol governance via GNS staking and voting. ## Overview Governance enables GNS holders to stake through `gov/staker`, receive xGNS, delegate voting power, create proposals, and vote on protocol changes. For more details, check out [docs](https://docs.gnoswap.io/core-concepts/governance). ## Configuration The `governance.Config` type defines the governance parameters. The values used by a proposal are stored with that proposal; later reconfiguration does not rewrite an existing proposal. The current configuration can be changed through the governance `Reconfigure` handler. This type can be found in [../config.gno](../config.gno). | Field | Description | Default | |-------|-------------|---------| | `VotingStartDelay` | Delay before voting starts after proposal creation | 1 day | | `VotingPeriod` | Duration for collecting votes | 7 days | | `VotingWeightSmoothingDuration` | Duration used for timestamp-based voting-weight averaging | 1 day | | `Quorum` | Percentage of total xGNS supply required for proposal passage; total supply includes launchpad-held issuance | 50% | | `ProposalCreationThreshold` | Minimum xGNS balance required to create a proposal | 1,000,000,000 xGNS | | `ExecutionDelay` | Waiting period after voting ends before execution | 1 day | | `ExecutionWindow` | Time window during which an approved proposal can be executed | 30 days | All values are configurable; the values above are the defaults in `NewDefaultConfig`. ## Core Mechanics ### Staking Flow ```plain GNS → Delegate → xGNS + delegated voting history → Vote ``` 1. Delegate GNS through `gov/staker` to receive an equal amount of xGNS. 2. Assign voting power to a delegatee (which may be the delegator itself). 3. Vote on proposals with the delegatee's timestamped delegation weight. 4. Undelegation removes voting power immediately; the default lockup before collecting GNS is 7 days and is configurable. Launchpad-backed xGNS is included in total xGNS supply for quorum, but is not an ordinary user delegation record. ### Proposal Types - **Text**: Signal proposals without execution. - **CommunityPoolSpend**: Treasury disbursements encoded as a community-pool transfer on execution. - **ParameterChange**: Protocol parameter updates dispatched to registered handlers. ## Proposal Lifecycle ### Creation - Requires the configured `ProposalCreationThreshold` xGNS balance (1,000,000,000 xGNS by default). - One active proposal per address. - Valid type and parameters are required. Community-pool spend amounts must be strictly positive, recipients must be valid, and token paths must be registered. - A proposal stores its configuration version, creation timestamp, creation block height, quorum amount, and timestamp used for historical delegation lookup. The block height is metadata; voting-weight lookup is timestamp-based. ### Voting - Voting starts after the configured start delay (1 day by default) and runs for the configured voting period (7 days by default). - Weight is the average of the caller's delegation at the proposal's stored snapshot timestamp and at proposal creation time. The snapshot timestamp is `createdAt - VotingWeightSmoothingDuration` (clamped at zero), and the smoothing duration defaults to 24 hours. - Each address can vote only once on a proposal. `Vote` returns the applied vote weight as a decimal string. ### Execution A proposal is considered valid and executable when: - The voting period has ended. - Total votes meet the quorum amount computed at creation from the total xGNS supply (including launchpad-held issuance) and the proposal's configured quorum percentage. - `YES` votes strictly exceed `NO` votes (ties do not pass). - The configured execution delay (1 day by default) has passed after voting ends. - Execution occurs within the configured execution window (30 days by default). - Text proposals are informational and cannot be executed; anyone can trigger execution of an approved executable proposal. An approved community-pool spend can still fail at execution if the pool no longer has enough of the registered token. ## Technical Details ### Vote Weight Calculation ```go // `smoothing` is the proposal's configured VotingWeightSmoothingDuration. snapshotTime = max(createdAt - smoothing, 0) weightAtSnapshot = getDelegationAt(voter, snapshotTime) weightAtCreation = getDelegationAt(voter, createdAt) voteWeight = (weightAtSnapshot + weightAtCreation) / 2 ``` The lookups read delegation history by Unix timestamp. This is not a block-height snapshot. ### Quorum Calculation ```go quorumWeight = totalXGnsSupplyAtProposalCreation // includes launchpad-held xGNS quorumAmount = quorumWeight * quorumPercent / 100 // quorumPercent defaults to 50 ``` The quorum amount is stored on the proposal and is not recomputed from the proposer's or voters' smoothed voting weight. A proposal passes only when total votes reach quorum and accumulated `YES` votes strictly exceed accumulated `NO` votes. ### Rewards Distribution Gov/staker exposes two reward streams: 1. **GNS emission rewards** use the emission accumulator and each staker's own stake history. 2. **Protocol-fee rewards** are tracked per token and accrual epoch. Each consumed bucket is divided by the total stake in force during that epoch, added to that token's Q128 accumulator, and settled over the staker's stake-event segments. Sub-unit Q128 remainders are retained for later collection. `CollectReward` settles the emission stream and every known protocol-fee token. `CollectEmissionReward` and `CollectProtocolFeeReward(tokenPath)` are narrower paths; the token-specific protocol-fee path bounds both pending accrual buckets and stake events per call, so remaining work is collected later. Launchpad project wallets use the corresponding launchpad-only entry points. ## Usage These snippets call the public domain proxies from a realm function with a current `cur` token. Import the corresponding proxy packages and qualify their function names in integrating code. ```go // Through gov/staker: delegate GNS for xGNS voting power. Delegate(cross(cur), delegatee, 1_000_000_000, "g1referrer...") // Create a text or community-pool proposal. ProposeText(cross(cur), "Title", "Description") ProposeCommunityPoolSpend(cross(cur), "Title", "Description", recipient, tokenPath, amount) // A parameter-change execution uses a registered handler. This is one valid // Reconfigure message (all seven parameters are required). execution := "gno.land/r/gnoswap/gov/governance*EXE*Reconfigure*EXE*86400,604800,86400,50,1000000000,86400,2592000" ProposeParameterChange(cross(cur), "Update config", "Rationale", 1, execution) // Vote; the return value is the applied weight formatted as a decimal string. voteWeight := Vote(cross(cur), proposalId, true) // YES // Execute after the configured timelock and then, if needed, start undelegation. Execute(cross(cur), proposalId) Undelegate(cross(cur), delegatee, 250_000_000) // Collect only after the configured undelegation lockup has expired. CollectUndelegatedGns(cross(cur)) ``` ## Security - Timestamp-based smoothing reduces flash-loan-style voting manipulation; it is not a block snapshot. - Sybil resistance comes from stake-weighted delegation. - The execution delay and window constrain when approved executable proposals can run. - A single active proposal is allowed per proposer address. - Quorum is fixed from the creation-time total xGNS supply, including launchpad-held issuance. - Community-pool balance is checked when the approved transfer executes, not when the proposal is created.
- #4assert.gno
- #5package governance import ( "errors" ufmt "gno.land/p/nt/ufmt/v0" "gno.land/r/gnoswap/common" "gno.land/r/gnoswap/halt/v1" "gno.land/r/gnoswap/gov/governance" ) // assertIsNotHaltedGovernanceForProposalData panics if governance is halted, // unless the proposal only targets the halt realm. // This keeps a recovery path open so governance can unhalt itself. func assertIsNotHaltedGovernanceForProposalData(proposalData *governance.ProposalData) { if isHaltRecoveryProposalData(proposalData) { return } halt.AssertIsNotHaltedGovernance() } // assertIsNotHaltedGovernanceForProposal panics if governance is halted, // unless the proposal only targets the halt realm. func assertIsNotHaltedGovernanceForProposal(gv *governanceV1, proposalID int64) { proposal, exists := gv.getProposal(proposalID) if !exists { halt.AssertIsNotHaltedGovernance() return } assertIsNotHaltedGovernanceForProposalData(proposal.Data()) } // isHaltRecoveryProposalData returns true if the proposal is a parameter change // whose every execution targets the halt realm. func isHaltRecoveryProposalData(proposalData *governance.ProposalData) bool { if proposalData == nil || proposalData.ProposalType() != governance.ParameterChange { return false } infos, err := NewProposalDataResolver(proposalData).ParameterChangesInfos() if err != nil || len(infos) == 0 { return false } for _, info := range infos { if info.PkgPath() != HALT_PATH { return false } } return true } // assertCallerIsProposer panics if the caller is not the proposer of the given proposal. func assertCallerIsProposer(gv *governanceV1, proposalID int64, caller address) { proposal, exists := gv.getProposal(proposalID) if !exists { panic(errors.New(errProposalNotFound)) } if !proposal.IsProposer(caller) { panic(errors.New(errNotProposer)) } } func assertIsValidSmoothingPeriod(smoothingPeriod int64) { if smoothingPeriod < 0 { panic(errors.New(errInvalidSmoothingPeriod)) } if smoothingPeriod > maxSmoothingPeriod { panic(errors.New(errInvalidSmoothingPeriod)) } } func assertIsValidToken(tokenPath string) { if tokenPath == "" { panic(makeErrorWithDetails( errInvalidInput, "tokenPath is empty")) } if err := common.IsRegistered(tokenPath); err != nil { panic(makeErrorWithDetails( errInvalidInput, ufmt.Sprintf("token(%s) is not registered", tokenPath), )) } }
- #6config.gno
- #7package governance import ( "chain" "time" "gno.land/p/gnoswap/utils/v1" "gno.land/r/gnoswap/access/v1" en "gno.land/r/gnoswap/emission" "gno.land/r/gnoswap/halt/v1" "gno.land/r/gnoswap/gov/governance" ) // makeConfig creates a new governance configuration. func makeConfig( votingStartDelay int64, votingPeriod int64, votingWeightSmoothingDuration int64, quorum int64, proposalCreationThreshold int64, executionDelay int64, executionWindow int64, ) governance.Config { // Create new configuration with provided parameters. Constructed via the // domain constructor so the Config value is allocated inside the governance // domain realm (satisfies the realm allocation check). return governance.NewConfig( votingStartDelay, votingPeriod, votingWeightSmoothingDuration, quorum, proposalCreationThreshold, executionDelay, executionWindow, ) } // Reconfigure updates governance configuration. // Only admin or governance contract can call this function. // Updates all governance parameters and emits a "Reconfigure" event. // // Parameters: // - _: Noncrossing implementation-call discriminator; pass 0. // - rlm: Current realm context forwarded unchanged by the governance proxy; validated before external calls. // - votingStartDelay: delay in seconds from proposal creation until voting starts // - votingPeriod: duration in seconds for which voting remains open // - votingWeightSmoothingDuration: duration in seconds used to smooth voting weight snapshots // - quorum: quorum percentage applied to the proposal's quorum-weight base // - proposalCreationThreshold: minimum voting weight required to create a proposal // - executionDelay: delay in seconds from voting end until execution becomes available // - executionWindow: duration in seconds during which an executable proposal may run // // Returns: // - int64: newly stored governance configuration version func (gv *governanceV1) Reconfigure( _ int, rlm realm, votingStartDelay int64, votingPeriod int64, votingWeightSmoothingDuration int64, quorum int64, proposalCreationThreshold int64, executionDelay int64, executionWindow int64, ) int64 { access.AssertIsRlmCurrent(0, rlm) // Check if system is halted before proceeding halt.AssertIsNotHaltedGovernance() prev := rlm.Previous() caller := prev.Address() access.AssertIsAdminOrGovernance(caller) assertIsValidSmoothingPeriod(votingWeightSmoothingDuration) // Create and validate new configuration newCfg := makeConfig( votingStartDelay, votingPeriod, votingWeightSmoothingDuration, quorum, proposalCreationThreshold, executionDelay, executionWindow, ) currentTime := time.Now().Unix() if err := newCfg.IsValid(currentTime); err != nil { panic(makeErrorWithDetails(errInvalidConfiguration, err.Error())) } // Mint and distribute GNS tokens as part of the process en.MintAndDistributeGns(cross(rlm)) // Store previous version for event emission previousVersion := gv.getCurrentConfigVersion() // Apply the new configuration nextVersion := gv.reconfigure(0, rlm, newCfg) chain.Emit( "Reconfigure", "prevAddr", caller.String(), "prevRealm", prev.PkgPath(), "votingStartDelay", utils.FormatInt(newCfg.VotingStartDelay), "votingPeriod", utils.FormatInt(newCfg.VotingPeriod), "votingWeightSmoothingDuration", utils.FormatInt(newCfg.VotingWeightSmoothingDuration), "quorum", utils.FormatInt(newCfg.Quorum), "proposalCreationThreshold", utils.FormatInt(newCfg.ProposalCreationThreshold), "executionDelay", utils.FormatInt(newCfg.ExecutionDelay), "executionPeriod", utils.FormatInt(newCfg.ExecutionWindow), "newConfigVersion", utils.FormatInt(nextVersion), "prevConfigVersion", utils.FormatInt(previousVersion), ) return nextVersion } // reconfigure stores the validated configuration with incremented version number. func (gv *governanceV1) reconfigure(_ int, rlm realm, cfg governance.Config) int64 { // Generate next version number nextVersion := gv.nextConfigVersion(0, rlm) // Store the new configuration with version err := gv.setConfig(0, rlm, nextVersion, cfg) if err != nil { panic("failed to set config: " + err.Error()) } return nextVersion }
- #8consts.gno
- #9package governance const ( // Governance can execute multiple messages in a single proposal // each message is a string with the following format: // <pkgPath>*EXE*<function>*EXE*<params> // To execute a message, we need to parse the message and call the corresponding function // with the given parameters parameterSeparator = "*EXE*" messageSeparator = "*GOV*" maxTitleLength = 255 maxDescriptionLength = 10_000 maxNumberOfExecution = 10 maxSmoothingPeriod = 30 * 60 * 60 * 24 // 30 days )
- #10doc.gno
- #11package governance
- #12errors.gno
- #13package governance import ( ufmt "gno.land/p/nt/ufmt/v0" ) const ( errInvalidInput = "[GNOSWAP-GOVERNANCE-001] invalid input" errDataNotFound = "[GNOSWAP-GOVERNANCE-002] requested data not found" errNotEnoughBalance = "[GNOSWAP-GOVERNANCE-003] not enough balance" errAlreadyVoted = "[GNOSWAP-GOVERNANCE-004] cannot vote twice" errNotEnoughVotingWeight = "[GNOSWAP-GOVERNANCE-005] not enough voting power" errAlreadyCanceledProposal = "[GNOSWAP-GOVERNANCE-006] cannot cancel already canceled proposal" errUnableToCancelVotingProposal = "[GNOSWAP-GOVERNANCE-007] unable to cancel voting proposal" errTextProposalNotExecutable = "[GNOSWAP-GOVERNANCE-008] cannot execute text proposal" errUnableToVoteOutOfPeriod = "[GNOSWAP-GOVERNANCE-009] unable to vote out of voting period" errInvalidMessageFormat = "[GNOSWAP-GOVERNANCE-010] invalid message format" errAlreadyActiveProposal = "[GNOSWAP-GOVERNANCE-011] already active proposal" errProposalNotFound = "[GNOSWAP-GOVERNANCE-012] proposal not found" errProposalNotExecutable = "[GNOSWAP-GOVERNANCE-013] proposal not executable" errNotProposer = "[GNOSWAP-GOVERNANCE-014] not proposer" errInvalidConfiguration = "[GNOSWAP-GOVERNANCE-015] invalid configuration" errInvalidExecution = "[GNOSWAP-GOVERNANCE-016] invalid execution: handler not found" errInvalidSmoothingPeriod = "[GNOSWAP-GOVERNANCE-017] invalid smoothing period" errProposalIndexNeedsCleanup = "[GNOSWAP-GOVERNANCE-018] active proposal index requires pruning" ) // makeErrorWithDetails creates an error with additional context. func makeErrorWithDetails(message string, detail string) error { return ufmt.Errorf("%s || %s", message, detail) }
- #14getter.gno
- #15package governance import ( "errors" "time" rotree "gno.land/p/nt/bptree/rotree/v0" ufmt "gno.land/p/nt/ufmt/v0" "gno.land/r/gnoswap/gov/governance" ) // GetLatestConfigVersion returns the current governance configuration version. // // Returns: // - int64: latest configuration version stored by governance. func (gv *governanceV1) GetLatestConfigVersion() int64 { return gv.getCurrentConfigVersion() } // GetCurrentProposalID returns the current proposal ID counter. // // Returns: // - int64: current proposal ID counter used for newly created proposals. func (gv *governanceV1) GetCurrentProposalID() int64 { return gv.getCurrentProposalID() } // GetMaxSmoothingPeriod returns the maximum smoothing period for delegation history cleanup. // // Returns: // - int64: maximum permitted smoothing period, in seconds. func (gv *governanceV1) GetMaxSmoothingPeriod() int64 { return maxSmoothingPeriod } // GetLatestConfig returns the latest governance configuration. // // Returns: // - governance.Config: latest stored configuration for the current version. // - error: nil on success; an error when the current configuration is missing. func (gv *governanceV1) GetLatestConfig() (governance.Config, error) { currentVersion := gv.getCurrentConfigVersion() config, ok := gv.getConfig(currentVersion) if !ok { return governance.NewConfig(0, 0, 0, 0, 0, 0, 0), errors.New(errDataNotFound) } return config, nil } // GetConfig returns a specific governance configuration by version. // // Parameters: // - configVersion: configuration version to retrieve. // // Returns: // - governance.Config: configuration stored under configVersion. // - error: nil on success; an error when configVersion is not found. func (gv *governanceV1) GetConfig(configVersion int64) (governance.Config, error) { config, ok := gv.getConfig(configVersion) if !ok { // Construct the zero Config via the domain constructor so it is // allocated inside the governance domain realm (realm allocation check). return governance.NewConfig(0, 0, 0, 0, 0, 0, 0), ufmt.Errorf("config version %d not found", configVersion) } return config, nil } // GetProposals returns a read-only view of every proposal, keyed by the decimal // string form of the proposal ID. // // Returns: // - *rotree.ReadOnlyTree: read-only proposal tree with decimal proposal IDs as keys. func (gv *governanceV1) GetProposals() *rotree.ReadOnlyTree { return rotree.Wrap(gv.store.GetProposals(), cloneProposalEntry) } // ExistsProposal checks if a proposal exists. // // Parameters: // - proposalID: proposal identifier to look up. // // Returns: // - bool: true when proposalID is stored, otherwise false. func (gv *governanceV1) ExistsProposal(proposalID int64) bool { _, exists := gv.store.GetProposal(proposalID) return exists } // GetProposerByProposalId returns the proposer address of a proposal. // // Parameters: // - proposalId: proposal identifier to look up. // // Returns: // - address: address that submitted the proposal. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetProposerByProposalId(proposalId int64) (address, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return "", ufmt.Errorf("proposal %d not found", proposalId) } return proposal.Proposer(), nil } // GetProposalTypeByProposalId returns the type of a proposal. // // Parameters: // - proposalId: proposal identifier to look up. // // Returns: // - governance.ProposalType: type recorded for the proposal. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetProposalTypeByProposalId(proposalId int64) (governance.ProposalType, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return governance.ProposalType(0), ufmt.Errorf("proposal %d not found", proposalId) } return proposal.Type(), nil } // GetProposalCreatedAt returns the creation timestamp of a proposal. // // Parameters: // - proposalId: proposal identifier to look up. // // Returns: // - int64: Unix timestamp at which the proposal was created. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetProposalCreatedAt(proposalId int64) (int64, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return 0, ufmt.Errorf("proposal %d not found", proposalId) } return proposal.CreatedAt(), nil } // GetProposalCreatedHeight returns the creation block height of a proposal. // // Parameters: // - proposalId: proposal identifier to look up. // // Returns: // - int64: block height at which the proposal was created. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetProposalCreatedHeight(proposalId int64) (int64, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return 0, ufmt.Errorf("proposal %d not found", proposalId) } return proposal.CreatedHeight(), nil } // GetProposalCommunityPoolSpendInfo returns the community pool spend info for a proposal. // // Parameters: // - proposalID: proposal identifier to inspect. // // Returns: // - *governance.CommunityPoolSpendInfo: spend details stored in the proposal. // - error: nil on success; an error when the proposal is missing or is not a // community pool spend proposal. func (gv *governanceV1) GetProposalCommunityPoolSpendInfo(proposalID int64) (*governance.CommunityPoolSpendInfo, error) { proposal, exists := gv.store.GetProposal(proposalID) if !exists { return nil, ufmt.Errorf("proposal %d not found", proposalID) } proposalData := proposal.Data() if proposalData == nil || proposalData.CommunityPoolSpend() == nil { return nil, ufmt.Errorf("proposal is not a community pool spend proposal") } return proposalData.CommunityPoolSpend(), nil } // GetProposalExecutionInfo returns the execution info for a proposal. // // Parameters: // - proposalID: proposal identifier to inspect. // // Returns: // - *governance.ExecutionInfo: execution messages and count stored in the proposal. // - error: nil on success; an error when the proposal is missing or is not a // parameter change proposal. func (gv *governanceV1) GetProposalExecutionInfo(proposalID int64) (*governance.ExecutionInfo, error) { proposal, exists := gv.store.GetProposal(proposalID) if !exists { return nil, ufmt.Errorf("proposal %d not found", proposalID) } proposalData := proposal.Data() if proposalData == nil || proposalData.Execution() == nil { return nil, ufmt.Errorf("proposal is not a parameter change proposal") } return proposalData.Execution(), nil } // GetYeaByProposalId returns the yes vote weight of a proposal. // // Parameters: // - proposalId: proposal identifier to inspect. // // Returns: // - int64: total voting weight recorded for yes votes. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetYeaByProposalId(proposalId int64) (int64, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return 0, ufmt.Errorf("proposal %d not found", proposalId) } return proposal.Status().YesWeight(), nil } // GetNayByProposalId returns the no vote weight of a proposal. // // Parameters: // - proposalId: proposal identifier to inspect. // // Returns: // - int64: total voting weight recorded for no votes. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetNayByProposalId(proposalId int64) (int64, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return 0, ufmt.Errorf("proposal %d not found", proposalId) } return proposal.Status().NoWeight(), nil } // GetConfigVersionByProposalId returns the config version used by a proposal. // // Parameters: // - proposalId: proposal identifier to inspect. // // Returns: // - int64: governance configuration version captured by the proposal. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetConfigVersionByProposalId(proposalId int64) (int64, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return 0, ufmt.Errorf("proposal %d not found", proposalId) } return proposal.ConfigVersion(), nil } // GetQuorumAmountByProposalId returns the quorum requirement for a proposal. // // Parameters: // - proposalId: proposal identifier to inspect. // // Returns: // - int64: minimum voting weight required for the proposal's quorum. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetQuorumAmountByProposalId(proposalId int64) (int64, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return 0, ufmt.Errorf("proposal %d not found", proposalId) } return proposal.Status().VoteStatus().QuorumAmount(), nil } // GetTitleByProposalId returns the title of a proposal. // // Parameters: // - proposalId: proposal identifier to inspect. // // Returns: // - string: proposal title. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetTitleByProposalId(proposalId int64) (string, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return "", ufmt.Errorf("proposal %d not found", proposalId) } return proposal.Metadata().Title(), nil } // GetDescriptionByProposalId returns the description of a proposal. // // Parameters: // - proposalId: proposal identifier to inspect. // // Returns: // - string: proposal description. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetDescriptionByProposalId(proposalId int64) (string, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return "", ufmt.Errorf("proposal %d not found", proposalId) } return proposal.Metadata().Description(), nil } // GetProposalStatusByProposalId returns the current status of a proposal. // // Parameters: // - proposalId: proposal identifier to inspect. // // Returns: // - string: status computed from the proposal and the current Unix time. // - error: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetProposalStatusByProposalId(proposalId int64) (string, error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return "", ufmt.Errorf("proposal %d not found", proposalId) } proposalResolver := NewProposalResolver(proposal) return proposalResolver.Status(time.Now().Unix()), nil } // GetVoteStatus returns the vote status of a proposal. // // Parameters: // - proposalId: proposal identifier to inspect. // // Returns: // - quorum: minimum vote weight required for proposal to pass. // - maxVotingWeight: maximum possible voting weight recorded for the proposal. // - yesWeight: total weight of "yes" votes. // - noWeight: total weight of "no" votes. // - err: nil on success; an error when the proposal does not exist. func (gv *governanceV1) GetVoteStatus(proposalId int64) (quorum, maxVotingWeight, yesWeight, noWeight int64, err error) { proposal, exists := gv.store.GetProposal(proposalId) if !exists { return 0, 0, 0, 0, ufmt.Errorf("proposal %d not found", proposalId) } voting := proposal.Status().VoteStatus() return voting.QuorumAmount(), voting.MaxVotingWeight(), voting.YesWeight(), voting.NoWeight(), nil } // GetVotingInfos returns a read-only view of a proposal's value-backed voting // infos, keyed by voter address. Existing proposals have an empty view before // the first vote; nil is returned only when the stored voting-info tree is absent. // // Parameters: // - proposalID: proposal identifier whose voting information is requested. // // Returns: // - *rotree.ReadOnlyTree: read-only voting-info tree keyed by voter address, or // nil when no tree is stored for proposalID. func (gv *governanceV1) GetVotingInfos(proposalID int64) *rotree.ReadOnlyTree { votingInfos, exists := gv.getProposalUserVotingInfos(proposalID) if !exists { return nil } return rotree.Wrap(votingInfos, nil) } // ExistsVotingInfo checks if a voting info exists for a user on a proposal. // // Parameters: // - proposalID: proposal identifier to inspect. // - addr: voter address to look up within proposalID. // // Returns: // - bool: true when voting information exists for the proposal and address. func (gv *governanceV1) ExistsVotingInfo(proposalID int64, addr address) bool { _, exists := gv.getProposalUserVotingInfo(proposalID, addr) return exists } // GetVoteWeight returns the voting weight of an address for a proposal. // // Parameters: // - proposalID: proposal identifier to inspect. // - addr: voter address whose recorded vote weight is requested. // // Returns: // - int64: weight recorded for addr's vote. // - error: nil on success; an error when no voting information exists for the // proposal and address. func (gv *governanceV1) GetVoteWeight(proposalID int64, addr address) (int64, error) { votingInfo, exists := gv.getProposalUserVotingInfo(proposalID, addr) if !exists { return 0, ufmt.Errorf("voting info not found for proposal %d and address %s", proposalID, addr.String()) } return votingInfo.VotedWeight(), nil } // GetVotedHeight returns the block height when an address voted on a proposal. // // Parameters: // - proposalID: proposal identifier to inspect. // - addr: voter address whose vote height is requested. // // Returns: // - int64: block height recorded for addr's vote. // - error: nil on success; an error when no voting information exists for the // proposal and address. func (gv *governanceV1) GetVotedHeight(proposalID int64, addr address) (int64, error) { votingInfo, exists := gv.getProposalUserVotingInfo(proposalID, addr) if !exists { return 0, ufmt.Errorf("voting info not found for proposal %d and address %s", proposalID, addr.String()) } return votingInfo.VotedHeight(), nil } // GetVotedAt returns the timestamp when an address voted on a proposal. // // Parameters: // - proposalID: proposal identifier to inspect. // - addr: voter address whose vote timestamp is requested. // // Returns: // - int64: Unix timestamp recorded for addr's vote. // - error: nil on success; an error when no voting information exists for the // proposal and address. func (gv *governanceV1) GetVotedAt(proposalID int64, addr address) (int64, error) { votingInfo, exists := gv.getProposalUserVotingInfo(proposalID, addr) if !exists { return 0, ufmt.Errorf("voting info not found for proposal %d and address %s", proposalID, addr.String()) } return votingInfo.VotedAt(), nil } // GetUserProposals returns a read-only view of the proposals created per user, // keyed by creator address with the creator's proposal IDs as the value. // // Returns: // - *rotree.ReadOnlyTree: read-only user-proposal tree keyed by creator address. func (gv *governanceV1) GetUserProposals() *rotree.ReadOnlyTree { return rotree.Wrap(gv.store.GetUserProposals(), cloneProposalIDsEntry) } // GetOldestActiveProposalSnapshotTime examines only the first timestamp-ordered index entry. // If it is inactive, the error identifies the proposal requiring maintenance // cleanup. An error must never be treated as an empty index when deleting // delegation history. // // Returns: // - int64: snapshot timestamp of the oldest active proposal, or zero when the // index is empty or an error occurs. // - bool: true when an active proposal was found at the oldest index entry. // - error: nil for an empty index or active entry; an error when the entry's // proposal is missing or inactive and the index needs cleanup. func (gv *governanceV1) GetOldestActiveProposalSnapshotTime() (int64, bool, error) { index := gv.store.GetActiveProposalsBySnapshot() if index.Size() == 0 { return 0, false, nil } _, value := index.GetByIndex(0) proposalID, ok := value.(int64) if !ok { panic(ufmt.Sprintf("failed to cast active proposal ID to int64: %T", value)) } proposal, exists := gv.getProposal(proposalID) if !exists { return 0, false, makeErrorWithDetails( errProposalNotFound, ufmt.Sprintf("proposalID: %d", proposalID), ) } if !NewProposalResolver(proposal).IsActive(time.Now().Unix()) { return 0, false, makeErrorWithDetails( errProposalIndexNeedsCleanup, ufmt.Sprintf("proposalID: %d", proposalID), ) } return proposal.SnapshotTime(), true, nil } // GetCurrentVotingWeightSnapshot returns the current total voting weight and // timestamp anchor computed with the configured smoothing duration. // // Returns: // - int64: total voting weight averaged over the configured smoothing window. // - int64: Unix timestamp used as the snapshot history anchor. // - error: nil on success; an error when the current configuration or snapshot // data cannot be retrieved. func (gv *governanceV1) GetCurrentVotingWeightSnapshot() (int64, int64, error) { current := time.Now().Unix() config, ok := gv.getCurrentConfig() if !ok { return 0, 0, ufmt.Errorf("current config not found") } return gv.getVotingWeightSnapshot(current, config.VotingWeightSmoothingDuration) }
- #16getter_utils.gno
- #17package governance import ( "gno.land/r/gnoswap/gov/governance" ) // The functions below are makeEntrySafeFn transformations handed to // rotree.Wrap. They run on every read through a read-only view, so each one // returns a value the caller may hold without reaching realm-owned state. // cloneProposalEntry is the transformation for the proposal tree. Proposal is // mutable and its setters are declared in /r/gnoswap/gov/governance, so an // external holder of the stored pointer could write realm state through it. // // Proposal.Clone leaves the type-specific data nil; see it for which lookups // read it instead. func cloneProposalEntry(value any) any { proposal, ok := value.(*governance.Proposal) if !ok { return nil } return proposal.Clone() } // cloneProposalIDsEntry is the transformation for the user proposal tree. The // stored value is a slice, so hand out a copy rather than letting a reader // write through the shared backing array. func cloneProposalIDsEntry(value any) any { proposalIDs, ok := value.([]int64) if !ok { return nil } cloned := make([]int64, len(proposalIDs)) copy(cloned, proposalIDs) return cloned }
- #18gnomod.toml
- #19module = "gno.land/r/gnoswap/gov/governance/v1" gno = "0.9"
- #20governance_execute.gno
- #21package governance import ( "chain" "chain/runtime" "errors" "time" ufmt "gno.land/p/nt/ufmt/v0" "gno.land/r/gnoswap/access/v1" "gno.land/p/gnoswap/utils/v1" "gno.land/r/gnoswap/common" "gno.land/r/gnoswap/emission" "gno.land/r/gnoswap/halt/v1" "gno.land/r/gnoswap/gov/governance" ) // Execute executes an approved proposal. // // Processes and implements governance decisions after successful voting. // Enforces the configured execution delay and execution window. // Anyone can trigger execution to ensure decentralization. // // Parameters: // - _: Internal call discriminator; pass 0. // - rlm: Propagated realm context; must be current for authorized governance state updates and is crossed into execution-dependent calls. // - proposalID: ID of the proposal to execute // // Requirements: // - Proposal must have passed (quorum and strict yes-over-no majority) // - Quorum must be reached using the proposal's stored amount, derived from // the total xGNS supply and configured quorum percentage at creation // - Execution delay must have elapsed (1 day by default) // - Must be within the execution window (30 days by default) // - Proposal not already executed or cancelled // // Effects: // - Executes proposal actions (parameter changes, treasury transfers) // - Marks proposal as executed // - Emits execution event // // Returns: // - proposalID: ID of the executed proposal // // Callable by anyone once the proposal is executable. func (gv *governanceV1) Execute(_ int, rlm realm, proposalID int64) int64 { access.AssertIsRlmCurrent(0, rlm) // Check if execution is allowed (system not halted for execution). // Halt recovery proposals remain executable while governance is halted. assertIsNotHaltedGovernanceForProposal(gv, proposalID) // Get caller information and current blockchain state prev := rlm.Previous() caller := prev.Address() currentHeight := runtime.ChainHeight() currentAt := time.Now().Unix() // Mint and distribute GNS tokens as part of the execution process emission.MintAndDistributeGns(cross(rlm)) // Attempt to execute the proposal with current context proposal, err := gv.executeProposal( 0, rlm, proposalID, currentAt, currentHeight, caller, ) if err != nil { panic(err) } if err := gv.removeActiveProposal(0, rlm, proposal); err != nil { panic(err) } // Emit execution event for tracking and auditing chain.Emit( "Execute", "prevAddr", prev.Address().String(), "prevRealm", prev.PkgPath(), "proposalId", utils.FormatInt(proposalID), ) return proposal.ID() } // executeProposal handles core logic of proposal execution. func (gv *governanceV1) executeProposal( _ int, rlm realm, proposalID int64, executedAt int64, executedHeight int64, executedBy address, ) (*governance.Proposal, error) { // Retrieve the proposal from storage proposal, ok := gv.getProposal(proposalID) if !ok { return nil, errors.New(errDataNotFound) } // Text proposals cannot be executed (they are informational only) if proposal.IsTextType() { return nil, errors.New(errTextProposalNotExecutable) } proposalResolver := NewProposalResolver(proposal) // Verify proposal is in executable state (timing and voting requirements met) if !proposalResolver.IsExecutable(executedAt) { return nil, errors.New(errProposalNotExecutable) } // Mark proposal as executed in its status err := proposalResolver.execute(executedAt, executedHeight, executedBy) if err != nil { return nil, err } // Execute proposal based on its type switch proposal.Type() { case governance.CommunityPoolSpend: // Execute community pool spending (token transfers) err = executeCommunityPoolSpend(0, rlm, proposal, globalParameterRegistry, executedAt, executedHeight, executedBy) if err != nil { return nil, err } case governance.ParameterChange: // Execute parameter changes (governance configuration updates) err = executeParameterChange(0, rlm, proposal, globalParameterRegistry, executedAt, executedHeight, executedBy) if err != nil { return nil, err } } return proposal, nil } // Cancel cancels a proposal in upcoming status. // // Allows proposers to withdraw their proposals before voting begins. // Prevents accidental or malicious proposals from reaching vote. // Safety mechanism for proposal errors or changed circumstances. // // Parameters: // - _: Internal call discriminator; pass 0. // - rlm: Propagated realm context; must be current for authorized governance state updates and is crossed into reward-distribution calls. // - proposalID: ID of the proposal to cancel // // Requirements: // - Must be called by original proposer // - Proposal must be in "upcoming" status // - Voting must not have started yet // - Proposal not already cancelled or executed // // Effects: // - Sets proposal status to "cancelled" // - Prevents future voting or execution // - Emits cancellation event // - Frees up proposer's proposal slot // // Returns: // - proposalID: ID of the cancelled proposal // // Only callable by original proposer before voting begins. func (gv *governanceV1) Cancel(_ int, rlm realm, proposalID int64) int64 { access.AssertIsRlmCurrent(0, rlm) halt.AssertIsNotHaltedGovernance() prev := rlm.Previous() caller := prev.Address() assertCallerIsProposer(gv, proposalID, caller) // Get current blockchain state and caller information currentHeight := runtime.ChainHeight() currentAt := time.Now().Unix() // Mint and distribute GNS tokens as part of the process emission.MintAndDistributeGns(cross(rlm)) // Attempt to cancel the proposal proposal, err := gv.cancel(proposalID, currentAt, currentHeight, caller) if err != nil { panic(err) } if err := gv.removeActiveProposal(0, rlm, proposal); err != nil { panic(err) } // Emit cancellation event for tracking chain.Emit( "Cancel", "prevAddr", prev.Address().String(), "prevRealm", prev.PkgPath(), "proposalId", utils.FormatInt(proposalID), ) return proposal.ID() } // RemoveInactiveProposalFromIndex removes one inactive proposal from the // snapshot index without deleting its proposal or voting history. // Parameters: // - _: Internal call discriminator; pass 0. // - rlm: Propagated realm context used for the authorized snapshot-index update. // - proposalID: Identifier of the inactive proposal to remove from the snapshot index. func (gv *governanceV1) RemoveInactiveProposalFromIndex(_ int, rlm realm, proposalID int64) { access.AssertIsRlmCurrent(0, rlm) halt.AssertIsNotHaltedGovernance() proposal, exists := gv.getProposal(proposalID) if !exists { panic(makeErrorWithDetails(errProposalNotFound, ufmt.Sprintf("proposalID: %d", proposalID))) } if NewProposalResolver(proposal).IsActive(time.Now().Unix()) { panic(makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("proposal %d is still active", proposalID))) } if err := gv.removeActiveProposal(0, rlm, proposal); err != nil { panic(err) } prev := rlm.Previous() chain.Emit( "RemoveInactiveProposalFromIndex", "prevAddr", prev.Address().String(), "prevRealm", prev.PkgPath(), "proposalId", utils.FormatInt(proposalID), ) } // cancel handles core logic of proposal cancellation. // Validates proposal state and updates status to canceled. func (gv *governanceV1) cancel( proposalID, canceledAt, canceledHeight int64, canceledBy address, ) (proposal *governance.Proposal, err error) { // Retrieve the proposal from storage proposal, ok := gv.getProposal(proposalID) if !ok { return nil, errors.New(errDataNotFound) } proposalResolver := NewProposalResolver(proposal) // Attempt to cancel the proposal (this validates cancellation conditions) err = proposalResolver.cancel(canceledAt, canceledHeight, canceledBy) if err != nil { return nil, err } return proposal, nil } // executeCommunityPoolSpend executes community pool spending proposals. // Handles token transfers from community pool to specified recipients. func executeCommunityPoolSpend( _ int, rlm realm, proposal *governance.Proposal, parameterRegistry *ParameterRegistry, executedAt int64, executedHeight int64, executedBy address, ) error { // Verify token registration for community pool spending if proposal.IsCommunityPoolSpendType() { common.MustRegistered(proposal.Data().CommunityPoolSpend().TokenPath()) } // Execute all parameter changes defined in the proposal dataResolver := NewProposalDataResolver(proposal.Data()) parameterChangesInfos, err := dataResolver.ParameterChangesInfos() if err != nil { return err } for _, parameterChangeInfo := range parameterChangesInfos { // Get the appropriate handler for this parameter change key := makeHandlerKey(parameterChangeInfo.PkgPath(), parameterChangeInfo.Function()) handler, err := parameterRegistry.Handler(key) if err != nil { return err } // Execute the parameter change with provided parameters err = handler.Execute(0, rlm, parameterChangeInfo.Params()) if err != nil { return err } } return nil } // executeParameterChange executes parameter change proposals. // Handles governance configuration updates and system parameter modifications. func executeParameterChange( _ int, rlm realm, proposal *governance.Proposal, parameterRegistry *ParameterRegistry, executedAt int64, executedHeight int64, executedBy address, ) error { // Execute all parameter changes defined in the proposal dataResolver := NewProposalDataResolver(proposal.Data()) parameterChangesInfos, err := dataResolver.ParameterChangesInfos() if err != nil { return err } for _, parameterChangeInfo := range parameterChangesInfos { // Get the appropriate handler for this parameter change key := makeHandlerKey(parameterChangeInfo.PkgPath(), parameterChangeInfo.Function()) handler, err := parameterRegistry.Handler(key) if err != nil { return err } // Execute the parameter change with provided parameters err = handler.Execute(0, rlm, parameterChangeInfo.Params()) if err != nil { return err } } return nil }
- #22governance_propose.gno
- #23package governance import ( "chain" "chain/runtime" "errors" "time" "gno.land/r/gnoswap/access/v1" gnsmath "gno.land/p/gnoswap/gnsmath/v1" "gno.land/p/gnoswap/utils/v1" "gno.land/r/gnoswap/gov/xgns" "gno.land/r/gnoswap/halt/v1" "gno.land/r/gnoswap/gov/governance" ) // ProposeText creates a text proposal for community discussion. // // Signal proposals for non-binding community sentiment. // Used for policy discussions, roadmap planning, and community feedback. // No on-chain execution, serves as formal governance record. // // Parameters: // // - _: Internal call discriminator; pass 0. // // - rlm: Propagated realm context; validated as current before proposal state writes. // // - title: Short, descriptive proposal title (up to 255 characters; 100 is a UX recommendation) // // - description: Full proposal content with rationale and context // // Requirements: // - Caller must hold at least the configured ProposalCreationThreshold amount in xGNS // - No other active proposal from same address // - Title and description must be non-empty // // Process: // - Configured delay before voting starts (1 day by default) // - Configured voting period (7 days by default) // - Quorum and a strict yes-over-no majority determine the outcome // - No execution phase (signal only) // // Returns: // - newProposalId: ID of the newly created text proposal func (gv *governanceV1) ProposeText( _ int, rlm realm, title string, description string, ) (newProposalId int64) { access.AssertIsRlmCurrent(0, rlm) halt.AssertIsNotHaltedGovernance() prev := rlm.Previous() callerAddress := prev.Address() createdAt := time.Now().Unix() createdHeight := runtime.ChainHeight() xgnsBalance := xgns.BalanceOf(callerAddress) config, ok := gv.getCurrentConfig() if !ok { panic(errors.New(errDataNotFound)) } // Clean up inactive user proposals before checking if caller already has an active proposal err := gv.removeInactiveUserProposals(0, rlm, callerAddress, createdAt) if err != nil { panic(err) } // Check if caller already has an active proposal (one proposal per address) if gv.hasActiveProposal(callerAddress) { panic(errors.New(errAlreadyActiveProposal)) } // Get timestamp anchor and total voting weight for proposal creation maxVotingWeight, snapshotTime, err := gv.getVotingWeightSnapshot( createdAt, config.VotingWeightSmoothingDuration, ) if err != nil { panic(err) } quorumWeight := gv.stakerAccessor.GetTotalxGnsSupply() // Create the text proposal with metadata proposal, err := gv.createProposal( 0, rlm, governance.Text, config, maxVotingWeight, quorumWeight, snapshotTime, governance.NewProposalMetadata(title, description), NewProposalTextData(), callerAddress, xgnsBalance, createdAt, createdHeight, ) if err != nil { panic(err) } // Initialize empty voting info tree for this proposal (votes will be added as users vote) err = gv.updateProposalUserVotes(0, rlm, proposal, governance.NewProposalUserVotingInfoTree()) if err != nil { panic(err) } // Emit proposal creation event for indexing and tracking chain.Emit( "ProposeText", "prevAddr", prev.Address().String(), "prevRealm", prev.PkgPath(), "title", title, "proposalId", utils.FormatInt(proposal.ID()), "quorumAmount", utils.FormatInt(proposal.VotingQuorumAmount()), "maxVotingWeight", utils.FormatInt(proposal.VotingMaxWeight()), "configVersion", utils.FormatInt(proposal.ConfigVersion()), "createdAt", utils.FormatInt(proposal.CreatedAt()), ) return proposal.ID() } // ProposeCommunityPoolSpend creates a treasury disbursement proposal. // // Allocates community pool funds for approved purposes. // Supports grants, development funding, and protocol incentives. // The transfer is attempted when the approved proposal executes. // // Parameters: // // - _: Internal call discriminator; pass 0. // // - rlm: Propagated realm context; validated as current before proposal state writes. // // - title: Proposal title describing purpose // // - description: Detailed justification and budget breakdown // // - to: Recipient address for funds // // - tokenPath: Registered token contract path (e.g., "gno.land/r/gnoswap/gns") // // - amount: Strictly positive amount to transfer (in smallest unit) // // Requirements: // - Caller must hold at least the configured ProposalCreationThreshold amount in xGNS // - Valid recipient address // - Registered token path // - Community-pool balance is checked at execution, not proposal creation // // Security: // - Enforces timelock after approval // - Single transfer per proposal // - Tracks all disbursements on-chain // // Returns: // - newProposalId: ID of the newly created community-pool-spend proposal func (gv *governanceV1) ProposeCommunityPoolSpend( _ int, rlm realm, title string, description string, to address, tokenPath string, amount int64, ) (newProposalId int64) { access.AssertIsRlmCurrent(0, rlm) halt.AssertIsNotHaltedGovernance() assertIsValidToken(tokenPath) createdAt := time.Now().Unix() createdHeight := runtime.ChainHeight() prev := rlm.Previous() callerAddress := prev.Address() xgnsBalance := xgns.BalanceOf(callerAddress) config, ok := gv.getCurrentConfig() if !ok { panic(errors.New(errDataNotFound)) } // Clean up inactive user proposals before checking if caller already has an active proposal err := gv.removeInactiveUserProposals(0, rlm, callerAddress, createdAt) if err != nil { panic(err) } // Check if caller already has an active proposal (one proposal per address) if gv.hasActiveProposal(callerAddress) { panic(errors.New(errAlreadyActiveProposal)) } // Get timestamp anchor and total voting weight for proposal creation maxVotingWeight, snapshotTime, err := gv.getVotingWeightSnapshot( createdAt, config.VotingWeightSmoothingDuration, ) if err != nil { panic(err) } quorumWeight := gv.stakerAccessor.GetTotalxGnsSupply() // Create the community pool spend proposal with execution data proposal, err := gv.createProposal( 0, rlm, governance.CommunityPoolSpend, config, maxVotingWeight, quorumWeight, snapshotTime, governance.NewProposalMetadata(title, description), NewProposalCommunityPoolSpendData(tokenPath, to, amount, COMMUNITY_POOL_PATH), callerAddress, xgnsBalance, createdAt, createdHeight, ) if err != nil { panic(err) } // Initialize empty voting info tree for this proposal (votes will be added as users vote) err = gv.updateProposalUserVotes(0, rlm, proposal, governance.NewProposalUserVotingInfoTree()) if err != nil { panic(err) } // Emit proposal creation event for indexing and tracking chain.Emit( "ProposeCommunityPoolSpend", "prevAddr", prev.Address().String(), "prevRealm", prev.PkgPath(), "title", title, "to", to.String(), "tokenPath", tokenPath, "amount", utils.FormatInt(amount), "proposalId", utils.FormatInt(proposal.ID()), "quorumAmount", utils.FormatInt(proposal.VotingQuorumAmount()), "maxVotingWeight", utils.FormatInt(proposal.VotingMaxWeight()), "configVersion", utils.FormatInt(proposal.ConfigVersion()), "createdAt", utils.FormatInt(proposal.CreatedAt()), ) return proposal.ID() } // ProposeParameterChange creates a protocol parameter update proposal. // // Modifies system parameters through governance. // Supports multiple parameter changes in single proposal. // Changes apply atomically on execution. // // Parameters: // // - _: Internal call discriminator; pass 0. // // - rlm: Propagated realm context; validated as current before proposal state writes. // // - title: Clear description of changes // // - description: Rationale and impact analysis // // - numToExecute: Number of parameter changes // // - executions: Raw execution string encoded as messages separated by *GOV*. // Each message is formatted as <pkgPath>*EXE*<function>*EXE*<params>. // // Example execution format (one registered handler; all seven arguments are required): // // gno.land/r/gnoswap/gov/governance*EXE*Reconfigure*EXE*86400,604800,86400,50,1000000000,86400,2592000 // // Requirements: // - Caller must hold at least the configured ProposalCreationThreshold amount in xGNS // - Encoded execution messages must match numToExecute // - Target handlers must exist in the parameter registry // - Parameters must match registered function signatures // // Returns: // - newProposalId: ID of the newly created parameter-change proposal func (gv *governanceV1) ProposeParameterChange( _ int, rlm realm, title string, description string, numToExecute int64, executions string, ) (newProposalId int64) { access.AssertIsRlmCurrent(0, rlm) proposalData := NewProposalExecutionData(numToExecute, executions) assertIsNotHaltedGovernanceForProposalData(proposalData) prev := rlm.Previous() callerAddress := prev.Address() createdAt := time.Now().Unix() createdHeight := runtime.ChainHeight() xgnsBalance := xgns.BalanceOf(callerAddress) config, ok := gv.getCurrentConfig() if !ok { panic(errors.New(errDataNotFound)) } // Clean up inactive user proposals before checking if caller already has an active proposal err := gv.removeInactiveUserProposals(0, rlm, callerAddress, createdAt) if err != nil { panic(err) } // Check if caller already has an active proposal (one proposal per address) if gv.hasActiveProposal(callerAddress) { panic(errors.New(errAlreadyActiveProposal)) } // Get timestamp anchor and total voting weight for proposal creation maxVotingWeight, snapshotTime, err := gv.getVotingWeightSnapshot( createdAt, config.VotingWeightSmoothingDuration, ) if err != nil { panic(err) } quorumWeight := gv.stakerAccessor.GetTotalxGnsSupply() // Create the parameter change proposal with execution data proposal, err := gv.createProposal( 0, rlm, governance.ParameterChange, config, maxVotingWeight, quorumWeight, snapshotTime, governance.NewProposalMetadata(title, description), proposalData, callerAddress, xgnsBalance, createdAt, createdHeight, ) if err != nil { panic(err) } // Initialize empty voting info tree for this proposal (votes will be added as users vote) err = gv.updateProposalUserVotes(0, rlm, proposal, governance.NewProposalUserVotingInfoTree()) if err != nil { panic(err) } // Emit proposal creation event for indexing and tracking chain.Emit( "ProposeParameterChange", "prevAddr", prev.Address().String(), "prevRealm", prev.PkgPath(), "title", title, "numToExecute", utils.FormatInt(numToExecute), "proposalId", utils.FormatInt(proposal.ID()), "quorumAmount", utils.FormatInt(proposal.VotingQuorumAmount()), "maxVotingWeight", utils.FormatInt(proposal.VotingMaxWeight()), "configVersion", utils.FormatInt(proposal.ConfigVersion()), "createdAt", utils.FormatInt(proposal.CreatedAt()), ) return proposal.ID() } // createProposal handles proposal creation logic. // Validates input data, checks proposer eligibility, and creates proposal object. func (gv *governanceV1) createProposal( _ int, rlm realm, proposalType governance.ProposalType, config governance.Config, maxVotingWeight int64, quorumWeight int64, snapshotTime int64, proposalMetadata *governance.ProposalMetadata, proposalData *governance.ProposalData, proposerAddress address, proposerXGnsBalance int64, createdAt int64, createdHeight int64, ) (*governance.Proposal, error) { // Validate proposal metadata (title and description) metadataResolver := NewProposalMetadataResolver(proposalMetadata) err := metadataResolver.Validate() if err != nil { return nil, err } // Validate proposal data (type-specific validation) dataResolver := NewProposalDataResolver(proposalData) err = dataResolver.Validate() if err != nil { return nil, err } // Check if proposer has enough xGNS balance to create proposal if proposerXGnsBalance < config.ProposalCreationThreshold { return nil, errors.New(errNotEnoughBalance) } // Generate unique proposal ID proposalID := gv.nextProposalID(0, rlm) // Create proposal status with voting schedule and requirements proposalStatus := NewProposalStatus( config, maxVotingWeight, proposalType.IsExecutable(), createdAt, quorumWeight, ) // Get current configuration version for tracking configVersion := gv.getCurrentConfigVersion() // Create the proposal object with a timestamp anchor for lazy voting-weight lookup proposal := governance.NewProposal( proposalID, proposalStatus, proposalMetadata, proposalData, proposerAddress, configVersion, snapshotTime, createdHeight, ) // Store the proposal in state success := gv.addProposal(0, rlm, proposal) if !success { return nil, errors.New(errDataNotFound) } return proposal, nil } // getVotingWeightSnapshot retrieves the averaged total voting weight for proposal creation. // It uses two snapshots (current and current - smoothingPeriod) and averages them. func (gv *governanceV1) getVotingWeightSnapshot( current, smoothingPeriod int64, ) (int64, int64, error) { // Calculate the earlier timestamp by subtracting the smoothing duration snapshotTime := current - smoothingPeriod if snapshotTime < 0 { snapshotTime = 0 } // Get total delegation at the earlier timestamp from staker history totalAtSnapshot, ok := gv.stakerAccessor.GetTotalDelegationAmountAtSnapshot(snapshotTime) if !ok { totalAtSnapshot = 0 } totalAtCurrent, ok := gv.stakerAccessor.GetTotalDelegationAmountAtSnapshot(current) if !ok { totalAtCurrent = 0 } totalVotingWeight := gnsmath.SafeAddInt64(totalAtSnapshot, totalAtCurrent) / 2 if totalVotingWeight <= 0 { return 0, snapshotTime, errors.New(errNotEnoughVotingWeight) } return totalVotingWeight, snapshotTime, nil }
- #24governance_vote.gno
Result log
msg:0,success:true,log:,events:[]