diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 7f42de2c..7c310902 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -8,6 +8,8 @@ import ( "github.com/cometbft/cometbft/libs/log" + govkeeper "github.com/atomone-hub/atomone/x/gov/keeper" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" servertypes "github.com/cosmos/cosmos-sdk/server/types" @@ -31,7 +33,6 @@ import ( evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" "github.com/cosmos/cosmos-sdk/x/feegrant" feegrantkeeper "github.com/cosmos/cosmos-sdk/x/feegrant/keeper" - govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" diff --git a/app/modules.go b/app/modules.go index 4731bb47..8768cf2f 100644 --- a/app/modules.go +++ b/app/modules.go @@ -25,7 +25,6 @@ import ( feegrantmodule "github.com/cosmos/cosmos-sdk/x/feegrant/module" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" - "github.com/cosmos/cosmos-sdk/x/gov" govclient "github.com/cosmos/cosmos-sdk/x/gov/client" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/cosmos/cosmos-sdk/x/mint" @@ -42,6 +41,7 @@ import ( upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" atomoneappparams "github.com/atomone-hub/atomone/app/params" + "github.com/atomone-hub/atomone/x/gov" ) var maccPerms = map[string][]string{ diff --git a/go.mod b/go.mod index 7ace0ec5..490dad2b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.21 require ( cosmossdk.io/api v0.3.1 + cosmossdk.io/core v0.5.1 + cosmossdk.io/depinject v1.0.0-alpha.4 cosmossdk.io/errors v1.0.1 cosmossdk.io/math v1.3.0 cosmossdk.io/simapp v0.0.0-20230602123434-616841b9704d @@ -12,7 +14,7 @@ require ( github.com/cometbft/cometbft-db v0.10.0 github.com/cosmos/cosmos-sdk v0.47.10 github.com/cosmos/go-bip39 v1.0.0 - github.com/cosmos/gogoproto v1.4.10 + github.com/golang/mock v1.6.0 github.com/google/gofuzz v1.2.0 github.com/gorilla/mux v1.8.1 github.com/ory/dockertest/v3 v3.10.0 @@ -22,6 +24,8 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 + golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb + gotest.tools/v3 v3.5.1 ) require ( @@ -34,12 +38,10 @@ require ( // github.com/gravity-devs/liquidity v1.6.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/grpc v1.60.1 // indirect + google.golang.org/grpc v1.60.1 ) require ( - cosmossdk.io/core v0.5.1 // indirect - cosmossdk.io/depinject v1.0.0-alpha.4 // indirect cosmossdk.io/log v1.3.1 // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect @@ -70,6 +72,7 @@ require ( github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.4 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect + github.com/cosmos/gogoproto v1.4.10 // indirect github.com/cosmos/iavl v0.20.1 // indirect github.com/cosmos/ledger-cosmos-go v0.12.4 // indirect github.com/cosmos/rosetta-sdk-go v0.10.0 // indirect @@ -101,7 +104,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.1.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -186,7 +188,6 @@ require ( go.opentelemetry.io/otel/metric v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.19.0 // indirect golang.org/x/crypto v0.16.0 // indirect - golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.13.0 // indirect diff --git a/go.sum b/go.sum index 3d244378..d572dae2 100644 --- a/go.sum +++ b/go.sum @@ -1940,7 +1940,6 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= diff --git a/tests/e2e/genesis.go b/tests/e2e/genesis.go index 46b29170..4da11e04 100644 --- a/tests/e2e/genesis.go +++ b/tests/e2e/genesis.go @@ -14,10 +14,8 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" - govmigrv3 "github.com/cosmos/cosmos-sdk/x/gov/migrations/v3" - govmigrv4 "github.com/cosmos/cosmos-sdk/x/gov/migrations/v4" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - govlegacytypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -129,23 +127,16 @@ func modifyGenesis(path, moniker, amountStr string, addrAll []sdk.AccAddress, de maxDepositPeriod := 10 * time.Minute votingPeriod := 15 * time.Second - govStateLegacy := govlegacytypes.NewGenesisState(1, - govlegacytypes.NewDepositParams(sdk.NewCoins(sdk.NewCoin(denom, amnt)), maxDepositPeriod), - govlegacytypes.NewVotingParams(votingPeriod), - govlegacytypes.NewTallyParams(quorum, threshold, govlegacytypes.DefaultVetoThreshold), + govGenState := govv1.NewGenesisState(1, + govv1.NewParams( + sdk.NewCoins(sdk.NewCoin(denom, amnt)), maxDepositPeriod, + votingPeriod, + quorum.String(), threshold.String(), govv1.DefaultVetoThreshold.String(), + sdk.ZeroDec().String(), + false, false, true, + ), ) - - govStateV3, err := govmigrv3.MigrateJSON(govStateLegacy) - if err != nil { - return fmt.Errorf("failed to migrate v1beta1 gov genesis state to v3: %w", err) - } - - govStateV4, err := govmigrv4.MigrateJSON(govStateV3) - if err != nil { - return fmt.Errorf("failed to migrate v1beta1 gov genesis state to v4: %w", err) - } - - govGenStateBz, err := cdc.MarshalJSON(govStateV4) + govGenStateBz, err := cdc.MarshalJSON(govGenState) if err != nil { return fmt.Errorf("failed to marshal gov genesis state: %w", err) } diff --git a/x/gov/README.md b/x/gov/README.md new file mode 100644 index 00000000..dbad79e2 --- /dev/null +++ b/x/gov/README.md @@ -0,0 +1,2597 @@ +--- +sidebar_position: 1 +--- + +# `x/gov` + +## Abstract + +This paper specifies the Governance module of the Cosmos SDK, which was first +described in the [Cosmos Whitepaper](https://cosmos.network/about/whitepaper) in +June 2016. + +The module enables Cosmos SDK based blockchain to support an on-chain governance +system. In this system, holders of the native staking token of the chain can vote +on proposals on a 1 token 1 vote basis. Next is a list of features the module +currently supports: + +* **Proposal submission:** Users can submit proposals with a deposit. Once the +minimum deposit is reached, the proposal enters voting period. The minimum deposit can be reached by collecting deposits from different users (including proposer) within deposit period. +* **Vote:** Participants can vote on proposals that reached MinDeposit and entered voting period. +* **Inheritance and penalties:** Delegators inherit their validator's vote if +they don't vote themselves. +* **Claiming deposit:** Users that deposited on proposals can recover their +deposits if the proposal was accepted or rejected. If the proposal was vetoed, or never entered voting period (minimum deposit not reached within deposit period), the deposit is burned. + +This module is in use in [AtomOne](https://github.com/atomone-hub/atomone)). +Features that may be added in the future are described in [Future Improvements](#future-improvements). + +## Contents + +The following specification uses *ATOM* as the native staking token. The module +can be adapted to any Proof-Of-Stake blockchain by replacing *ATOM* with the native +staking token of the chain. + + +* [Concepts](#concepts) + * [Proposal submission](#proposal-submission) + * [Deposit](#deposit) + * [Vote](#vote) + * [Software Upgrade](#software-upgrade) +* [State](#state) + * [Proposals](#proposals) + * [Parameters and base types](#parameters-and-base-types) + * [Deposit](#deposit-1) + * [ValidatorGovInfo](#validatorgovinfo) + * [Stores](#stores) + * [Proposal Processing Queue](#proposal-processing-queue) + * [Legacy Proposal](#legacy-proposal) +* [Messages](#messages) + * [Proposal Submission](#proposal-submission-1) + * [Deposit](#deposit-2) + * [Vote](#vote-1) +* [Events](#events) + * [EndBlocker](#endblocker) + * [Handlers](#handlers) +* [Parameters](#parameters) +* [Client](#client) + * [CLI](#cli) + * [gRPC](#grpc) + * [REST](#rest) +* [Metadata](#metadata) + * [Proposal](#proposal-3) + * [Vote](#vote-5) +* [Future Improvements](#future-improvements) + +## Concepts + +*Disclaimer: This is work in progress. Mechanisms are susceptible to change.* + +The governance process is divided in a few steps that are outlined below: + +* **Proposal submission:** Proposal is submitted to the blockchain with a + deposit. +* **Vote:** Once deposit reaches a certain value (`MinDeposit`), proposal is + confirmed and vote opens. Bonded Atom holders can then send `TxGovVote` + transactions to vote on the proposal. +* **Execution** After a period of time, the votes are tallied and depending + on the result, the messages in the proposal will be executed. + +### Proposal submission + +#### Right to submit a proposal + +Every account can submit proposals by sending a `MsgSubmitProposal` transaction. +Once a proposal is submitted, it is identified by its unique `proposalID`. + +#### Proposal Messages + +A proposal includes an array of `sdk.Msg`s which are executed automatically if the +proposal passes. The messages are executed by the governance `ModuleAccount` itself. Modules +such as `x/upgrade`, that want to allow certain messages to be executed by governance +only should add a whitelist within the respective msg server, granting the governance +module the right to execute the message once a quorum has been reached. The governance +module uses the `MsgServiceRouter` to check that these messages are correctly constructed +and have a respective path to execute on but do not perform a full validity check. + +### Deposit + +To prevent spam, proposals must be submitted with a deposit in the coins defined by +the `MinDeposit` param. + +When a proposal is submitted, it has to be accompanied with a deposit that must be +strictly positive, but can be inferior to `MinDeposit`. The submitter doesn't need +to pay for the entire deposit on their own. The newly created proposal is stored in +an *inactive proposal queue* and stays there until its deposit passes the `MinDeposit`. +Other token holders can increase the proposal's deposit by sending a `Deposit` +transaction. If a proposal doesn't pass the `MinDeposit` before the deposit end time +(the time when deposits are no longer accepted), the proposal will be destroyed: the +proposal will be removed from state and the deposit will be burned (see x/gov `EndBlocker`). +When a proposal deposit passes the `MinDeposit` threshold (even during the proposal +submission) before the deposit end time, the proposal will be moved into the +*active proposal queue* and the voting period will begin. + +The deposit is kept in escrow and held by the governance `ModuleAccount` until the +proposal is finalized (passed or rejected). + +#### Deposit refund and burn + +When a proposal is finalized, the coins from the deposit are either refunded or burned +according to the final tally of the proposal: + +* If the proposal is approved or rejected but *not* vetoed, each deposit will be + automatically refunded to its respective depositor (transferred from the governance + `ModuleAccount`). +* When the proposal is vetoed with greater than 1/3, deposits will be burned from the + governance `ModuleAccount` and the proposal information along with its deposit + information will be removed from state. +* All refunded or burned deposits are removed from the state. Events are issued when + burning or refunding a deposit. + +### Vote + +#### Participants + +*Participants* are users that have the right to vote on proposals. On the +Cosmos Hub, participants are bonded Atom holders. Unbonded Atom holders and +other users do not get the right to participate in governance. However, they +can submit and deposit on proposals. + +Note that when *participants* have bonded and unbonded Atoms, their voting power is calculated from their bonded Atom holdings only. + +#### Voting period + +Once a proposal reaches `MinDeposit`, it immediately enters `Voting period`. We +define `Voting period` as the interval between the moment the vote opens and +the moment the vote closes. `Voting period` should always be shorter than +`Unbonding period` to prevent double voting. The initial value of +`Voting period` is 2 weeks. + +#### Option set + +The option set of a proposal refers to the set of choices a participant can +choose from when casting its vote. + +The initial option set includes the following options: + +* `Yes` +* `No` +* `NoWithVeto` +* `Abstain` + +`NoWithVeto` counts as `No` but also adds a `Veto` vote. `Abstain` option +allows voters to signal that they do not intend to vote in favor or against the +proposal but accept the result of the vote. + +*Note: from the UI, for urgent proposals we should maybe add a ‘Not Urgent’ option that casts a `NoWithVeto` vote.* + +#### Weighted Votes + +[ADR-037](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-037-gov-split-vote.md) introduces the weighted vote feature which allows a staker to split their votes into several voting options. For example, it could use 70% of its voting power to vote Yes and 30% of its voting power to vote No. + +Often times the entity owning that address might not be a single individual. For example, a company might have different stakeholders who want to vote differently, and so it makes sense to allow them to split their voting power. Currently, it is not possible for them to do "passthrough voting" and giving their users voting rights over their tokens. However, with this system, exchanges can poll their users for voting preferences, and then vote on-chain proportionally to the results of the poll. + +To represent weighted vote on chain, we use the following Protobuf message. + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1beta1/gov.proto#L34-L47 +``` + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1beta1/gov.proto#L181-L201 +``` + +For a weighted vote to be valid, the `options` field must not contain duplicate vote options, and the sum of weights of all options must be equal to 1. + +### Quorum + +Quorum is defined as the minimum percentage of voting power that needs to be +cast on a proposal for the result to be valid. + +#### Threshold + +Threshold is defined as the minimum proportion of `Yes` votes (excluding +`Abstain` votes) for the proposal to be accepted. + +Initially, the threshold is set at 50% of `Yes` votes, excluding `Abstain` +votes. A possibility to veto exists if more than 1/3rd of all votes are +`NoWithVeto` votes. Note, both of these values are derived from the `TallyParams` +on-chain parameter, which is modifiable by governance. +This means that proposals are accepted iff: + +* There exist bonded tokens. +* Quorum has been achieved. +* The proportion of `Abstain` votes is inferior to 1/1. +* The proportion of `NoWithVeto` votes is inferior to 1/3, including + `Abstain` votes. +* The proportion of `Yes` votes, excluding `Abstain` votes, at the end of + the voting period is superior to 1/2. + +#### No inheritance + +If a delegator does not vote, it won't inherit its validator vote. + +Similarly, a validator's voting power is only equal to its own stake. + +#### Validator’s punishment for non-voting + +At present, validators are not punished for failing to vote. + +#### Governance address + +Later, we may add permissioned keys that could only sign txs from certain modules. For the MVP, the `Governance address` will be the main validator address generated at account creation. This address corresponds to a different PrivKey than the CometBFT PrivKey which is responsible for signing consensus messages. Validators thus do not have to sign governance transactions with the sensitive CometBFT PrivKey. + +#### Burnable Params + +There are three parameters that define if the deposit of a proposal should be burned or returned to the depositors. + +* `BurnVoteVeto` burns the proposal deposit if the proposal gets vetoed. +* `BurnVoteQuorum` burns the proposal deposit if the proposal deposit if the vote does not reach quorum. +* `BurnProposalDepositPrevote` burns the proposal deposit if it does not enter the voting phase. + +> Note: These parameters are modifiable via governance. + +## State + +### Proposals + +`Proposal` objects are used to tally votes and generally track the proposal's state. +They contain an array of arbitrary `sdk.Msg`'s which the governance module will attempt +to resolve and then execute if the proposal passes. `Proposal`'s are identified by a +unique id and contains a series of timestamps: `submit_time`, `deposit_end_time`, +`voting_start_time`, `voting_end_time` which track the lifecycle of a proposal + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L51-L99 +``` + +A proposal will generally require more than just a set of messages to explain its +purpose but need some greater justification and allow a means for interested participants +to discuss and debate the proposal. +In most cases, **it is encouraged to have an off-chain system that supports the on-chain governance process**. +To accommodate for this, a proposal contains a special **`metadata`** field, a string, +which can be used to add context to the proposal. The `metadata` field allows custom use for networks, +however, it is expected that the field contains a URL or some form of CID using a system such as +[IPFS](https://docs.ipfs.io/concepts/content-addressing/). To support the case of +interoperability across networks, the SDK recommends that the `metadata` represents +the following `JSON` template: + +```json +{ + "title": "...", + "description": "...", + "forum": "...", // a link to the discussion platform (i.e. Discord) + "other": "..." // any extra data that doesn't correspond to the other fields +} +``` + +This makes it far easier for clients to support multiple networks. + +The metadata has a maximum length that is chosen by the app developer, and +passed into the gov keeper as a config. The default maximum length in the SDK is 255 characters. + +#### Writing a module that uses governance + +There are many aspects of a chain, or of the individual modules that you may want to +use governance to perform such as changing various parameters. This is very simple +to do. First, write out your message types and `MsgServer` implementation. Add an +`authority` field to the keeper which will be populated in the constructor with the +governance module account: `govKeeper.GetGovernanceAccount().GetAddress()`. Then for +the methods in the `msg_server.go`, perform a check on the message that the signer +matches `authority`. This will prevent any user from executing that message. + +### Parameters and base types + +`Parameters` define the rules according to which votes are run. There can only +be one active parameter set at any given time. If governance wants to change a +parameter set, either to modify a value or add/remove a parameter field, a new +parameter set has to be created and the previous one rendered inactive. + +#### DepositParams + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L152-L162 +``` + +#### VotingParams + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L164-L168 +``` + +#### TallyParams + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L170-L182 +``` + +Parameters are stored in a global `GlobalParams` KVStore. + +Additionally, we introduce some basic types: + +```go +type Vote byte + +const ( + VoteYes = 0x1 + VoteNo = 0x2 + VoteNoWithVeto = 0x3 + VoteAbstain = 0x4 +) + +type ProposalType string + +const ( + ProposalTypePlainText = "Text" + ProposalTypeSoftwareUpgrade = "SoftwareUpgrade" +) + +type ProposalStatus byte + + +const ( + StatusNil ProposalStatus = 0x00 + StatusDepositPeriod ProposalStatus = 0x01 // Proposal is submitted. Participants can deposit on it but not vote + StatusVotingPeriod ProposalStatus = 0x02 // MinDeposit is reached, participants can vote + StatusPassed ProposalStatus = 0x03 // Proposal passed and successfully executed + StatusRejected ProposalStatus = 0x04 // Proposal has been rejected + StatusFailed ProposalStatus = 0x05 // Proposal passed but failed execution +) +``` + +### Deposit + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L38-L49 +``` + +### ValidatorGovInfo + +This type is used in a temp map when tallying + +```go + type ValidatorGovInfo struct { + Minus sdk.Dec + Vote Vote + } +``` + +## Stores + +:::note +Stores are KVStores in the multi-store. The key to find the store is the first parameter in the list +::: + +We will use one KVStore `Governance` to store four mappings: + +* A mapping from `proposalID|'proposal'` to `Proposal`. +* A mapping from `proposalID|'addresses'|address` to `Vote`. This mapping allows + us to query all addresses that voted on the proposal along with their vote by + doing a range query on `proposalID:addresses`. +* A mapping from `ParamsKey|'Params'` to `Params`. This map allows to query all + x/gov params. +* A mapping from `VotingPeriodProposalKeyPrefix|proposalID` to a single byte. This allows + us to know if a proposal is in the voting period or not with very low gas cost. + +For pseudocode purposes, here are the two function we will use to read or write in stores: + +* `load(StoreKey, Key)`: Retrieve item stored at key `Key` in store found at key `StoreKey` in the multistore +* `store(StoreKey, Key, value)`: Write value `Value` at key `Key` in store found at key `StoreKey` in the multistore + +### Proposal Processing Queue + +**Store:** + +* `ProposalProcessingQueue`: A queue `queue[proposalID]` containing all the + `ProposalIDs` of proposals that reached `MinDeposit`. During each `EndBlock`, + all the proposals that have reached the end of their voting period are processed. + To process a finished proposal, the application tallies the votes, computes the + votes of each validator and checks if every validator in the validator set has + voted. If the proposal is accepted, deposits are refunded. Finally, the proposal + content `Handler` is executed. + +And the pseudocode for the `ProposalProcessingQueue`: + +```go + in EndBlock do + + for finishedProposalID in GetAllFinishedProposalIDs(block.Time) + proposal = load(Governance, ) // proposal is a const key + + validators = Keeper.getAllValidators() + tmpValMap := map(sdk.AccAddress)ValidatorGovInfo + + // Initiate mapping at 0. This is the amount of shares of the validator's vote that will be overridden by their delegator's votes + for each validator in validators + tmpValMap(validator.OperatorAddr).Minus = 0 + + // Tally + voterIterator = rangeQuery(Governance, ) //return all the addresses that voted on the proposal + for each (voterAddress, vote) in voterIterator + delegations = stakingKeeper.getDelegations(voterAddress) // get all delegations for current voter + + for each delegation in delegations + // make sure delegation.Shares does NOT include shares being unbonded + tmpValMap(delegation.ValidatorAddr).Minus += delegation.Shares + proposal.updateTally(vote, delegation.Shares) + + _, isVal = stakingKeeper.getValidator(voterAddress) + if (isVal) + tmpValMap(voterAddress).Vote = vote + + tallyingParam = load(GlobalParams, 'TallyingParam') + + // Update tally if validator voted + for each validator in validators + if tmpValMap(validator).HasVoted + proposal.updateTally(tmpValMap(validator).Vote, (validator.TotalShares - tmpValMap(validator).Minus)) + + + + // Check if proposal is accepted or rejected + totalNonAbstain := proposal.YesVotes + proposal.NoVotes + proposal.NoWithVetoVotes + if (proposal.Votes.YesVotes/totalNonAbstain > tallyingParam.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < tallyingParam.Veto) + // proposal was accepted at the end of the voting period + // refund deposits (non-voters already punished) + for each (amount, depositor) in proposal.Deposits + depositor.AtomBalance += amount + + stateWriter, err := proposal.Handler() + if err != nil + // proposal passed but failed during state execution + proposal.CurrentStatus = ProposalStatusFailed + else + // proposal pass and state is persisted + proposal.CurrentStatus = ProposalStatusAccepted + stateWriter.save() + else + // proposal was rejected + proposal.CurrentStatus = ProposalStatusRejected + + store(Governance, , proposal) +``` + +### Legacy Proposal + +A legacy proposal is the old implementation of governance proposal. +Contrary to proposal that can contain any messages, a legacy proposal allows to submit a set of pre-defined proposals. +These proposal are defined by their types. + +While proposals should use the new implementation of the governance proposal, we need still to use legacy proposal in order to submit a `software-upgrade` and a `cancel-software-upgrade` proposal. + +More information on how to submit proposals in the [client section](#client). + +## Messages + +### Proposal Submission + +Proposals can be submitted by any account via a `MsgSubmitProposal` transaction. + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L42-L69 +``` + +All `sdk.Msgs` passed into the `messages` field of a `MsgSubmitProposal` message +must be registered in the app's `MsgServiceRouter`. Each of these messages must +have one signer, namely the gov module account. And finally, the metadata length +must not be larger than the `maxMetadataLen` config passed into the gov keeper. + +**State modifications:** + +* Generate new `proposalID` +* Create new `Proposal` +* Initialise `Proposal`'s attributes +* Decrease balance of sender by `InitialDeposit` +* If `MinDeposit` is reached: + * Push `proposalID` in `ProposalProcessingQueue` +* Transfer `InitialDeposit` from the `Proposer` to the governance `ModuleAccount` + +A `MsgSubmitProposal` transaction can be handled according to the following +pseudocode. + +```go +// PSEUDOCODE // +// Check if MsgSubmitProposal is valid. If it is, create proposal // + +upon receiving txGovSubmitProposal from sender do + + if !correctlyFormatted(txGovSubmitProposal) + // check if proposal is correctly formatted and the messages have routes to other modules. Includes fee payment. + // check if all messages' unique Signer is the gov acct. + // check if the metadata is not too long. + throw + + initialDeposit = txGovSubmitProposal.InitialDeposit + if (initialDeposit.Atoms <= 0) OR (sender.AtomBalance < initialDeposit.Atoms) + // InitialDeposit is negative or null OR sender has insufficient funds + throw + + if (txGovSubmitProposal.Type != ProposalTypePlainText) OR (txGovSubmitProposal.Type != ProposalTypeSoftwareUpgrade) + + sender.AtomBalance -= initialDeposit.Atoms + + depositParam = load(GlobalParams, 'DepositParam') + + proposalID = generate new proposalID + proposal = NewProposal() + + proposal.Messages = txGovSubmitProposal.Messages + proposal.Metadata = txGovSubmitProposal.Metadata + proposal.TotalDeposit = initialDeposit + proposal.SubmitTime = + proposal.DepositEndTime = .Add(depositParam.MaxDepositPeriod) + proposal.Deposits.append({initialDeposit, sender}) + proposal.Submitter = sender + proposal.YesVotes = 0 + proposal.NoVotes = 0 + proposal.NoWithVetoVotes = 0 + proposal.AbstainVotes = 0 + proposal.CurrentStatus = ProposalStatusOpen + + store(Proposals, , proposal) // Store proposal in Proposals mapping + return proposalID +``` + +### Deposit + +Once a proposal is submitted, if +`Proposal.TotalDeposit < ActiveParam.MinDeposit`, Atom holders can send +`MsgDeposit` transactions to increase the proposal's deposit. + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L134-L147 +``` + +**State modifications:** + +* Decrease balance of sender by `deposit` +* Add `deposit` of sender in `proposal.Deposits` +* Increase `proposal.TotalDeposit` by sender's `deposit` +* If `MinDeposit` is reached: + * Push `proposalID` in `ProposalProcessingQueueEnd` +* Transfer `Deposit` from the `proposer` to the governance `ModuleAccount` + +A `MsgDeposit` transaction has to go through a number of checks to be valid. +These checks are outlined in the following pseudocode. + +```go +// PSEUDOCODE // +// Check if MsgDeposit is valid. If it is, increase deposit and check if MinDeposit is reached + +upon receiving txGovDeposit from sender do + // check if proposal is correctly formatted. Includes fee payment. + + if !correctlyFormatted(txGovDeposit) + throw + + proposal = load(Proposals, ) // proposal is a const key, proposalID is variable + + if (proposal == nil) + // There is no proposal for this proposalID + throw + + if (txGovDeposit.Deposit.Atoms <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit.Atoms) OR (proposal.CurrentStatus != ProposalStatusOpen) + + // deposit is negative or null + // OR sender has insufficient funds + // OR proposal is not open for deposit anymore + + throw + + depositParam = load(GlobalParams, 'DepositParam') + + if (CurrentBlock >= proposal.SubmitBlock + depositParam.MaxDepositPeriod) + proposal.CurrentStatus = ProposalStatusClosed + + else + // sender can deposit + sender.AtomBalance -= txGovDeposit.Deposit.Atoms + + proposal.Deposits.append({txGovVote.Deposit, sender}) + proposal.TotalDeposit.Plus(txGovDeposit.Deposit) + + if (proposal.TotalDeposit >= depositParam.MinDeposit) + // MinDeposit is reached, vote opens + + proposal.VotingStartBlock = CurrentBlock + proposal.CurrentStatus = ProposalStatusActive + ProposalProcessingQueue.push(txGovDeposit.ProposalID) + + store(Proposals, , proposal) +``` + +### Vote + +Once `ActiveParam.MinDeposit` is reached, voting period starts. From there, +bonded Atom holders are able to send `MsgVote` transactions to cast their +vote on the proposal. + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/tx.proto#L92-L108 +``` + +**State modifications:** + +* Record `Vote` of sender + +:::note +Gas cost for this message has to take into account the future tallying of the vote in EndBlocker. +::: + +Next is a pseudocode outline of the way `MsgVote` transactions are handled: + +```go + // PSEUDOCODE // + // Check if MsgVote is valid. If it is, count vote// + + upon receiving txGovVote from sender do + // check if proposal is correctly formatted. Includes fee payment. + + if !correctlyFormatted(txGovDeposit) + throw + + proposal = load(Proposals, ) + + if (proposal == nil) + // There is no proposal for this proposalID + throw + + + if (proposal.CurrentStatus == ProposalStatusActive) + + + // Sender can vote if + // Proposal is active + // Sender has some bonds + + store(Governance, , txGovVote.Vote) // Voters can vote multiple times. Re-voting overrides previous vote. This is ok because tallying is done once at the end. +``` + +## Events + +The governance module emits the following events: + +### EndBlocker + +| Type | Attribute Key | Attribute Value | +|-------------------|-----------------|------------------| +| inactive_proposal | proposal_id | {proposalID} | +| inactive_proposal | proposal_result | {proposalResult} | +| active_proposal | proposal_id | {proposalID} | +| active_proposal | proposal_result | {proposalResult} | + +### Handlers + +#### MsgSubmitProposal + +| Type | Attribute Key | Attribute Value | +|---------------------|---------------------|-----------------| +| submit_proposal | proposal_id | {proposalID} | +| submit_proposal [0] | voting_period_start | {proposalID} | +| proposal_deposit | amount | {depositAmount} | +| proposal_deposit | proposal_id | {proposalID} | +| message | module | governance | +| message | action | submit_proposal | +| message | sender | {senderAddress} | + +* [0] Event only emitted if the voting period starts during the submission. + +#### MsgVote + +| Type | Attribute Key | Attribute Value | +|---------------|---------------|-----------------| +| proposal_vote | option | {voteOption} | +| proposal_vote | proposal_id | {proposalID} | +| message | module | governance | +| message | action | vote | +| message | sender | {senderAddress} | + +#### MsgVoteWeighted + +| Type | Attribute Key | Attribute Value | +| ------------- | ------------- | ------------------------ | +| proposal_vote | option | {weightedVoteOptions} | +| proposal_vote | proposal_id | {proposalID} | +| message | module | governance | +| message | action | vote | +| message | sender | {senderAddress} | + +#### MsgDeposit + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------------|-----------------| +| proposal_deposit | amount | {depositAmount} | +| proposal_deposit | proposal_id | {proposalID} | +| proposal_deposit [0] | voting_period_start | {proposalID} | +| message | module | governance | +| message | action | deposit | +| message | sender | {senderAddress} | + +* [0] Event only emitted if the voting period starts during the submission. + +## Parameters + +The governance module contains the following parameters: + +| Key | Type | Example | +|-------------------------------|------------------|-----------------------------------------| +| min_deposit | array (coins) | [{"denom":"uatom","amount":"10000000"}] | +| max_deposit_period | string (time ns) | "172800000000000" (17280s) | +| voting_period | string (time ns) | "172800000000000" (17280s) | +| quorum | string (dec) | "0.334000000000000000" | +| threshold | string (dec) | "0.500000000000000000" | +| veto | string (dec) | "0.334000000000000000" | +| burn_proposal_deposit_prevote | bool | false | +| burn_vote_quorum | bool | false | +| burn_vote_veto | bool | true | + +**NOTE**: The governance module contains parameters that are objects unlike other +modules. If only a subset of parameters are desired to be changed, only they need +to be included and not the entire parameter object structure. + +## Client + +### CLI + +A user can query and interact with the `gov` module using the CLI. + +#### Query + +The `query` commands allow users to query `gov` state. + +```bash +simd query gov --help +``` + +##### deposit + +The `deposit` command allows users to query a deposit for a given proposal from a given depositor. + +```bash +simd query gov deposit [proposal-id] [depositer-addr] [flags] +``` + +Example: + +```bash +simd query gov deposit 1 cosmos1.. +``` + +Example Output: + +```bash +amount: +- amount: "100" + denom: stake +depositor: cosmos1.. +proposal_id: "1" +``` + +##### deposits + +The `deposits` command allows users to query all deposits for a given proposal. + +```bash +simd query gov deposits [proposal-id] [flags] +``` + +Example: + +```bash +simd query gov deposits 1 +``` + +Example Output: + +```bash +deposits: +- amount: + - amount: "100" + denom: stake + depositor: cosmos1.. + proposal_id: "1" +pagination: + next_key: null + total: "0" +``` + +##### param + +The `param` command allows users to query a given parameter for the `gov` module. + +```bash +simd query gov param [param-type] [flags] +``` + +Example: + +```bash +simd query gov param voting +``` + +Example Output: + +```bash +voting_period: "172800000000000" +``` + +##### params + +The `params` command allows users to query all parameters for the `gov` module. + +```bash +simd query gov params [flags] +``` + +Example: + +```bash +simd query gov params +``` + +Example Output: + +```bash +deposit_params: + max_deposit_period: "172800000000000" + min_deposit: + - amount: "10000000" + denom: stake +tally_params: + quorum: "0.334000000000000000" + threshold: "0.500000000000000000" + veto_threshold: "0.334000000000000000" +voting_params: + voting_period: "172800000000000" +``` + +##### proposal + +The `proposal` command allows users to query a given proposal. + +```bash +simd query gov proposal [proposal-id] [flags] +``` + +Example: + +```bash +simd query gov proposal 1 +``` + +Example Output: + +```bash +deposit_end_time: "2022-03-30T11:50:20.819676256Z" +final_tally_result: + abstain_count: "0" + no_count: "0" + no_with_veto_count: "0" + yes_count: "0" +id: "1" +messages: +- '@type': /cosmos.bank.v1beta1.MsgSend + amount: + - amount: "10" + denom: stake + from_address: cosmos1.. + to_address: cosmos1.. +metadata: AQ== +status: PROPOSAL_STATUS_DEPOSIT_PERIOD +submit_time: "2022-03-28T11:50:20.819676256Z" +total_deposit: +- amount: "10" + denom: stake +voting_end_time: null +voting_start_time: null +``` + +##### proposals + +The `proposals` command allows users to query all proposals with optional filters. + +```bash +simd query gov proposals [flags] +``` + +Example: + +```bash +simd query gov proposals +``` + +Example Output: + +```bash +pagination: + next_key: null + total: "0" +proposals: +- deposit_end_time: "2022-03-30T11:50:20.819676256Z" + final_tally_result: + abstain_count: "0" + no_count: "0" + no_with_veto_count: "0" + yes_count: "0" + id: "1" + messages: + - '@type': /cosmos.bank.v1beta1.MsgSend + amount: + - amount: "10" + denom: stake + from_address: cosmos1.. + to_address: cosmos1.. + metadata: AQ== + status: PROPOSAL_STATUS_DEPOSIT_PERIOD + submit_time: "2022-03-28T11:50:20.819676256Z" + total_deposit: + - amount: "10" + denom: stake + voting_end_time: null + voting_start_time: null +- deposit_end_time: "2022-03-30T14:02:41.165025015Z" + final_tally_result: + abstain_count: "0" + no_count: "0" + no_with_veto_count: "0" + yes_count: "0" + id: "2" + messages: + - '@type': /cosmos.bank.v1beta1.MsgSend + amount: + - amount: "10" + denom: stake + from_address: cosmos1.. + to_address: cosmos1.. + metadata: AQ== + status: PROPOSAL_STATUS_DEPOSIT_PERIOD + submit_time: "2022-03-28T14:02:41.165025015Z" + total_deposit: + - amount: "10" + denom: stake + voting_end_time: null + voting_start_time: null +``` + +##### proposer + +The `proposer` command allows users to query the proposer for a given proposal. + +```bash +simd query gov proposer [proposal-id] [flags] +``` + +Example: + +```bash +simd query gov proposer 1 +``` + +Example Output: + +```bash +proposal_id: "1" +proposer: cosmos1.. +``` + +##### tally + +The `tally` command allows users to query the tally of a given proposal vote. + +```bash +simd query gov tally [proposal-id] [flags] +``` + +Example: + +```bash +simd query gov tally 1 +``` + +Example Output: + +```bash +abstain: "0" +"no": "0" +no_with_veto: "0" +"yes": "1" +``` + +##### vote + +The `vote` command allows users to query a vote for a given proposal. + +```bash +simd query gov vote [proposal-id] [voter-addr] [flags] +``` + +Example: + +```bash +simd query gov vote 1 cosmos1.. +``` + +Example Output: + +```bash +option: VOTE_OPTION_YES +options: +- option: VOTE_OPTION_YES + weight: "1.000000000000000000" +proposal_id: "1" +voter: cosmos1.. +``` + +##### votes + +The `votes` command allows users to query all votes for a given proposal. + +```bash +simd query gov votes [proposal-id] [flags] +``` + +Example: + +```bash +simd query gov votes 1 +``` + +Example Output: + +```bash +pagination: + next_key: null + total: "0" +votes: +- option: VOTE_OPTION_YES + options: + - option: VOTE_OPTION_YES + weight: "1.000000000000000000" + proposal_id: "1" + voter: cosmos1.. +``` + +#### Transactions + +The `tx` commands allow users to interact with the `gov` module. + +```bash +simd tx gov --help +``` + +##### deposit + +The `deposit` command allows users to deposit tokens for a given proposal. + +```bash +simd tx gov deposit [proposal-id] [deposit] [flags] +``` + +Example: + +```bash +simd tx gov deposit 1 10000000stake --from cosmos1.. +``` + +##### draft-proposal + +The `draft-proposal` command allows users to draft any type of proposal. +The command returns a `draft_proposal.json`, to be used by `submit-proposal` after being completed. +The `draft_metadata.json` is meant to be uploaded to [IPFS](#metadata). + +```bash +simd tx gov draft-proposal +``` + +##### submit-proposal + +The `submit-proposal` command allows users to submit a governance proposal along with some messages and metadata. +Messages, metadata and deposit are defined in a JSON file. + +```bash +simd tx gov submit-proposal [path-to-proposal-json] [flags] +``` + +Example: + +```bash +simd tx gov submit-proposal /path/to/proposal.json --from cosmos1.. +``` + +where `proposal.json` contains: + +```json +{ + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "cosmos1...", // The gov module module address + "to_address": "cosmos1...", + "amount":[{"denom": "stake","amount": "10"}] + } + ], + "metadata": "AQ==", + "deposit": "10stake", + "title": "Proposal Title", + "summary": "Proposal Summary" +} +``` + +:::note +By default the metadata, summary and title are both limited by 255 characters, this can be overridden by the application developer. +::: + +##### submit-legacy-proposal + +The `submit-legacy-proposal` command allows users to submit a governance legacy proposal along with an initial deposit. + +```bash +simd tx gov submit-legacy-proposal [command] [flags] +``` + +Example: + +```bash +simd tx gov submit-legacy-proposal --title="Test Proposal" --description="testing" --type="Text" --deposit="100000000stake" --from cosmos1.. +``` + +Example (`cancel-software-upgrade`): + +```bash +simd tx gov submit-legacy-proposal cancel-software-upgrade --title="Test Proposal" --description="testing" --deposit="100000000stake" --from cosmos1.. +``` + +Example (`param-change`): + +```bash +simd tx gov submit-legacy-proposal param-change proposal.json --from cosmos1.. +``` + +```json +{ + "title": "Test Proposal", + "description": "testing, testing, 1, 2, 3", + "changes": [ + { + "subspace": "staking", + "key": "MaxValidators", + "value": 100 + } + ], + "deposit": "10000000stake" +} +``` + +Example (`software-upgrade`): + +```bash +simd tx gov submit-legacy-proposal software-upgrade v2 --title="Test Proposal" --description="testing, testing, 1, 2, 3" --upgrade-height 1000000 --from cosmos1.. +``` + +##### vote + +The `vote` command allows users to submit a vote for a given governance proposal. + +```bash +simd tx gov vote [command] [flags] +``` + +Example: + +```bash +simd tx gov vote 1 yes --from cosmos1.. +``` + +##### weighted-vote + +The `weighted-vote` command allows users to submit a weighted vote for a given governance proposal. + +```bash +simd tx gov weighted-vote [proposal-id] [weighted-options] [flags] +``` + +Example: + +```bash +simd tx gov weighted-vote 1 yes=0.5,no=0.5 --from cosmos1.. +``` + +### gRPC + +A user can query the `gov` module using gRPC endpoints. + +#### Proposal + +The `Proposal` endpoint allows users to query a given proposal. + +Using legacy v1beta1: + +```bash +cosmos.gov.v1beta1.Query/Proposal +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1"}' \ + localhost:9090 \ + cosmos.gov.v1beta1.Query/Proposal +``` + +Example Output: + +```bash +{ + "proposal": { + "proposalId": "1", + "content": {"@type":"/cosmos.gov.v1beta1.TextProposal","description":"testing, testing, 1, 2, 3","title":"Test Proposal"}, + "status": "PROPOSAL_STATUS_VOTING_PERIOD", + "finalTallyResult": { + "yes": "0", + "abstain": "0", + "no": "0", + "noWithVeto": "0" + }, + "submitTime": "2021-09-16T19:40:08.712440474Z", + "depositEndTime": "2021-09-18T19:40:08.712440474Z", + "totalDeposit": [ + { + "denom": "stake", + "amount": "10000000" + } + ], + "votingStartTime": "2021-09-16T19:40:08.712440474Z", + "votingEndTime": "2021-09-18T19:40:08.712440474Z", + "title": "Test Proposal", + "summary": "testing, testing, 1, 2, 3" + } +} +``` + +Using v1: + +```bash +cosmos.gov.v1.Query/Proposal +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1"}' \ + localhost:9090 \ + cosmos.gov.v1.Query/Proposal +``` + +Example Output: + +```bash +{ + "proposal": { + "id": "1", + "messages": [ + {"@type":"/cosmos.bank.v1beta1.MsgSend","amount":[{"denom":"stake","amount":"10"}],"fromAddress":"cosmos1..","toAddress":"cosmos1.."} + ], + "status": "PROPOSAL_STATUS_VOTING_PERIOD", + "finalTallyResult": { + "yesCount": "0", + "abstainCount": "0", + "noCount": "0", + "noWithVetoCount": "0" + }, + "submitTime": "2022-03-28T11:50:20.819676256Z", + "depositEndTime": "2022-03-30T11:50:20.819676256Z", + "totalDeposit": [ + { + "denom": "stake", + "amount": "10000000" + } + ], + "votingStartTime": "2022-03-28T14:25:26.644857113Z", + "votingEndTime": "2022-03-30T14:25:26.644857113Z", + "metadata": "AQ==", + "title": "Test Proposal", + "summary": "testing, testing, 1, 2, 3" + } +} +``` + + +#### Proposals + +The `Proposals` endpoint allows users to query all proposals with optional filters. + +Using legacy v1beta1: + +```bash +cosmos.gov.v1beta1.Query/Proposals +``` + +Example: + +```bash +grpcurl -plaintext \ + localhost:9090 \ + cosmos.gov.v1beta1.Query/Proposals +``` + +Example Output: + +```bash +{ + "proposals": [ + { + "proposalId": "1", + "status": "PROPOSAL_STATUS_VOTING_PERIOD", + "finalTallyResult": { + "yes": "0", + "abstain": "0", + "no": "0", + "noWithVeto": "0" + }, + "submitTime": "2022-03-28T11:50:20.819676256Z", + "depositEndTime": "2022-03-30T11:50:20.819676256Z", + "totalDeposit": [ + { + "denom": "stake", + "amount": "10000000010" + } + ], + "votingStartTime": "2022-03-28T14:25:26.644857113Z", + "votingEndTime": "2022-03-30T14:25:26.644857113Z" + }, + { + "proposalId": "2", + "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", + "finalTallyResult": { + "yes": "0", + "abstain": "0", + "no": "0", + "noWithVeto": "0" + }, + "submitTime": "2022-03-28T14:02:41.165025015Z", + "depositEndTime": "2022-03-30T14:02:41.165025015Z", + "totalDeposit": [ + { + "denom": "stake", + "amount": "10" + } + ], + "votingStartTime": "0001-01-01T00:00:00Z", + "votingEndTime": "0001-01-01T00:00:00Z" + } + ], + "pagination": { + "total": "2" + } +} + +``` + +Using v1: + +```bash +cosmos.gov.v1.Query/Proposals +``` + +Example: + +```bash +grpcurl -plaintext \ + localhost:9090 \ + cosmos.gov.v1.Query/Proposals +``` + +Example Output: + +```bash +{ + "proposals": [ + { + "id": "1", + "messages": [ + {"@type":"/cosmos.bank.v1beta1.MsgSend","amount":[{"denom":"stake","amount":"10"}],"fromAddress":"cosmos1..","toAddress":"cosmos1.."} + ], + "status": "PROPOSAL_STATUS_VOTING_PERIOD", + "finalTallyResult": { + "yesCount": "0", + "abstainCount": "0", + "noCount": "0", + "noWithVetoCount": "0" + }, + "submitTime": "2022-03-28T11:50:20.819676256Z", + "depositEndTime": "2022-03-30T11:50:20.819676256Z", + "totalDeposit": [ + { + "denom": "stake", + "amount": "10000000010" + } + ], + "votingStartTime": "2022-03-28T14:25:26.644857113Z", + "votingEndTime": "2022-03-30T14:25:26.644857113Z", + "metadata": "AQ==", + "title": "Proposal Title", + "summary": "Proposal Summary" + }, + { + "id": "2", + "messages": [ + {"@type":"/cosmos.bank.v1beta1.MsgSend","amount":[{"denom":"stake","amount":"10"}],"fromAddress":"cosmos1..","toAddress":"cosmos1.."} + ], + "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", + "finalTallyResult": { + "yesCount": "0", + "abstainCount": "0", + "noCount": "0", + "noWithVetoCount": "0" + }, + "submitTime": "2022-03-28T14:02:41.165025015Z", + "depositEndTime": "2022-03-30T14:02:41.165025015Z", + "totalDeposit": [ + { + "denom": "stake", + "amount": "10" + } + ], + "metadata": "AQ==", + "title": "Proposal Title", + "summary": "Proposal Summary" + } + ], + "pagination": { + "total": "2" + } +} +``` + +#### Vote + +The `Vote` endpoint allows users to query a vote for a given proposal. + +Using legacy v1beta1: + +```bash +cosmos.gov.v1beta1.Query/Vote +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1","voter":"cosmos1.."}' \ + localhost:9090 \ + cosmos.gov.v1beta1.Query/Vote +``` + +Example Output: + +```bash +{ + "vote": { + "proposalId": "1", + "voter": "cosmos1..", + "option": "VOTE_OPTION_YES", + "options": [ + { + "option": "VOTE_OPTION_YES", + "weight": "1000000000000000000" + } + ] + } +} +``` + +Using v1: + +```bash +cosmos.gov.v1.Query/Vote +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1","voter":"cosmos1.."}' \ + localhost:9090 \ + cosmos.gov.v1.Query/Vote +``` + +Example Output: + +```bash +{ + "vote": { + "proposalId": "1", + "voter": "cosmos1..", + "option": "VOTE_OPTION_YES", + "options": [ + { + "option": "VOTE_OPTION_YES", + "weight": "1.000000000000000000" + } + ] + } +} +``` + +#### Votes + +The `Votes` endpoint allows users to query all votes for a given proposal. + +Using legacy v1beta1: + +```bash +cosmos.gov.v1beta1.Query/Votes +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1"}' \ + localhost:9090 \ + cosmos.gov.v1beta1.Query/Votes +``` + +Example Output: + +```bash +{ + "votes": [ + { + "proposalId": "1", + "voter": "cosmos1..", + "options": [ + { + "option": "VOTE_OPTION_YES", + "weight": "1000000000000000000" + } + ] + } + ], + "pagination": { + "total": "1" + } +} +``` + +Using v1: + +```bash +cosmos.gov.v1.Query/Votes +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1"}' \ + localhost:9090 \ + cosmos.gov.v1.Query/Votes +``` + +Example Output: + +```bash +{ + "votes": [ + { + "proposalId": "1", + "voter": "cosmos1..", + "options": [ + { + "option": "VOTE_OPTION_YES", + "weight": "1.000000000000000000" + } + ] + } + ], + "pagination": { + "total": "1" + } +} +``` + +#### Params + +The `Params` endpoint allows users to query all parameters for the `gov` module. + + + +Using legacy v1beta1: + +```bash +cosmos.gov.v1beta1.Query/Params +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"params_type":"voting"}' \ + localhost:9090 \ + cosmos.gov.v1beta1.Query/Params +``` + +Example Output: + +```bash +{ + "votingParams": { + "votingPeriod": "172800s" + }, + "depositParams": { + "maxDepositPeriod": "0s" + }, + "tallyParams": { + "quorum": "MA==", + "threshold": "MA==", + "vetoThreshold": "MA==" + } +} +``` + +Using v1: + +```bash +cosmos.gov.v1.Query/Params +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"params_type":"voting"}' \ + localhost:9090 \ + cosmos.gov.v1.Query/Params +``` + +Example Output: + +```bash +{ + "votingParams": { + "votingPeriod": "172800s" + } +} +``` + +#### Deposit + +The `Deposit` endpoint allows users to query a deposit for a given proposal from a given depositor. + +Using legacy v1beta1: + +```bash +cosmos.gov.v1beta1.Query/Deposit +``` + +Example: + +```bash +grpcurl -plaintext \ + '{"proposal_id":"1","depositor":"cosmos1.."}' \ + localhost:9090 \ + cosmos.gov.v1beta1.Query/Deposit +``` + +Example Output: + +```bash +{ + "deposit": { + "proposalId": "1", + "depositor": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10000000" + } + ] + } +} +``` + +Using v1: + +```bash +cosmos.gov.v1.Query/Deposit +``` + +Example: + +```bash +grpcurl -plaintext \ + '{"proposal_id":"1","depositor":"cosmos1.."}' \ + localhost:9090 \ + cosmos.gov.v1.Query/Deposit +``` + +Example Output: + +```bash +{ + "deposit": { + "proposalId": "1", + "depositor": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10000000" + } + ] + } +} +``` + +#### deposits + +The `Deposits` endpoint allows users to query all deposits for a given proposal. + +Using legacy v1beta1: + +```bash +cosmos.gov.v1beta1.Query/Deposits +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1"}' \ + localhost:9090 \ + cosmos.gov.v1beta1.Query/Deposits +``` + +Example Output: + +```bash +{ + "deposits": [ + { + "proposalId": "1", + "depositor": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10000000" + } + ] + } + ], + "pagination": { + "total": "1" + } +} +``` + +Using v1: + +```bash +cosmos.gov.v1.Query/Deposits +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1"}' \ + localhost:9090 \ + cosmos.gov.v1.Query/Deposits +``` + +Example Output: + +```bash +{ + "deposits": [ + { + "proposalId": "1", + "depositor": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10000000" + } + ] + } + ], + "pagination": { + "total": "1" + } +} +``` + +#### TallyResult + +The `TallyResult` endpoint allows users to query the tally of a given proposal. + +Using legacy v1beta1: + +```bash +cosmos.gov.v1beta1.Query/TallyResult +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1"}' \ + localhost:9090 \ + cosmos.gov.v1beta1.Query/TallyResult +``` + +Example Output: + +```bash +{ + "tally": { + "yes": "1000000", + "abstain": "0", + "no": "0", + "noWithVeto": "0" + } +} +``` + +Using v1: + +```bash +cosmos.gov.v1.Query/TallyResult +``` + +Example: + +```bash +grpcurl -plaintext \ + -d '{"proposal_id":"1"}' \ + localhost:9090 \ + cosmos.gov.v1.Query/TallyResult +``` + +Example Output: + +```bash +{ + "tally": { + "yes": "1000000", + "abstain": "0", + "no": "0", + "noWithVeto": "0" + } +} +``` + +### REST + +A user can query the `gov` module using REST endpoints. + +#### proposal + +The `proposals` endpoint allows users to query a given proposal. + +Using legacy v1beta1: + +```bash +/cosmos/gov/v1beta1/proposals/{proposal_id} +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1beta1/proposals/1 +``` + +Example Output: + +```bash +{ + "proposal": { + "proposal_id": "1", + "content": null, + "status": "PROPOSAL_STATUS_VOTING_PERIOD", + "final_tally_result": { + "yes": "0", + "abstain": "0", + "no": "0", + "no_with_veto": "0" + }, + "submit_time": "2022-03-28T11:50:20.819676256Z", + "deposit_end_time": "2022-03-30T11:50:20.819676256Z", + "total_deposit": [ + { + "denom": "stake", + "amount": "10000000010" + } + ], + "voting_start_time": "2022-03-28T14:25:26.644857113Z", + "voting_end_time": "2022-03-30T14:25:26.644857113Z" + } +} +``` + +Using v1: + +```bash +/cosmos/gov/v1/proposals/{proposal_id} +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1/proposals/1 +``` + +Example Output: + +```bash +{ + "proposal": { + "id": "1", + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "cosmos1..", + "to_address": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10" + } + ] + } + ], + "status": "PROPOSAL_STATUS_VOTING_PERIOD", + "final_tally_result": { + "yes_count": "0", + "abstain_count": "0", + "no_count": "0", + "no_with_veto_count": "0" + }, + "submit_time": "2022-03-28T11:50:20.819676256Z", + "deposit_end_time": "2022-03-30T11:50:20.819676256Z", + "total_deposit": [ + { + "denom": "stake", + "amount": "10000000" + } + ], + "voting_start_time": "2022-03-28T14:25:26.644857113Z", + "voting_end_time": "2022-03-30T14:25:26.644857113Z", + "metadata": "AQ==", + "title": "Proposal Title", + "summary": "Proposal Summary" + } +} +``` + +#### proposals + +The `proposals` endpoint also allows users to query all proposals with optional filters. + +Using legacy v1beta1: + +```bash +/cosmos/gov/v1beta1/proposals +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1beta1/proposals +``` + +Example Output: + +```bash +{ + "proposals": [ + { + "proposal_id": "1", + "content": null, + "status": "PROPOSAL_STATUS_VOTING_PERIOD", + "final_tally_result": { + "yes": "0", + "abstain": "0", + "no": "0", + "no_with_veto": "0" + }, + "submit_time": "2022-03-28T11:50:20.819676256Z", + "deposit_end_time": "2022-03-30T11:50:20.819676256Z", + "total_deposit": [ + { + "denom": "stake", + "amount": "10000000" + } + ], + "voting_start_time": "2022-03-28T14:25:26.644857113Z", + "voting_end_time": "2022-03-30T14:25:26.644857113Z" + }, + { + "proposal_id": "2", + "content": null, + "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", + "final_tally_result": { + "yes": "0", + "abstain": "0", + "no": "0", + "no_with_veto": "0" + }, + "submit_time": "2022-03-28T14:02:41.165025015Z", + "deposit_end_time": "2022-03-30T14:02:41.165025015Z", + "total_deposit": [ + { + "denom": "stake", + "amount": "10" + } + ], + "voting_start_time": "0001-01-01T00:00:00Z", + "voting_end_time": "0001-01-01T00:00:00Z" + } + ], + "pagination": { + "next_key": null, + "total": "2" + } +} +``` + +Using v1: + +```bash +/cosmos/gov/v1/proposals +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1/proposals +``` + +Example Output: + +```bash +{ + "proposals": [ + { + "id": "1", + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "cosmos1..", + "to_address": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10" + } + ] + } + ], + "status": "PROPOSAL_STATUS_VOTING_PERIOD", + "final_tally_result": { + "yes_count": "0", + "abstain_count": "0", + "no_count": "0", + "no_with_veto_count": "0" + }, + "submit_time": "2022-03-28T11:50:20.819676256Z", + "deposit_end_time": "2022-03-30T11:50:20.819676256Z", + "total_deposit": [ + { + "denom": "stake", + "amount": "10000000010" + } + ], + "voting_start_time": "2022-03-28T14:25:26.644857113Z", + "voting_end_time": "2022-03-30T14:25:26.644857113Z", + "metadata": "AQ==", + "title": "Proposal Title", + "summary": "Proposal Summary" + }, + { + "id": "2", + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "cosmos1..", + "to_address": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10" + } + ] + } + ], + "status": "PROPOSAL_STATUS_DEPOSIT_PERIOD", + "final_tally_result": { + "yes_count": "0", + "abstain_count": "0", + "no_count": "0", + "no_with_veto_count": "0" + }, + "submit_time": "2022-03-28T14:02:41.165025015Z", + "deposit_end_time": "2022-03-30T14:02:41.165025015Z", + "total_deposit": [ + { + "denom": "stake", + "amount": "10" + } + ], + "voting_start_time": null, + "voting_end_time": null, + "metadata": "AQ==", + "title": "Proposal Title", + "summary": "Proposal Summary" + } + ], + "pagination": { + "next_key": null, + "total": "2" + } +} +``` + +#### voter vote + +The `votes` endpoint allows users to query a vote for a given proposal. + +Using legacy v1beta1: + +```bash +/cosmos/gov/v1beta1/proposals/{proposal_id}/votes/{voter} +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1beta1/proposals/1/votes/cosmos1.. +``` + +Example Output: + +```bash +{ + "vote": { + "proposal_id": "1", + "voter": "cosmos1..", + "option": "VOTE_OPTION_YES", + "options": [ + { + "option": "VOTE_OPTION_YES", + "weight": "1.000000000000000000" + } + ] + } +} +``` + +Using v1: + +```bash +/cosmos/gov/v1/proposals/{proposal_id}/votes/{voter} +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1/proposals/1/votes/cosmos1.. +``` + +Example Output: + +```bash +{ + "vote": { + "proposal_id": "1", + "voter": "cosmos1..", + "options": [ + { + "option": "VOTE_OPTION_YES", + "weight": "1.000000000000000000" + } + ], + "metadata": "" + } +} +``` + +#### votes + +The `votes` endpoint allows users to query all votes for a given proposal. + +Using legacy v1beta1: + +```bash +/cosmos/gov/v1beta1/proposals/{proposal_id}/votes +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1beta1/proposals/1/votes +``` + +Example Output: + +```bash +{ + "votes": [ + { + "proposal_id": "1", + "voter": "cosmos1..", + "option": "VOTE_OPTION_YES", + "options": [ + { + "option": "VOTE_OPTION_YES", + "weight": "1.000000000000000000" + } + ] + } + ], + "pagination": { + "next_key": null, + "total": "1" + } +} +``` + +Using v1: + +```bash +/cosmos/gov/v1/proposals/{proposal_id}/votes +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1/proposals/1/votes +``` + +Example Output: + +```bash +{ + "votes": [ + { + "proposal_id": "1", + "voter": "cosmos1..", + "options": [ + { + "option": "VOTE_OPTION_YES", + "weight": "1.000000000000000000" + } + ], + "metadata": "" + } + ], + "pagination": { + "next_key": null, + "total": "1" + } +} +``` + +#### params + +The `params` endpoint allows users to query all parameters for the `gov` module. + + + +Using legacy v1beta1: + +```bash +/cosmos/gov/v1beta1/params/{params_type} +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1beta1/params/voting +``` + +Example Output: + +```bash +{ + "voting_params": { + "voting_period": "172800s" + }, + "deposit_params": { + "min_deposit": [ + ], + "max_deposit_period": "0s" + }, + "tally_params": { + "quorum": "0.000000000000000000", + "threshold": "0.000000000000000000", + "veto_threshold": "0.000000000000000000" + } +} +``` + +Using v1: + +```bash +/cosmos/gov/v1/params/{params_type} +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1/params/voting +``` + +Example Output: + +```bash +{ + "voting_params": { + "voting_period": "172800s" + }, + "deposit_params": { + "min_deposit": [ + ], + "max_deposit_period": "0s" + }, + "tally_params": { + "quorum": "0.000000000000000000", + "threshold": "0.000000000000000000", + "veto_threshold": "0.000000000000000000" + } +} +``` + +#### deposits + +The `deposits` endpoint allows users to query a deposit for a given proposal from a given depositor. + +Using legacy v1beta1: + +```bash +/cosmos/gov/v1beta1/proposals/{proposal_id}/deposits/{depositor} +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1beta1/proposals/1/deposits/cosmos1.. +``` + +Example Output: + +```bash +{ + "deposit": { + "proposal_id": "1", + "depositor": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10000000" + } + ] + } +} +``` + +Using v1: + +```bash +/cosmos/gov/v1/proposals/{proposal_id}/deposits/{depositor} +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1/proposals/1/deposits/cosmos1.. +``` + +Example Output: + +```bash +{ + "deposit": { + "proposal_id": "1", + "depositor": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10000000" + } + ] + } +} +``` + +#### proposal deposits + +The `deposits` endpoint allows users to query all deposits for a given proposal. + +Using legacy v1beta1: + +```bash +/cosmos/gov/v1beta1/proposals/{proposal_id}/deposits +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1beta1/proposals/1/deposits +``` + +Example Output: + +```bash +{ + "deposits": [ + { + "proposal_id": "1", + "depositor": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10000000" + } + ] + } + ], + "pagination": { + "next_key": null, + "total": "1" + } +} +``` + +Using v1: + +```bash +/cosmos/gov/v1/proposals/{proposal_id}/deposits +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1/proposals/1/deposits +``` + +Example Output: + +```bash +{ + "deposits": [ + { + "proposal_id": "1", + "depositor": "cosmos1..", + "amount": [ + { + "denom": "stake", + "amount": "10000000" + } + ] + } + ], + "pagination": { + "next_key": null, + "total": "1" + } +} +``` + +#### tally + +The `tally` endpoint allows users to query the tally of a given proposal. + +Using legacy v1beta1: + +```bash +/cosmos/gov/v1beta1/proposals/{proposal_id}/tally +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1beta1/proposals/1/tally +``` + +Example Output: + +```bash +{ + "tally": { + "yes": "1000000", + "abstain": "0", + "no": "0", + "no_with_veto": "0" + } +} +``` + +Using v1: + +```bash +/cosmos/gov/v1/proposals/{proposal_id}/tally +``` + +Example: + +```bash +curl localhost:1317/cosmos/gov/v1/proposals/1/tally +``` + +Example Output: + +```bash +{ + "tally": { + "yes": "1000000", + "abstain": "0", + "no": "0", + "no_with_veto": "0" + } +} +``` + + +## Metadata + +The gov module has two locations for metadata where users can provide further context about the on-chain actions they are taking. By default all metadata fields have a 255 character length field where metadata can be stored in json format, either on-chain or off-chain depending on the amount of data required. Here we provide a recommendation for the json structure and where the data should be stored. There are two important factors in making these recommendations. First, that the gov and group modules are consistent with one another, note the number of proposals made by all groups may be quite large. Second, that client applications such as block explorers and governance interfaces have confidence in the consistency of metadata structure accross chains. + +### Proposal + +Location: off-chain as json object stored on IPFS (mirrors [group proposal](../group/README.md#metadata)) + +```json +{ + "title": "", + "authors": [""], + "summary": "", + "details": "", + "proposal_forum_url": "", + "vote_option_context": "", +} +``` + +:::note +The `authors` field is an array of strings, this is to allow for multiple authors to be listed in the metadata. +In v0.46, the `authors` field is a comma-separated string. Frontends are encouraged to support both formats for backwards compatibility. +::: + +### Vote + +Location: on-chain as json within 255 character limit (mirrors [group vote](../group/README.md#metadata)) + +```json +{ + "justification": "", +} +``` + +## Future Improvements + +The current documentation only describes the minimum viable product for the +governance module. Future improvements may include: + +* **`BountyProposals`:** If accepted, a `BountyProposal` creates an open + bounty. The `BountyProposal` specifies how many Atoms will be given upon + completion. These Atoms will be taken from the `reserve pool`. After a + `BountyProposal` is accepted by governance, anybody can submit a + `SoftwareUpgradeProposal` with the code to claim the bounty. Note that once a + `BountyProposal` is accepted, the corresponding funds in the `reserve pool` + are locked so that payment can always be honored. In order to link a + `SoftwareUpgradeProposal` to an open bounty, the submitter of the + `SoftwareUpgradeProposal` will use the `Proposal.LinkedProposal` attribute. + If a `SoftwareUpgradeProposal` linked to an open bounty is accepted by + governance, the funds that were reserved are automatically transferred to the + submitter. +* **Complex delegation:** Delegators could choose other representatives than + their validators. Ultimately, the chain of representatives would always end + up to a validator, but delegators could inherit the vote of their chosen + representative before they inherit the vote of their validator. In other + words, they would only inherit the vote of their validator if their other + appointed representative did not vote. +* **Better process for proposal review:** There would be two parts to + `proposal.Deposit`, one for anti-spam (same as in MVP) and an other one to + reward third party auditors. diff --git a/x/gov/abci.go b/x/gov/abci.go new file mode 100644 index 00000000..3dc03b99 --- /dev/null +++ b/x/gov/abci.go @@ -0,0 +1,151 @@ +package gov + +import ( + "fmt" + "time" + + "github.com/atomone-hub/atomone/x/gov/keeper" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// EndBlocker called every block, process inflation, update validator set. +func EndBlocker(ctx sdk.Context, keeper *keeper.Keeper) { + defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker) + + logger := keeper.Logger(ctx) + + // delete dead proposals from store and returns theirs deposits. + // A proposal is dead when it's inactive and didn't get enough deposit on time to get into voting phase. + keeper.IterateInactiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal v1.Proposal) bool { + keeper.DeleteProposal(ctx, proposal.Id) + + params := keeper.GetParams(ctx) + if !params.BurnProposalDepositPrevote { + keeper.RefundAndDeleteDeposits(ctx, proposal.Id) // refund deposit if proposal got removed without getting 100% of the proposal + } else { + keeper.DeleteAndBurnDeposits(ctx, proposal.Id) // burn the deposit if proposal got removed without getting 100% of the proposal + } + + // called when proposal become inactive + keeper.Hooks().AfterProposalFailedMinDeposit(ctx, proposal.Id) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeInactiveProposal, + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)), + sdk.NewAttribute(types.AttributeKeyProposalResult, types.AttributeValueProposalDropped), + ), + ) + + logger.Info( + "proposal did not meet minimum deposit; deleted", + "proposal", proposal.Id, + "min_deposit", sdk.NewCoins(params.MinDeposit...).String(), + "total_deposit", sdk.NewCoins(proposal.TotalDeposit...).String(), + ) + + return false + }) + + // fetch active proposals whose voting periods have ended (are passed the block time) + keeper.IterateActiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal v1.Proposal) bool { + var tagValue, logMsg string + + passes, burnDeposits, tallyResults := keeper.Tally(ctx, proposal) + + if burnDeposits { + keeper.DeleteAndBurnDeposits(ctx, proposal.Id) + } else { + keeper.RefundAndDeleteDeposits(ctx, proposal.Id) + } + + if passes { + var ( + idx int + events sdk.Events + msg sdk.Msg + ) + + // attempt to execute all messages within the passed proposal + // Messages may mutate state thus we use a cached context. If one of + // the handlers fails, no state mutation is written and the error + // message is logged. + cacheCtx, writeCache := ctx.CacheContext() + messages, err := proposal.GetMsgs() + if err == nil { + for idx, msg = range messages { + handler := keeper.Router().Handler(msg) + var res *sdk.Result + res, err = safeExecuteHandler(cacheCtx, msg, handler) + if err != nil { + break + } + + events = append(events, res.GetEvents()...) + } + } + + // `err == nil` when all handlers passed. + // Or else, `idx` and `err` are populated with the msg index and error. + if err == nil { + proposal.Status = v1.StatusPassed + tagValue = types.AttributeValueProposalPassed + logMsg = "passed" + + // write state to the underlying multi-store + writeCache() + + // propagate the msg events to the current context + ctx.EventManager().EmitEvents(events) + } else { + proposal.Status = v1.StatusFailed + tagValue = types.AttributeValueProposalFailed + logMsg = fmt.Sprintf("passed, but msg %d (%s) failed on execution: %s", idx, sdk.MsgTypeURL(msg), err) + } + } else { + proposal.Status = v1.StatusRejected + tagValue = types.AttributeValueProposalRejected + logMsg = "rejected" + } + + proposal.FinalTallyResult = &tallyResults + + keeper.SetProposal(ctx, proposal) + keeper.RemoveFromActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime) + + // when proposal become active + keeper.Hooks().AfterProposalVotingPeriodEnded(ctx, proposal.Id) + + logger.Info( + "proposal tallied", + "proposal", proposal.Id, + "results", logMsg, + ) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeActiveProposal, + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)), + sdk.NewAttribute(types.AttributeKeyProposalResult, tagValue), + ), + ) + return false + }) +} + +// executes handle(msg) and recovers from panic. +func safeExecuteHandler(ctx sdk.Context, msg sdk.Msg, handler baseapp.MsgServiceHandler, +) (res *sdk.Result, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("handling x/gov proposal msg [%s] PANICKED: %v", msg, r) + } + }() + res, err = handler(ctx, msg) + return +} diff --git a/x/gov/abci_internal_test.go b/x/gov/abci_internal_test.go new file mode 100644 index 00000000..1421a81b --- /dev/null +++ b/x/gov/abci_internal_test.go @@ -0,0 +1,32 @@ +package gov + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func failingHandler(_ sdk.Context, _ sdk.Msg) (*sdk.Result, error) { + panic("test-fail") +} + +func okHandler(_ sdk.Context, _ sdk.Msg) (*sdk.Result, error) { + return new(sdk.Result), nil +} + +func TestSafeExecuteHandler(t *testing.T) { + t.Parallel() + + require := require.New(t) + var ctx sdk.Context + + r, err := safeExecuteHandler(ctx, nil, failingHandler) + require.ErrorContains(err, "test-fail") + require.Nil(r) + + r, err = safeExecuteHandler(ctx, nil, okHandler) + require.Nil(err) + require.NotNil(r) +} diff --git a/x/gov/abci_test.go b/x/gov/abci_test.go new file mode 100644 index 00000000..89118475 --- /dev/null +++ b/x/gov/abci_test.go @@ -0,0 +1,403 @@ +package gov_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + abci "github.com/cometbft/cometbft/abci/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/atomone-hub/atomone/x/gov" + "github.com/atomone-hub/atomone/x/gov/keeper" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func TestTickExpiredDepositPeriod(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := simtestutil.AddTestAddrs(suite.BankKeeper, suite.StakingKeeper, ctx, 10, valTokens) + + header := tmproto.Header{Height: app.LastBlockHeight() + 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + govMsgSvr := keeper.NewMsgServerImpl(suite.GovKeeper) + + inactiveQueue := suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newProposalMsg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{mkTestLegacyContent(t)}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}, + addrs[0].String(), + "", + "Proposal", + "description of proposal", + ) + require.NoError(t, err) + + res, err := govMsgSvr.SubmitProposal(sdk.WrapSDKContext(ctx), newProposalMsg) + require.NoError(t, err) + require.NotNil(t, res) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newHeader := ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(time.Duration(1) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newHeader = ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(*suite.GovKeeper.GetParams(ctx).MaxDepositPeriod) + ctx = ctx.WithBlockHeader(newHeader) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.True(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + gov.EndBlocker(ctx, suite.GovKeeper) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() +} + +func TestTickMultipleExpiredDepositPeriod(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := simtestutil.AddTestAddrs(suite.BankKeeper, suite.StakingKeeper, ctx, 10, valTokens) + + header := tmproto.Header{Height: app.LastBlockHeight() + 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + govMsgSvr := keeper.NewMsgServerImpl(suite.GovKeeper) + + inactiveQueue := suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newProposalMsg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{mkTestLegacyContent(t)}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}, + addrs[0].String(), + "", + "Proposal", + "description of proposal", + ) + require.NoError(t, err) + + res, err := govMsgSvr.SubmitProposal(sdk.WrapSDKContext(ctx), newProposalMsg) + require.NoError(t, err) + require.NotNil(t, res) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newHeader := ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(time.Duration(2) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newProposalMsg2, err := v1.NewMsgSubmitProposal( + []sdk.Msg{mkTestLegacyContent(t)}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}, + addrs[0].String(), + "", + "Proposal", + "description of proposal", + ) + require.NoError(t, err) + + res, err = govMsgSvr.SubmitProposal(sdk.WrapSDKContext(ctx), newProposalMsg2) + require.NoError(t, err) + require.NotNil(t, res) + + newHeader = ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(*suite.GovKeeper.GetParams(ctx).MaxDepositPeriod).Add(time.Duration(-1) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.True(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + gov.EndBlocker(ctx, suite.GovKeeper) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newHeader = ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(time.Duration(5) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.True(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + gov.EndBlocker(ctx, suite.GovKeeper) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() +} + +func TestTickPassedDepositPeriod(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := simtestutil.AddTestAddrs(suite.BankKeeper, suite.StakingKeeper, ctx, 10, valTokens) + + header := tmproto.Header{Height: app.LastBlockHeight() + 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + govMsgSvr := keeper.NewMsgServerImpl(suite.GovKeeper) + + inactiveQueue := suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + activeQueue := suite.GovKeeper.ActiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, activeQueue.Valid()) + activeQueue.Close() + + newProposalMsg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{mkTestLegacyContent(t)}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}, + addrs[0].String(), + "", + "Proposal", + "description of proposal", + ) + require.NoError(t, err) + + res, err := govMsgSvr.SubmitProposal(sdk.WrapSDKContext(ctx), newProposalMsg) + require.NoError(t, err) + require.NotNil(t, res) + + proposalID := res.ProposalId + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newHeader := ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(time.Duration(1) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + newDepositMsg := v1.NewMsgDeposit(addrs[1], proposalID, sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 5)}) + + res1, err := govMsgSvr.Deposit(sdk.WrapSDKContext(ctx), newDepositMsg) + require.NoError(t, err) + require.NotNil(t, res1) + + activeQueue = suite.GovKeeper.ActiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, activeQueue.Valid()) + activeQueue.Close() +} + +func TestTickPassedVotingPeriod(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := simtestutil.AddTestAddrs(suite.BankKeeper, suite.StakingKeeper, ctx, 10, valTokens) + + SortAddresses(addrs) + + header := tmproto.Header{Height: app.LastBlockHeight() + 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + govMsgSvr := keeper.NewMsgServerImpl(suite.GovKeeper) + + inactiveQueue := suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + activeQueue := suite.GovKeeper.ActiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, activeQueue.Valid()) + activeQueue.Close() + + proposalCoins := sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, suite.StakingKeeper.TokensFromConsensusPower(ctx, 5))} + newProposalMsg, err := v1.NewMsgSubmitProposal([]sdk.Msg{mkTestLegacyContent(t)}, proposalCoins, addrs[0].String(), "", "Proposal", "description of proposal") + require.NoError(t, err) + + wrapCtx := sdk.WrapSDKContext(ctx) + + res, err := govMsgSvr.SubmitProposal(wrapCtx, newProposalMsg) + require.NoError(t, err) + require.NotNil(t, res) + + proposalID := res.ProposalId + + newHeader := ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(time.Duration(1) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + + newDepositMsg := v1.NewMsgDeposit(addrs[1], proposalID, proposalCoins) + + res1, err := govMsgSvr.Deposit(wrapCtx, newDepositMsg) + require.NoError(t, err) + require.NotNil(t, res1) + + newHeader = ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(*suite.GovKeeper.GetParams(ctx).MaxDepositPeriod).Add(*suite.GovKeeper.GetParams(ctx).VotingPeriod) + ctx = ctx.WithBlockHeader(newHeader) + + inactiveQueue = suite.GovKeeper.InactiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, inactiveQueue.Valid()) + inactiveQueue.Close() + + activeQueue = suite.GovKeeper.ActiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.True(t, activeQueue.Valid()) + + activeProposalID := types.GetProposalIDFromBytes(activeQueue.Value()) + proposal, ok := suite.GovKeeper.GetProposal(ctx, activeProposalID) + require.True(t, ok) + require.Equal(t, v1.StatusVotingPeriod, proposal.Status) + + activeQueue.Close() + + gov.EndBlocker(ctx, suite.GovKeeper) + + activeQueue = suite.GovKeeper.ActiveProposalQueueIterator(ctx, ctx.BlockHeader().Time) + require.False(t, activeQueue.Valid()) + activeQueue.Close() +} + +func TestProposalPassedEndblocker(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := simtestutil.AddTestAddrs(suite.BankKeeper, suite.StakingKeeper, ctx, 10, valTokens) + + SortAddresses(addrs) + + govMsgSvr := keeper.NewMsgServerImpl(suite.GovKeeper) + stakingMsgSvr := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper) + + header := tmproto.Header{Height: app.LastBlockHeight() + 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + valAddr := sdk.ValAddress(addrs[0]) + + createValidators(t, stakingMsgSvr, ctx, []sdk.ValAddress{valAddr}, []int64{10}) + staking.EndBlocker(ctx, suite.StakingKeeper) + + macc := suite.GovKeeper.GetGovernanceAccount(ctx) + require.NotNil(t, macc) + initialModuleAccCoins := suite.BankKeeper.GetAllBalances(ctx, macc.GetAddress()) + + proposal, err := suite.GovKeeper.SubmitProposal(ctx, []sdk.Msg{mkTestLegacyContent(t)}, "", "title", "summary", addrs[0]) + require.NoError(t, err) + + proposalCoins := sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, suite.StakingKeeper.TokensFromConsensusPower(ctx, 10))} + newDepositMsg := v1.NewMsgDeposit(addrs[0], proposal.Id, proposalCoins) + + res, err := govMsgSvr.Deposit(sdk.WrapSDKContext(ctx), newDepositMsg) + require.NoError(t, err) + require.NotNil(t, res) + + macc = suite.GovKeeper.GetGovernanceAccount(ctx) + require.NotNil(t, macc) + moduleAccCoins := suite.BankKeeper.GetAllBalances(ctx, macc.GetAddress()) + + deposits := initialModuleAccCoins.Add(proposal.TotalDeposit...).Add(proposalCoins...) + require.True(t, moduleAccCoins.IsEqual(deposits)) + + err = suite.GovKeeper.AddVote(ctx, proposal.Id, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), "") + require.NoError(t, err) + + newHeader := ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(*suite.GovKeeper.GetParams(ctx).MaxDepositPeriod).Add(*suite.GovKeeper.GetParams(ctx).VotingPeriod) + ctx = ctx.WithBlockHeader(newHeader) + + gov.EndBlocker(ctx, suite.GovKeeper) + + macc = suite.GovKeeper.GetGovernanceAccount(ctx) + require.NotNil(t, macc) + require.True(t, suite.BankKeeper.GetAllBalances(ctx, macc.GetAddress()).IsEqual(initialModuleAccCoins)) +} + +func TestEndBlockerProposalHandlerFailed(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + addrs := simtestutil.AddTestAddrs(suite.BankKeeper, suite.StakingKeeper, ctx, 1, valTokens) + + SortAddresses(addrs) + + stakingMsgSvr := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper) + header := tmproto.Header{Height: app.LastBlockHeight() + 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + + valAddr := sdk.ValAddress(addrs[0]) + + createValidators(t, stakingMsgSvr, ctx, []sdk.ValAddress{valAddr}, []int64{10}) + staking.EndBlocker(ctx, suite.StakingKeeper) + + msg := banktypes.NewMsgSend(authtypes.NewModuleAddress(types.ModuleName), addrs[0], sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000)))) + proposal, err := suite.GovKeeper.SubmitProposal(ctx, []sdk.Msg{msg}, "", "Bank Msg Send", "send message", addrs[0]) + require.NoError(t, err) + + proposalCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, suite.StakingKeeper.TokensFromConsensusPower(ctx, 10))) + newDepositMsg := v1.NewMsgDeposit(addrs[0], proposal.Id, proposalCoins) + + govMsgSvr := keeper.NewMsgServerImpl(suite.GovKeeper) + res, err := govMsgSvr.Deposit(sdk.WrapSDKContext(ctx), newDepositMsg) + require.NoError(t, err) + require.NotNil(t, res) + + err = suite.GovKeeper.AddVote(ctx, proposal.Id, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), "") + require.NoError(t, err) + + newHeader := ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(*suite.GovKeeper.GetParams(ctx).MaxDepositPeriod).Add(*suite.GovKeeper.GetParams(ctx).VotingPeriod) + ctx = ctx.WithBlockHeader(newHeader) + + // validate that the proposal fails/has been rejected + gov.EndBlocker(ctx, suite.GovKeeper) + + proposal, ok := suite.GovKeeper.GetProposal(ctx, proposal.Id) + require.True(t, ok) + require.Equal(t, v1.StatusFailed, proposal.Status) +} + +func createValidators(t *testing.T, stakingMsgSvr stakingtypes.MsgServer, ctx sdk.Context, addrs []sdk.ValAddress, powerAmt []int64) { + require.True(t, len(addrs) <= len(pubkeys), "Not enough pubkeys specified at top of file.") + + for i := 0; i < len(addrs); i++ { + valTokens := sdk.TokensFromConsensusPower(powerAmt[i], sdk.DefaultPowerReduction) + valCreateMsg, err := stakingtypes.NewMsgCreateValidator( + addrs[i], pubkeys[i], sdk.NewCoin(sdk.DefaultBondDenom, valTokens), + TestDescription, TestCommissionRates, math.OneInt(), + ) + require.NoError(t, err) + res, err := stakingMsgSvr.CreateValidator(sdk.WrapSDKContext(ctx), valCreateMsg) + require.NoError(t, err) + require.NotNil(t, res) + } +} diff --git a/x/gov/autocli.go b/x/gov/autocli.go new file mode 100644 index 00000000..3b53657f --- /dev/null +++ b/x/gov/autocli.go @@ -0,0 +1,27 @@ +package gov + +import ( + autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" + govv1 "cosmossdk.io/api/cosmos/gov/v1" + govv1beta1 "cosmossdk.io/api/cosmos/gov/v1beta1" +) + +// AutoCLIOptions implements the autocli.HasAutoCLIConfig interface. +func (am AppModule) AutoCLIOptions() *autocliv1.ModuleOptions { + return &autocliv1.ModuleOptions{ + Tx: &autocliv1.ServiceCommandDescriptor{ + Service: govv1.Msg_ServiceDesc.ServiceName, + // map v1beta1 as a sub-command + SubCommands: map[string]*autocliv1.ServiceCommandDescriptor{ + "v1beta1": {Service: govv1beta1.Msg_ServiceDesc.ServiceName}, + }, + }, + Query: &autocliv1.ServiceCommandDescriptor{ + Service: govv1.Query_ServiceDesc.ServiceName, + // map v1beta1 as a sub-command + SubCommands: map[string]*autocliv1.ServiceCommandDescriptor{ + "v1beta1": {Service: govv1beta1.Query_ServiceDesc.ServiceName}, + }, + }, + } +} diff --git a/x/gov/common_test.go b/x/gov/common_test.go new file mode 100644 index 00000000..9bf417c3 --- /dev/null +++ b/x/gov/common_test.go @@ -0,0 +1,128 @@ +package gov_test + +import ( + "bytes" + "log" + "sort" + "testing" + + "github.com/atomone-hub/atomone/x/gov/keeper" + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/testutil/configurator" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + _ "github.com/cosmos/cosmos-sdk/x/auth" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + _ "github.com/cosmos/cosmos-sdk/x/bank" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + _ "github.com/cosmos/cosmos-sdk/x/consensus" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + _ "github.com/cosmos/cosmos-sdk/x/params" + _ "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +var ( + valTokens = sdk.TokensFromConsensusPower(42, sdk.DefaultPowerReduction) + TestProposal = v1beta1.NewTextProposal("Test", "description") + TestDescription = stakingtypes.NewDescription("T", "E", "S", "T", "Z") + TestCommissionRates = stakingtypes.NewCommissionRates(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()) +) + +// mkTestLegacyContent creates a MsgExecLegacyContent for testing purposes. +func mkTestLegacyContent(t *testing.T) *v1.MsgExecLegacyContent { + msgContent, err := v1.NewLegacyContent(TestProposal, authtypes.NewModuleAddress(types.ModuleName).String()) + require.NoError(t, err) + + return msgContent +} + +// SortAddresses - Sorts Addresses +func SortAddresses(addrs []sdk.AccAddress) { + byteAddrs := make([][]byte, len(addrs)) + + for i, addr := range addrs { + byteAddrs[i] = addr.Bytes() + } + + SortByteArrays(byteAddrs) + + for i, byteAddr := range byteAddrs { + addrs[i] = byteAddr + } +} + +// implement `Interface` in sort package. +type sortByteArrays [][]byte + +func (b sortByteArrays) Len() int { + return len(b) +} + +func (b sortByteArrays) Less(i, j int) bool { + // bytes package already implements Comparable for []byte. + switch bytes.Compare(b[i], b[j]) { + case -1: + return true + case 0, 1: + return false + default: + log.Panic("not fail-able with `bytes.Comparable` bounded [-1, 1].") + return false + } +} + +func (b sortByteArrays) Swap(i, j int) { + b[j], b[i] = b[i], b[j] +} + +// SortByteArrays - sorts the provided byte array +func SortByteArrays(src [][]byte) [][]byte { + sorted := sortByteArrays(src) + sort.Sort(sorted) + return sorted +} + +var pubkeys = []cryptotypes.PubKey{ + ed25519.GenPrivKey().PubKey(), + ed25519.GenPrivKey().PubKey(), + ed25519.GenPrivKey().PubKey(), +} + +type suite struct { + AccountKeeper authkeeper.AccountKeeper + BankKeeper bankkeeper.Keeper + GovKeeper *keeper.Keeper + StakingKeeper *stakingkeeper.Keeper + App *runtime.App +} + +func createTestSuite(t *testing.T) suite { + res := suite{} + + app, err := simtestutil.SetupWithConfiguration( + configurator.NewAppConfig( + configurator.ParamsModule(), + configurator.AuthModule(), + configurator.StakingModule(), + configurator.BankModule(), + configurator.GovModule(), + configurator.ConsensusModule(), + ), + simtestutil.DefaultStartUpConfig(), + &res.AccountKeeper, &res.BankKeeper, &res.GovKeeper, &res.StakingKeeper, + ) + require.NoError(t, err) + + res.App = app + return res +} diff --git a/x/gov/exported/exported.go b/x/gov/exported/exported.go new file mode 100644 index 00000000..08a5d259 --- /dev/null +++ b/x/gov/exported/exported.go @@ -0,0 +1,18 @@ +package exported + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" +) + +type ( + ParamSet = paramtypes.ParamSet + + // Subspace defines an interface that implements the legacy x/params Subspace + // type. + // + // NOTE: This is used solely for migration of x/params managed parameters. + ParamSubspace interface { + Get(ctx sdk.Context, key []byte, ptr interface{}) + } +) diff --git a/x/gov/genesis.go b/x/gov/genesis.go new file mode 100644 index 00000000..764877b7 --- /dev/null +++ b/x/gov/genesis.go @@ -0,0 +1,79 @@ +package gov + +import ( + "fmt" + + "github.com/atomone-hub/atomone/x/gov/keeper" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// InitGenesis - store genesis parameters +func InitGenesis(ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, data *v1.GenesisState) { + k.SetProposalID(ctx, data.StartingProposalId) + k.SetParams(ctx, *data.Params) + + // check if the deposits pool account exists + moduleAcc := k.GetGovernanceAccount(ctx) + if moduleAcc == nil { + panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) + } + + var totalDeposits sdk.Coins + for _, deposit := range data.Deposits { + k.SetDeposit(ctx, *deposit) + totalDeposits = totalDeposits.Add(deposit.Amount...) + } + + for _, vote := range data.Votes { + k.SetVote(ctx, *vote) + } + + for _, proposal := range data.Proposals { + switch proposal.Status { + case v1.StatusDepositPeriod: + k.InsertInactiveProposalQueue(ctx, proposal.Id, *proposal.DepositEndTime) + case v1.StatusVotingPeriod: + k.InsertActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime) + } + k.SetProposal(ctx, *proposal) + } + + // if account has zero balance it probably means it's not set, so we set it + balance := bk.GetAllBalances(ctx, moduleAcc.GetAddress()) + if balance.IsZero() { + ak.SetModuleAccount(ctx, moduleAcc) + } + + // check if total deposits equals balance, if it doesn't panic because there were export/import errors + if !balance.IsEqual(totalDeposits) { + panic(fmt.Sprintf("expected module account was %s but we got %s", balance.String(), totalDeposits.String())) + } +} + +// ExportGenesis - output genesis parameters +func ExportGenesis(ctx sdk.Context, k *keeper.Keeper) *v1.GenesisState { + startingProposalID, _ := k.GetProposalID(ctx) + proposals := k.GetProposals(ctx) + params := k.GetParams(ctx) + + var proposalsDeposits v1.Deposits + var proposalsVotes v1.Votes + for _, proposal := range proposals { + deposits := k.GetDeposits(ctx, proposal.Id) + proposalsDeposits = append(proposalsDeposits, deposits...) + + votes := k.GetVotes(ctx, proposal.Id) + proposalsVotes = append(proposalsVotes, votes...) + } + + return &v1.GenesisState{ + StartingProposalId: startingProposalID, + Deposits: proposalsDeposits, + Votes: proposalsVotes, + Proposals: proposals, + Params: ¶ms, + } +} diff --git a/x/gov/genesis_test.go b/x/gov/genesis_test.go new file mode 100644 index 00000000..22b64f02 --- /dev/null +++ b/x/gov/genesis_test.go @@ -0,0 +1,39 @@ +package gov_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/atomone-hub/atomone/x/gov" + + sdk "github.com/cosmos/cosmos-sdk/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +func TestImportExportQueues_ErrorUnconsistentState(t *testing.T) { + suite := createTestSuite(t) + app := suite.App + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + require.Panics(t, func() { + gov.InitGenesis(ctx, suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper, &v1.GenesisState{ + Deposits: v1.Deposits{ + { + ProposalId: 1234, + Depositor: "me", + Amount: sdk.Coins{ + sdk.NewCoin( + "stake", + sdk.NewInt(1234), + ), + }, + }, + }, + }) + }) + gov.InitGenesis(ctx, suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper, v1.DefaultGenesisState()) + genState := gov.ExportGenesis(ctx, suite.GovKeeper) + require.Equal(t, genState, v1.DefaultGenesisState()) +} diff --git a/x/gov/keeper/common_test.go b/x/gov/keeper/common_test.go new file mode 100644 index 00000000..48cb6909 --- /dev/null +++ b/x/gov/keeper/common_test.go @@ -0,0 +1,127 @@ +package keeper_test + +import ( + "fmt" + "testing" + + "github.com/golang/mock/gomock" + + "cosmossdk.io/math" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmtime "github.com/cometbft/cometbft/types/time" + + "github.com/atomone-hub/atomone/x/gov/keeper" + govtestutil "github.com/atomone-hub/atomone/x/gov/testutil" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/testutil" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" +) + +var ( + _, _, addr = testdata.KeyTestPubAddr() + govAcct = authtypes.NewModuleAddress(types.ModuleName) + TestProposal = getTestProposal() +) + +// getTestProposal creates and returns a test proposal message. +func getTestProposal() []sdk.Msg { + legacyProposalMsg, err := v1.NewLegacyContent(v1beta1.NewTextProposal("Title", "description"), authtypes.NewModuleAddress(types.ModuleName).String()) + if err != nil { + panic(err) + } + + return []sdk.Msg{ + banktypes.NewMsgSend(govAcct, addr, sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000)))), + legacyProposalMsg, + } +} + +// setupGovKeeper creates a govKeeper as well as all its dependencies. +func setupGovKeeper(t *testing.T) ( + *keeper.Keeper, + *govtestutil.MockAccountKeeper, + *govtestutil.MockBankKeeper, + *govtestutil.MockStakingKeeper, + moduletestutil.TestEncodingConfig, + sdk.Context, +) { + key := sdk.NewKVStoreKey(types.StoreKey) + testCtx := testutil.DefaultContextWithDB(t, key, sdk.NewTransientStoreKey("transient_test")) + ctx := testCtx.Ctx.WithBlockHeader(tmproto.Header{Time: tmtime.Now()}) + encCfg := moduletestutil.MakeTestEncodingConfig() + v1.RegisterInterfaces(encCfg.InterfaceRegistry) + v1beta1.RegisterInterfaces(encCfg.InterfaceRegistry) + banktypes.RegisterInterfaces(encCfg.InterfaceRegistry) + + // Create MsgServiceRouter, but don't populate it before creating the gov + // keeper. + msr := baseapp.NewMsgServiceRouter() + + // gomock initializations + ctrl := gomock.NewController(t) + acctKeeper := govtestutil.NewMockAccountKeeper(ctrl) + bankKeeper := govtestutil.NewMockBankKeeper(ctrl) + stakingKeeper := govtestutil.NewMockStakingKeeper(ctrl) + acctKeeper.EXPECT().GetModuleAddress(types.ModuleName).Return(govAcct).AnyTimes() + acctKeeper.EXPECT().GetModuleAccount(gomock.Any(), types.ModuleName).Return(authtypes.NewEmptyModuleAccount(types.ModuleName)).AnyTimes() + trackMockBalances(bankKeeper) + stakingKeeper.EXPECT().TokensFromConsensusPower(ctx, gomock.Any()).DoAndReturn(func(ctx sdk.Context, power int64) math.Int { + return sdk.TokensFromConsensusPower(power, math.NewIntFromUint64(1000000)) + }).AnyTimes() + stakingKeeper.EXPECT().BondDenom(ctx).Return("stake").AnyTimes() + stakingKeeper.EXPECT().IterateBondedValidatorsByPower(gomock.Any(), gomock.Any()).AnyTimes() + stakingKeeper.EXPECT().IterateDelegations(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + stakingKeeper.EXPECT().TotalBondedTokens(gomock.Any()).Return(math.NewInt(10000000)).AnyTimes() + + // Gov keeper initializations + govKeeper := keeper.NewKeeper(encCfg.Codec, key, acctKeeper, bankKeeper, stakingKeeper, msr, types.DefaultConfig(), govAcct.String()) + govKeeper.SetProposalID(ctx, 1) + govRouter := v1beta1.NewRouter() // Also register legacy gov handlers to test them too. + govRouter.AddRoute(types.RouterKey, v1beta1.ProposalHandler) + govKeeper.SetLegacyRouter(govRouter) + govKeeper.SetParams(ctx, v1.DefaultParams()) + + // Register all handlers for the MegServiceRouter. + msr.SetInterfaceRegistry(encCfg.InterfaceRegistry) + v1.RegisterMsgServer(msr, keeper.NewMsgServerImpl(govKeeper)) + banktypes.RegisterMsgServer(msr, nil) // Nil is fine here as long as we never execute the proposal's Msgs. + + return govKeeper, acctKeeper, bankKeeper, stakingKeeper, encCfg, ctx +} + +// trackMockBalances sets up expected calls on the Mock BankKeeper, and also +// locally tracks accounts balances (not modules balances). +func trackMockBalances(bankKeeper *govtestutil.MockBankKeeper) { + balances := make(map[string]sdk.Coins) + + // We don't track module account balances. + bankKeeper.EXPECT().MintCoins(gomock.Any(), minttypes.ModuleName, gomock.Any()).AnyTimes() + bankKeeper.EXPECT().BurnCoins(gomock.Any(), types.ModuleName, gomock.Any()).AnyTimes() + bankKeeper.EXPECT().SendCoinsFromModuleToModule(gomock.Any(), minttypes.ModuleName, types.ModuleName, gomock.Any()).AnyTimes() + + // But we do track normal account balances. + bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).DoAndReturn(func(_ sdk.Context, sender sdk.AccAddress, _ string, coins sdk.Coins) error { + newBalance, negative := balances[sender.String()].SafeSub(coins...) + if negative { + return fmt.Errorf("not enough balance") + } + balances[sender.String()] = newBalance + return nil + }).AnyTimes() + bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_ sdk.Context, module string, rcpt sdk.AccAddress, coins sdk.Coins) error { + balances[rcpt.String()] = balances[rcpt.String()].Add(coins...) + return nil + }).AnyTimes() + bankKeeper.EXPECT().GetAllBalances(gomock.Any(), gomock.Any()).DoAndReturn(func(_ sdk.Context, addr sdk.AccAddress) sdk.Coins { + return balances[addr.String()] + }).AnyTimes() +} diff --git a/x/gov/keeper/deposit.go b/x/gov/keeper/deposit.go new file mode 100644 index 00000000..62b90bd4 --- /dev/null +++ b/x/gov/keeper/deposit.go @@ -0,0 +1,202 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// GetDeposit gets the deposit of a specific depositor on a specific proposal +func (keeper Keeper) GetDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) (deposit v1.Deposit, found bool) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.DepositKey(proposalID, depositorAddr)) + if bz == nil { + return deposit, false + } + + keeper.cdc.MustUnmarshal(bz, &deposit) + + return deposit, true +} + +// SetDeposit sets a Deposit to the gov store +func (keeper Keeper) SetDeposit(ctx sdk.Context, deposit v1.Deposit) { + store := ctx.KVStore(keeper.storeKey) + bz := keeper.cdc.MustMarshal(&deposit) + depositor := sdk.MustAccAddressFromBech32(deposit.Depositor) + + store.Set(types.DepositKey(deposit.ProposalId, depositor), bz) +} + +// GetAllDeposits returns all the deposits from the store +func (keeper Keeper) GetAllDeposits(ctx sdk.Context) (deposits v1.Deposits) { + keeper.IterateAllDeposits(ctx, func(deposit v1.Deposit) bool { + deposits = append(deposits, &deposit) + return false + }) + + return +} + +// GetDeposits returns all the deposits of a proposal +func (keeper Keeper) GetDeposits(ctx sdk.Context, proposalID uint64) (deposits v1.Deposits) { + keeper.IterateDeposits(ctx, proposalID, func(deposit v1.Deposit) bool { + deposits = append(deposits, &deposit) + return false + }) + + return +} + +// DeleteAndBurnDeposits deletes and burns all the deposits on a specific proposal. +func (keeper Keeper) DeleteAndBurnDeposits(ctx sdk.Context, proposalID uint64) { + store := ctx.KVStore(keeper.storeKey) + + keeper.IterateDeposits(ctx, proposalID, func(deposit v1.Deposit) bool { + err := keeper.bankKeeper.BurnCoins(ctx, types.ModuleName, deposit.Amount) + if err != nil { + panic(err) + } + + depositor := sdk.MustAccAddressFromBech32(deposit.Depositor) + + store.Delete(types.DepositKey(proposalID, depositor)) + return false + }) +} + +// IterateAllDeposits iterates over all the stored deposits and performs a callback function. +func (keeper Keeper) IterateAllDeposits(ctx sdk.Context, cb func(deposit v1.Deposit) (stop bool)) { + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.DepositsKeyPrefix) + + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var deposit v1.Deposit + + keeper.cdc.MustUnmarshal(iterator.Value(), &deposit) + + if cb(deposit) { + break + } + } +} + +// IterateDeposits iterates over all the proposals deposits and performs a callback function +func (keeper Keeper) IterateDeposits(ctx sdk.Context, proposalID uint64, cb func(deposit v1.Deposit) (stop bool)) { + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.DepositsKey(proposalID)) + + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var deposit v1.Deposit + + keeper.cdc.MustUnmarshal(iterator.Value(), &deposit) + + if cb(deposit) { + break + } + } +} + +// AddDeposit adds or updates a deposit of a specific depositor on a specific proposal. +// Activates voting period when appropriate and returns true in that case, else returns false. +func (keeper Keeper) AddDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress, depositAmount sdk.Coins) (bool, error) { + // Checks to see if proposal exists + proposal, ok := keeper.GetProposal(ctx, proposalID) + if !ok { + return false, sdkerrors.Wrapf(types.ErrUnknownProposal, "%d", proposalID) + } + + // Check if proposal is still depositable + if (proposal.Status != v1.StatusDepositPeriod) && (proposal.Status != v1.StatusVotingPeriod) { + return false, sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID) + } + + // update the governance module's account coins pool + err := keeper.bankKeeper.SendCoinsFromAccountToModule(ctx, depositorAddr, types.ModuleName, depositAmount) + if err != nil { + return false, err + } + + // Update proposal + proposal.TotalDeposit = sdk.NewCoins(proposal.TotalDeposit...).Add(depositAmount...) + keeper.SetProposal(ctx, proposal) + + // Check if deposit has provided sufficient total funds to transition the proposal into the voting period + activatedVotingPeriod := false + + if proposal.Status == v1.StatusDepositPeriod && sdk.NewCoins(proposal.TotalDeposit...).IsAllGTE(keeper.GetParams(ctx).MinDeposit) { + keeper.ActivateVotingPeriod(ctx, proposal) + + activatedVotingPeriod = true + } + + // Add or update deposit object + deposit, found := keeper.GetDeposit(ctx, proposalID, depositorAddr) + + if found { + deposit.Amount = sdk.NewCoins(deposit.Amount...).Add(depositAmount...) + } else { + deposit = v1.NewDeposit(proposalID, depositorAddr, depositAmount) + } + + // called when deposit has been added to a proposal, however the proposal may not be active + keeper.Hooks().AfterProposalDeposit(ctx, proposalID, depositorAddr) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeProposalDeposit, + sdk.NewAttribute(sdk.AttributeKeyAmount, depositAmount.String()), + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)), + ), + ) + + keeper.SetDeposit(ctx, deposit) + + return activatedVotingPeriod, nil +} + +// RefundAndDeleteDeposits refunds and deletes all the deposits on a specific proposal. +func (keeper Keeper) RefundAndDeleteDeposits(ctx sdk.Context, proposalID uint64) { + store := ctx.KVStore(keeper.storeKey) + + keeper.IterateDeposits(ctx, proposalID, func(deposit v1.Deposit) bool { + depositor := sdk.MustAccAddressFromBech32(deposit.Depositor) + + err := keeper.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, deposit.Amount) + if err != nil { + panic(err) + } + + store.Delete(types.DepositKey(proposalID, depositor)) + return false + }) +} + +// validateInitialDeposit validates if initial deposit is greater than or equal to the minimum +// required at the time of proposal submission. This threshold amount is determined by +// the deposit parameters. Returns nil on success, error otherwise. +func (keeper Keeper) validateInitialDeposit(ctx sdk.Context, initialDeposit sdk.Coins) error { + params := keeper.GetParams(ctx) + minInitialDepositRatio, err := sdk.NewDecFromStr(params.MinInitialDepositRatio) + if err != nil { + return err + } + if minInitialDepositRatio.IsZero() { + return nil + } + minDepositCoins := params.MinDeposit + for i := range minDepositCoins { + minDepositCoins[i].Amount = sdk.NewDecFromInt(minDepositCoins[i].Amount).Mul(minInitialDepositRatio).RoundInt() + } + if !initialDeposit.IsAllGTE(minDepositCoins) { + return sdkerrors.Wrapf(types.ErrMinDepositTooSmall, "was (%s), need (%s)", initialDeposit, minDepositCoins) + } + return nil +} diff --git a/x/gov/keeper/deposit_test.go b/x/gov/keeper/deposit_test.go new file mode 100644 index 00000000..204dc665 --- /dev/null +++ b/x/gov/keeper/deposit_test.go @@ -0,0 +1,213 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +const ( + baseDepositTestAmount = 100 + baseDepositTestPercent = 25 +) + +func TestDeposits(t *testing.T) { + govKeeper, _, bankKeeper, stakingKeeper, _, ctx := setupGovKeeper(t) + trackMockBalances(bankKeeper) + TestAddrs := simtestutil.AddTestAddrsIncremental(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(10000000)) + + tp := TestProposal + proposal, err := govKeeper.SubmitProposal(ctx, tp, "", "title", "description", TestAddrs[0]) + require.NoError(t, err) + proposalID := proposal.Id + + fourStake := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 4))) + fiveStake := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, stakingKeeper.TokensFromConsensusPower(ctx, 5))) + + addr0Initial := bankKeeper.GetAllBalances(ctx, TestAddrs[0]) + addr1Initial := bankKeeper.GetAllBalances(ctx, TestAddrs[1]) + + require.True(t, sdk.NewCoins(proposal.TotalDeposit...).IsEqual(sdk.NewCoins())) + + // Check no deposits at beginning + deposit, found := govKeeper.GetDeposit(ctx, proposalID, TestAddrs[1]) + require.False(t, found) + proposal, ok := govKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + require.Nil(t, proposal.VotingStartTime) + + // Check first deposit + votingStarted, err := govKeeper.AddDeposit(ctx, proposalID, TestAddrs[0], fourStake) + require.NoError(t, err) + require.False(t, votingStarted) + deposit, found = govKeeper.GetDeposit(ctx, proposalID, TestAddrs[0]) + require.True(t, found) + require.Equal(t, fourStake, sdk.NewCoins(deposit.Amount...)) + require.Equal(t, TestAddrs[0].String(), deposit.Depositor) + proposal, ok = govKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + require.Equal(t, fourStake, sdk.NewCoins(proposal.TotalDeposit...)) + require.Equal(t, addr0Initial.Sub(fourStake...), bankKeeper.GetAllBalances(ctx, TestAddrs[0])) + + // Check a second deposit from same address + votingStarted, err = govKeeper.AddDeposit(ctx, proposalID, TestAddrs[0], fiveStake) + require.NoError(t, err) + require.False(t, votingStarted) + deposit, found = govKeeper.GetDeposit(ctx, proposalID, TestAddrs[0]) + require.True(t, found) + require.Equal(t, fourStake.Add(fiveStake...), sdk.NewCoins(deposit.Amount...)) + require.Equal(t, TestAddrs[0].String(), deposit.Depositor) + proposal, ok = govKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + require.Equal(t, fourStake.Add(fiveStake...), sdk.NewCoins(proposal.TotalDeposit...)) + require.Equal(t, addr0Initial.Sub(fourStake...).Sub(fiveStake...), bankKeeper.GetAllBalances(ctx, TestAddrs[0])) + + // Check third deposit from a new address + votingStarted, err = govKeeper.AddDeposit(ctx, proposalID, TestAddrs[1], fourStake) + require.NoError(t, err) + require.True(t, votingStarted) + deposit, found = govKeeper.GetDeposit(ctx, proposalID, TestAddrs[1]) + require.True(t, found) + require.Equal(t, TestAddrs[1].String(), deposit.Depositor) + require.Equal(t, fourStake, sdk.NewCoins(deposit.Amount...)) + proposal, ok = govKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + require.Equal(t, fourStake.Add(fiveStake...).Add(fourStake...), sdk.NewCoins(proposal.TotalDeposit...)) + require.Equal(t, addr1Initial.Sub(fourStake...), bankKeeper.GetAllBalances(ctx, TestAddrs[1])) + + // Check that proposal moved to voting period + proposal, ok = govKeeper.GetProposal(ctx, proposalID) + require.True(t, ok) + require.True(t, proposal.VotingStartTime.Equal(ctx.BlockHeader().Time)) + + // Test deposit iterator + // NOTE order of deposits is determined by the addresses + deposits := govKeeper.GetAllDeposits(ctx) + require.Len(t, deposits, 2) + require.Equal(t, deposits, govKeeper.GetDeposits(ctx, proposalID)) + require.Equal(t, TestAddrs[0].String(), deposits[0].Depositor) + require.Equal(t, fourStake.Add(fiveStake...), sdk.NewCoins(deposits[0].Amount...)) + require.Equal(t, TestAddrs[1].String(), deposits[1].Depositor) + require.Equal(t, fourStake, sdk.NewCoins(deposits[1].Amount...)) + + // Test Refund Deposits + deposit, found = govKeeper.GetDeposit(ctx, proposalID, TestAddrs[1]) + require.True(t, found) + require.Equal(t, fourStake, sdk.NewCoins(deposit.Amount...)) + govKeeper.RefundAndDeleteDeposits(ctx, proposalID) + deposit, found = govKeeper.GetDeposit(ctx, proposalID, TestAddrs[1]) + require.False(t, found) + require.Equal(t, addr0Initial, bankKeeper.GetAllBalances(ctx, TestAddrs[0])) + require.Equal(t, addr1Initial, bankKeeper.GetAllBalances(ctx, TestAddrs[1])) + + // Test delete and burn deposits + proposal, err = govKeeper.SubmitProposal(ctx, tp, "", "title", "description", TestAddrs[0]) + require.NoError(t, err) + proposalID = proposal.Id + _, err = govKeeper.AddDeposit(ctx, proposalID, TestAddrs[0], fourStake) + require.NoError(t, err) + govKeeper.DeleteAndBurnDeposits(ctx, proposalID) + deposits = govKeeper.GetDeposits(ctx, proposalID) + require.Len(t, deposits, 0) + require.Equal(t, addr0Initial.Sub(fourStake...), bankKeeper.GetAllBalances(ctx, TestAddrs[0])) +} + +func TestValidateInitialDeposit(t *testing.T) { + testcases := map[string]struct { + minDeposit sdk.Coins + minInitialDepositPercent int64 + initialDeposit sdk.Coins + + expectError bool + }{ + "min deposit * initial percent == initial deposit: success": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositPercent: baseDepositTestPercent, + initialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100))), + }, + "min deposit * initial percent < initial deposit: success": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositPercent: baseDepositTestPercent, + initialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100+1))), + }, + "min deposit * initial percent > initial deposit: error": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositPercent: baseDepositTestPercent, + initialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100-1))), + + expectError: true, + }, + "min deposit * initial percent == initial deposit (non-base values and denom): success": { + minDeposit: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(56912))), + minInitialDepositPercent: 50, + initialDeposit: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(56912/2+10))), + }, + "min deposit * initial percent == initial deposit but different denoms: error": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositPercent: baseDepositTestPercent, + initialDeposit: sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100))), + + expectError: true, + }, + "min deposit * initial percent == initial deposit (multiple coins): success": { + minDeposit: sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount)), + sdk.NewCoin("uosmo", sdk.NewInt(baseDepositTestAmount*2))), + minInitialDepositPercent: baseDepositTestPercent, + initialDeposit: sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100)), + sdk.NewCoin("uosmo", sdk.NewInt(baseDepositTestAmount*2*baseDepositTestPercent/100)), + ), + }, + "min deposit * initial percent > initial deposit (multiple coins): error": { + minDeposit: sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount)), + sdk.NewCoin("uosmo", sdk.NewInt(baseDepositTestAmount*2))), + minInitialDepositPercent: baseDepositTestPercent, + initialDeposit: sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100)), + sdk.NewCoin("uosmo", sdk.NewInt(baseDepositTestAmount*2*baseDepositTestPercent/100-1)), + ), + + expectError: true, + }, + "min deposit * initial percent < initial deposit (multiple coins - coin not required by min deposit): success": { + minDeposit: sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositPercent: baseDepositTestPercent, + initialDeposit: sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100)), + sdk.NewCoin("uosmo", sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100-1)), + ), + }, + "0 initial percent: success": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositPercent: 0, + initialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount*baseDepositTestPercent/100))), + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + govKeeper, _, _, _, _, ctx := setupGovKeeper(t) + + params := v1.DefaultParams() + params.MinDeposit = tc.minDeposit + params.MinInitialDepositRatio = sdk.NewDec(tc.minInitialDepositPercent).Quo(sdk.NewDec(100)).String() + + govKeeper.SetParams(ctx, params) + + err := govKeeper.ValidateInitialDeposit(ctx, tc.initialDeposit) + + if tc.expectError { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/x/gov/keeper/export_test.go b/x/gov/keeper/export_test.go new file mode 100644 index 00000000..83ec9e27 --- /dev/null +++ b/x/gov/keeper/export_test.go @@ -0,0 +1,9 @@ +package keeper + +import sdk "github.com/cosmos/cosmos-sdk/types" + +// ValidateInitialDeposit is a helper function used only in deposit tests which returns the same +// functionality of validateInitialDeposit private function. +func (k Keeper) ValidateInitialDeposit(ctx sdk.Context, initialDeposit sdk.Coins) error { + return k.validateInitialDeposit(ctx, initialDeposit) +} diff --git a/x/gov/keeper/grpc_query.go b/x/gov/keeper/grpc_query.go new file mode 100644 index 00000000..fb552b7a --- /dev/null +++ b/x/gov/keeper/grpc_query.go @@ -0,0 +1,459 @@ +package keeper + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + v3 "github.com/cosmos/cosmos-sdk/x/gov/migrations/v3" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +var _ v1.QueryServer = Keeper{} + +// Proposal returns proposal details based on ProposalID +func (q Keeper) Proposal(c context.Context, req *v1.QueryProposalRequest) (*v1.QueryProposalResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + if req.ProposalId == 0 { + return nil, status.Error(codes.InvalidArgument, "proposal id can not be 0") + } + + ctx := sdk.UnwrapSDKContext(c) + + proposal, found := q.GetProposal(ctx, req.ProposalId) + if !found { + return nil, status.Errorf(codes.NotFound, "proposal %d doesn't exist", req.ProposalId) + } + + return &v1.QueryProposalResponse{Proposal: &proposal}, nil +} + +// Proposals implements the Query/Proposals gRPC method +func (q Keeper) Proposals(c context.Context, req *v1.QueryProposalsRequest) (*v1.QueryProposalsResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + store := ctx.KVStore(q.storeKey) + proposalStore := prefix.NewStore(store, types.ProposalsKeyPrefix) + + filteredProposals, pageRes, err := query.GenericFilteredPaginate( + q.cdc, + proposalStore, + req.Pagination, + func(key []byte, p *v1.Proposal) (*v1.Proposal, error) { + matchVoter, matchDepositor, matchStatus := true, true, true + + // match status (if supplied/valid) + if v1.ValidProposalStatus(req.ProposalStatus) { + matchStatus = p.Status == req.ProposalStatus + } + + // match voter address (if supplied) + if len(req.Voter) > 0 { + voter, err := sdk.AccAddressFromBech32(req.Voter) + if err != nil { + return nil, err + } + + _, matchVoter = q.GetVote(ctx, p.Id, voter) + } + + // match depositor (if supplied) + if len(req.Depositor) > 0 { + depositor, err := sdk.AccAddressFromBech32(req.Depositor) + if err != nil { + return nil, err + } + _, matchDepositor = q.GetDeposit(ctx, p.Id, depositor) + } + + if matchVoter && matchDepositor && matchStatus { + return p, nil + } + + return nil, nil + }, func() *v1.Proposal { + return &v1.Proposal{} + }) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &v1.QueryProposalsResponse{Proposals: filteredProposals, Pagination: pageRes}, nil +} + +// Vote returns Voted information based on proposalID, voterAddr +func (q Keeper) Vote(c context.Context, req *v1.QueryVoteRequest) (*v1.QueryVoteResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + if req.ProposalId == 0 { + return nil, status.Error(codes.InvalidArgument, "proposal id can not be 0") + } + + if req.Voter == "" { + return nil, status.Error(codes.InvalidArgument, "empty voter address") + } + + ctx := sdk.UnwrapSDKContext(c) + + voter, err := sdk.AccAddressFromBech32(req.Voter) + if err != nil { + return nil, err + } + vote, found := q.GetVote(ctx, req.ProposalId, voter) + if !found { + return nil, status.Errorf(codes.InvalidArgument, + "voter: %v not found for proposal: %v", req.Voter, req.ProposalId) + } + + return &v1.QueryVoteResponse{Vote: &vote}, nil +} + +// Votes returns single proposal's votes +func (q Keeper) Votes(c context.Context, req *v1.QueryVotesRequest) (*v1.QueryVotesResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + if req.ProposalId == 0 { + return nil, status.Error(codes.InvalidArgument, "proposal id can not be 0") + } + + var votes v1.Votes + ctx := sdk.UnwrapSDKContext(c) + + store := ctx.KVStore(q.storeKey) + votesStore := prefix.NewStore(store, types.VotesKey(req.ProposalId)) + + pageRes, err := query.Paginate(votesStore, req.Pagination, func(key []byte, value []byte) error { + var vote v1.Vote + if err := q.cdc.Unmarshal(value, &vote); err != nil { + return err + } + + votes = append(votes, &vote) + return nil + }) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &v1.QueryVotesResponse{Votes: votes, Pagination: pageRes}, nil +} + +// Params queries all params +func (q Keeper) Params(c context.Context, req *v1.QueryParamsRequest) (*v1.QueryParamsResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + ctx := sdk.UnwrapSDKContext(c) + params := q.GetParams(ctx) + + response := &v1.QueryParamsResponse{} + + //nolint:staticcheck + switch req.ParamsType { + case v1.ParamDeposit: + depositParams := v1.NewDepositParams(params.MinDeposit, params.MaxDepositPeriod) + response.DepositParams = &depositParams + + case v1.ParamVoting: + votingParams := v1.NewVotingParams(params.VotingPeriod) + response.VotingParams = &votingParams + + case v1.ParamTallying: + tallyParams := v1.NewTallyParams(params.Quorum, params.Threshold, params.VetoThreshold) + response.TallyParams = &tallyParams + + default: + return nil, status.Errorf(codes.InvalidArgument, + "%s is not a valid parameter type", req.ParamsType) + + } + response.Params = ¶ms + + return response, nil +} + +// Deposit queries single deposit information based on proposalID, depositAddr. +func (q Keeper) Deposit(c context.Context, req *v1.QueryDepositRequest) (*v1.QueryDepositResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + if req.ProposalId == 0 { + return nil, status.Error(codes.InvalidArgument, "proposal id can not be 0") + } + + if req.Depositor == "" { + return nil, status.Error(codes.InvalidArgument, "empty depositor address") + } + + ctx := sdk.UnwrapSDKContext(c) + + depositor, err := sdk.AccAddressFromBech32(req.Depositor) + if err != nil { + return nil, err + } + deposit, found := q.GetDeposit(ctx, req.ProposalId, depositor) + if !found { + return nil, status.Errorf(codes.InvalidArgument, + "depositer: %v not found for proposal: %v", req.Depositor, req.ProposalId) + } + + return &v1.QueryDepositResponse{Deposit: &deposit}, nil +} + +// Deposits returns single proposal's all deposits +func (q Keeper) Deposits(c context.Context, req *v1.QueryDepositsRequest) (*v1.QueryDepositsResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + if req.ProposalId == 0 { + return nil, status.Error(codes.InvalidArgument, "proposal id can not be 0") + } + + var deposits []*v1.Deposit + ctx := sdk.UnwrapSDKContext(c) + + store := ctx.KVStore(q.storeKey) + depositStore := prefix.NewStore(store, types.DepositsKey(req.ProposalId)) + + pageRes, err := query.Paginate(depositStore, req.Pagination, func(key []byte, value []byte) error { + var deposit v1.Deposit + if err := q.cdc.Unmarshal(value, &deposit); err != nil { + return err + } + + deposits = append(deposits, &deposit) + return nil + }) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &v1.QueryDepositsResponse{Deposits: deposits, Pagination: pageRes}, nil +} + +// TallyResult queries the tally of a proposal vote +func (q Keeper) TallyResult(c context.Context, req *v1.QueryTallyResultRequest) (*v1.QueryTallyResultResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + if req.ProposalId == 0 { + return nil, status.Error(codes.InvalidArgument, "proposal id can not be 0") + } + + ctx := sdk.UnwrapSDKContext(c) + + proposal, ok := q.GetProposal(ctx, req.ProposalId) + if !ok { + return nil, status.Errorf(codes.NotFound, "proposal %d doesn't exist", req.ProposalId) + } + + var tallyResult v1.TallyResult + + switch { + case proposal.Status == v1.StatusDepositPeriod: + tallyResult = v1.EmptyTallyResult() + + case proposal.Status == v1.StatusPassed || proposal.Status == v1.StatusRejected || proposal.Status == v1.StatusFailed: + tallyResult = *proposal.FinalTallyResult + + default: + // proposal is in voting period + _, _, tallyResult = q.Tally(ctx, proposal) + } + + return &v1.QueryTallyResultResponse{Tally: &tallyResult}, nil +} + +var _ v1beta1.QueryServer = legacyQueryServer{} + +type legacyQueryServer struct { + keeper *Keeper +} + +// NewLegacyQueryServer returns an implementation of the v1beta1 legacy QueryServer interface. +func NewLegacyQueryServer(k *Keeper) v1beta1.QueryServer { + return &legacyQueryServer{keeper: k} +} + +func (q legacyQueryServer) Proposal(c context.Context, req *v1beta1.QueryProposalRequest) (*v1beta1.QueryProposalResponse, error) { + resp, err := q.keeper.Proposal(c, &v1.QueryProposalRequest{ + ProposalId: req.ProposalId, + }) + if err != nil { + return nil, err + } + + proposal, err := v3.ConvertToLegacyProposal(*resp.Proposal) + if err != nil { + return nil, err + } + + return &v1beta1.QueryProposalResponse{Proposal: proposal}, nil +} + +func (q legacyQueryServer) Proposals(c context.Context, req *v1beta1.QueryProposalsRequest) (*v1beta1.QueryProposalsResponse, error) { + resp, err := q.keeper.Proposals(c, &v1.QueryProposalsRequest{ + ProposalStatus: v1.ProposalStatus(req.ProposalStatus), + Voter: req.Voter, + Depositor: req.Depositor, + Pagination: req.Pagination, + }) + if err != nil { + return nil, err + } + + legacyProposals := make([]v1beta1.Proposal, len(resp.Proposals)) + for idx, proposal := range resp.Proposals { + legacyProposals[idx], err = v3.ConvertToLegacyProposal(*proposal) + if err != nil { + return nil, err + } + } + + return &v1beta1.QueryProposalsResponse{ + Proposals: legacyProposals, + Pagination: resp.Pagination, + }, nil +} + +func (q legacyQueryServer) Vote(c context.Context, req *v1beta1.QueryVoteRequest) (*v1beta1.QueryVoteResponse, error) { + resp, err := q.keeper.Vote(c, &v1.QueryVoteRequest{ + ProposalId: req.ProposalId, + Voter: req.Voter, + }) + if err != nil { + return nil, err + } + + vote, err := v3.ConvertToLegacyVote(*resp.Vote) + if err != nil { + return nil, err + } + + return &v1beta1.QueryVoteResponse{Vote: vote}, nil +} + +func (q legacyQueryServer) Votes(c context.Context, req *v1beta1.QueryVotesRequest) (*v1beta1.QueryVotesResponse, error) { + resp, err := q.keeper.Votes(c, &v1.QueryVotesRequest{ + ProposalId: req.ProposalId, + Pagination: req.Pagination, + }) + if err != nil { + return nil, err + } + + votes := make([]v1beta1.Vote, len(resp.Votes)) + for i, v := range resp.Votes { + votes[i], err = v3.ConvertToLegacyVote(*v) + if err != nil { + return nil, err + } + } + + return &v1beta1.QueryVotesResponse{ + Votes: votes, + Pagination: resp.Pagination, + }, nil +} + +//nolint:staticcheck +func (q legacyQueryServer) Params(c context.Context, req *v1beta1.QueryParamsRequest) (*v1beta1.QueryParamsResponse, error) { + resp, err := q.keeper.Params(c, &v1.QueryParamsRequest{ + ParamsType: req.ParamsType, + }) + if err != nil { + return nil, err + } + + response := &v1beta1.QueryParamsResponse{} + + if resp.DepositParams != nil { + minDeposit := sdk.NewCoins(resp.DepositParams.MinDeposit...) + response.DepositParams = v1beta1.NewDepositParams(minDeposit, *resp.DepositParams.MaxDepositPeriod) + } + + if resp.VotingParams != nil { + response.VotingParams = v1beta1.NewVotingParams(*resp.VotingParams.VotingPeriod) + } + + if resp.TallyParams != nil { + quorum, err := sdk.NewDecFromStr(resp.TallyParams.Quorum) + if err != nil { + return nil, err + } + threshold, err := sdk.NewDecFromStr(resp.TallyParams.Threshold) + if err != nil { + return nil, err + } + vetoThreshold, err := sdk.NewDecFromStr(resp.TallyParams.VetoThreshold) + if err != nil { + return nil, err + } + + response.TallyParams = v1beta1.NewTallyParams(quorum, threshold, vetoThreshold) + } + + return response, nil +} + +func (q legacyQueryServer) Deposit(c context.Context, req *v1beta1.QueryDepositRequest) (*v1beta1.QueryDepositResponse, error) { + resp, err := q.keeper.Deposit(c, &v1.QueryDepositRequest{ + ProposalId: req.ProposalId, + Depositor: req.Depositor, + }) + if err != nil { + return nil, err + } + + deposit := v3.ConvertToLegacyDeposit(resp.Deposit) + return &v1beta1.QueryDepositResponse{Deposit: deposit}, nil +} + +func (q legacyQueryServer) Deposits(c context.Context, req *v1beta1.QueryDepositsRequest) (*v1beta1.QueryDepositsResponse, error) { + resp, err := q.keeper.Deposits(c, &v1.QueryDepositsRequest{ + ProposalId: req.ProposalId, + Pagination: req.Pagination, + }) + if err != nil { + return nil, err + } + deposits := make([]v1beta1.Deposit, len(resp.Deposits)) + for idx, deposit := range resp.Deposits { + deposits[idx] = v3.ConvertToLegacyDeposit(deposit) + } + + return &v1beta1.QueryDepositsResponse{Deposits: deposits, Pagination: resp.Pagination}, nil +} + +func (q legacyQueryServer) TallyResult(c context.Context, req *v1beta1.QueryTallyResultRequest) (*v1beta1.QueryTallyResultResponse, error) { + resp, err := q.keeper.TallyResult(c, &v1.QueryTallyResultRequest{ + ProposalId: req.ProposalId, + }) + if err != nil { + return nil, err + } + + tally, err := v3.ConvertToLegacyTallyResult(resp.Tally) + if err != nil { + return nil, err + } + + return &v1beta1.QueryTallyResultResponse{Tally: tally}, nil +} diff --git a/x/gov/keeper/grpc_query_test.go b/x/gov/keeper/grpc_query_test.go new file mode 100644 index 00000000..6675e479 --- /dev/null +++ b/x/gov/keeper/grpc_query_test.go @@ -0,0 +1,1697 @@ +package keeper_test + +import ( + gocontext "context" + "fmt" + "time" + + "cosmossdk.io/math" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + v3 "github.com/cosmos/cosmos-sdk/x/gov/migrations/v3" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +func (suite *KeeperTestSuite) TestGRPCQueryProposal() { + suite.reset() + ctx, queryClient := suite.ctx, suite.queryClient + + var ( + req *v1.QueryProposalRequest + expProposal v1.Proposal + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1.QueryProposalRequest{} + }, + false, + }, + { + "non existing proposal request", + func() { + req = &v1.QueryProposalRequest{ProposalId: 2} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1.QueryProposalRequest{ProposalId: 0} + }, + false, + }, + { + "valid request", + func() { + req = &v1.QueryProposalRequest{ProposalId: 1} + testProposal := v1beta1.NewTextProposal("Proposal", "testing proposal") + msgContent, err := v1.NewLegacyContent(testProposal, govAcct.String()) + suite.Require().NoError(err) + submittedProposal, err := suite.govKeeper.SubmitProposal(ctx, []sdk.Msg{msgContent}, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NoError(err) + suite.Require().NotEmpty(submittedProposal) + + expProposal = submittedProposal + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + proposalRes, err := queryClient.Proposal(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + // Instead of using MashalJSON, we could compare .String() output too. + // https://github.com/cosmos/cosmos-sdk/issues/10965 + expJSON, err := suite.cdc.MarshalJSON(&expProposal) + suite.Require().NoError(err) + actualJSON, err := suite.cdc.MarshalJSON(proposalRes.Proposal) + suite.Require().NoError(err) + suite.Require().Equal(expJSON, actualJSON) + } else { + suite.Require().Error(err) + suite.Require().Nil(proposalRes) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyGRPCQueryProposal() { + suite.reset() + ctx, queryClient := suite.ctx, suite.legacyQueryClient + + var ( + req *v1beta1.QueryProposalRequest + expProposal v1beta1.Proposal + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1beta1.QueryProposalRequest{} + }, + false, + }, + { + "non existing proposal request", + func() { + req = &v1beta1.QueryProposalRequest{ProposalId: 3} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1beta1.QueryProposalRequest{ProposalId: 0} + }, + false, + }, + { + "valid request", + func() { + req = &v1beta1.QueryProposalRequest{ProposalId: 1} + testProposal := v1beta1.NewTextProposal("Proposal", "testing proposal") + msgContent, err := v1.NewLegacyContent(testProposal, govAcct.String()) + suite.Require().NoError(err) + submittedProposal, err := suite.govKeeper.SubmitProposal(ctx, []sdk.Msg{msgContent}, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NoError(err) + suite.Require().NotEmpty(submittedProposal) + + expProposal, err = v3.ConvertToLegacyProposal(submittedProposal) + suite.Require().NoError(err) + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + proposalRes, err := queryClient.Proposal(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + // Instead of using MashalJSON, we could compare .String() output too. + // https://github.com/cosmos/cosmos-sdk/issues/10965 + expJSON, err := suite.cdc.MarshalJSON(&expProposal) + suite.Require().NoError(err) + actualJSON, err := suite.cdc.MarshalJSON(&proposalRes.Proposal) + suite.Require().NoError(err) + suite.Require().Equal(expJSON, actualJSON) + } else { + suite.Require().Error(err) + suite.Require().Nil(proposalRes) + } + }) + } +} + +func (suite *KeeperTestSuite) TestGRPCQueryProposals() { + suite.reset() + ctx, queryClient, addrs := suite.ctx, suite.queryClient, suite.addrs + + testProposals := []*v1.Proposal{} + + var ( + req *v1.QueryProposalsRequest + expRes *v1.QueryProposalsResponse + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty state request", + func() { + req = &v1.QueryProposalsRequest{} + }, + true, + }, + { + "request proposals with limit 3", + func() { + // create 5 test proposals + for i := 0; i < 5; i++ { + govAddress := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress() + testProposal := []sdk.Msg{ + v1.NewMsgVote(govAddress, uint64(i), v1.OptionYes, ""), + } + proposal, err := suite.govKeeper.SubmitProposal(ctx, testProposal, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NotEmpty(proposal) + suite.Require().NoError(err) + testProposals = append(testProposals, &proposal) + } + + req = &v1.QueryProposalsRequest{ + Pagination: &query.PageRequest{Limit: 3}, + } + + expRes = &v1.QueryProposalsResponse{ + Proposals: testProposals[:3], + } + }, + true, + }, + { + "request 2nd page with limit 4", + func() { + req = &v1.QueryProposalsRequest{ + Pagination: &query.PageRequest{Offset: 3, Limit: 3}, + } + + expRes = &v1.QueryProposalsResponse{ + Proposals: testProposals[3:], + } + }, + true, + }, + { + "request with limit 2 and count true", + func() { + req = &v1.QueryProposalsRequest{ + Pagination: &query.PageRequest{Limit: 2, CountTotal: true}, + } + + expRes = &v1.QueryProposalsResponse{ + Proposals: testProposals[:2], + } + }, + true, + }, + { + "request with filter of status deposit period", + func() { + req = &v1.QueryProposalsRequest{ + ProposalStatus: v1.StatusDepositPeriod, + } + + expRes = &v1.QueryProposalsResponse{ + Proposals: testProposals, + } + }, + true, + }, + { + "request with filter of deposit address", + func() { + depositCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, suite.stakingKeeper.TokensFromConsensusPower(ctx, 20))) + deposit := v1.NewDeposit(testProposals[0].Id, addrs[0], depositCoins) + suite.govKeeper.SetDeposit(ctx, deposit) + + req = &v1.QueryProposalsRequest{ + Depositor: addrs[0].String(), + } + + expRes = &v1.QueryProposalsResponse{ + Proposals: testProposals[:1], + } + }, + true, + }, + { + "request with filter of deposit address", + func() { + testProposals[1].Status = v1.StatusVotingPeriod + suite.govKeeper.SetProposal(ctx, *testProposals[1]) + suite.Require().NoError(suite.govKeeper.AddVote(ctx, testProposals[1].Id, addrs[0], v1.NewNonSplitVoteOption(v1.OptionAbstain), "")) + + req = &v1.QueryProposalsRequest{ + Voter: addrs[0].String(), + } + + expRes = &v1.QueryProposalsResponse{ + Proposals: testProposals[1:2], + } + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + proposals, err := queryClient.Proposals(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + + suite.Require().Len(proposals.GetProposals(), len(expRes.GetProposals())) + for i := 0; i < len(proposals.GetProposals()); i++ { + // Instead of using MashalJSON, we could compare .String() output too. + // https://github.com/cosmos/cosmos-sdk/issues/10965 + expJSON, err := suite.cdc.MarshalJSON(expRes.GetProposals()[i]) + suite.Require().NoError(err) + actualJSON, err := suite.cdc.MarshalJSON(proposals.GetProposals()[i]) + suite.Require().NoError(err) + + suite.Require().Equal(expJSON, actualJSON) + } + + } else { + suite.Require().Error(err) + suite.Require().Nil(proposals) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyGRPCQueryProposals() { + suite.reset() + ctx, queryClient := suite.ctx, suite.legacyQueryClient + + var req *v1beta1.QueryProposalsRequest + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "valid request", + func() { + req = &v1beta1.QueryProposalsRequest{} + testProposal := v1beta1.NewTextProposal("Proposal", "testing proposal") + msgContent, err := v1.NewLegacyContent(testProposal, govAcct.String()) + suite.Require().NoError(err) + submittedProposal, err := suite.govKeeper.SubmitProposal(ctx, []sdk.Msg{msgContent}, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NoError(err) + suite.Require().NotEmpty(submittedProposal) + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + proposalRes, err := queryClient.Proposals(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().NotNil(proposalRes.Proposals) + suite.Require().Equal(len(proposalRes.Proposals), 1) + } else { + suite.Require().Error(err) + suite.Require().Nil(proposalRes) + } + }) + } +} + +func (suite *KeeperTestSuite) TestGRPCQueryVote() { + ctx, queryClient, addrs := suite.ctx, suite.queryClient, suite.addrs + + var ( + req *v1.QueryVoteRequest + expRes *v1.QueryVoteResponse + proposal v1.Proposal + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1.QueryVoteRequest{} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1.QueryVoteRequest{ + ProposalId: 0, + Voter: addrs[0].String(), + } + }, + false, + }, + { + "empty voter request", + func() { + req = &v1.QueryVoteRequest{ + ProposalId: 1, + Voter: "", + } + }, + false, + }, + { + "non existed proposal", + func() { + req = &v1.QueryVoteRequest{ + ProposalId: 3, + Voter: addrs[0].String(), + } + }, + false, + }, + { + "no votes present", + func() { + var err error + proposal, err = suite.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "summary", addrs[0]) + suite.Require().NoError(err) + + req = &v1.QueryVoteRequest{ + ProposalId: proposal.Id, + Voter: addrs[0].String(), + } + + expRes = &v1.QueryVoteResponse{} + }, + false, + }, + { + "valid request", + func() { + proposal.Status = v1.StatusVotingPeriod + suite.govKeeper.SetProposal(ctx, proposal) + suite.Require().NoError(suite.govKeeper.AddVote(ctx, proposal.Id, addrs[0], v1.NewNonSplitVoteOption(v1.OptionAbstain), "")) + + req = &v1.QueryVoteRequest{ + ProposalId: proposal.Id, + Voter: addrs[0].String(), + } + + expRes = &v1.QueryVoteResponse{Vote: &v1.Vote{ProposalId: proposal.Id, Voter: addrs[0].String(), Options: []*v1.WeightedVoteOption{{Option: v1.OptionAbstain, Weight: sdk.MustNewDecFromStr("1.0").String()}}}} + }, + true, + }, + { + "wrong voter id request", + func() { + req = &v1.QueryVoteRequest{ + ProposalId: proposal.Id, + Voter: addrs[1].String(), + } + + expRes = &v1.QueryVoteResponse{} + }, + false, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + vote, err := queryClient.Vote(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expRes, vote) + } else { + suite.Require().Error(err) + suite.Require().Nil(vote) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyGRPCQueryVote() { + ctx, queryClient, addrs := suite.ctx, suite.legacyQueryClient, suite.addrs + + var ( + req *v1beta1.QueryVoteRequest + expRes *v1beta1.QueryVoteResponse + proposal v1.Proposal + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1beta1.QueryVoteRequest{} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1beta1.QueryVoteRequest{ + ProposalId: 0, + Voter: addrs[0].String(), + } + }, + false, + }, + { + "empty voter request", + func() { + req = &v1beta1.QueryVoteRequest{ + ProposalId: 1, + Voter: "", + } + }, + false, + }, + { + "non existed proposal", + func() { + req = &v1beta1.QueryVoteRequest{ + ProposalId: 3, + Voter: addrs[0].String(), + } + }, + false, + }, + { + "no votes present", + func() { + var err error + proposal, err = suite.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "summary", addrs[0]) + suite.Require().NoError(err) + + req = &v1beta1.QueryVoteRequest{ + ProposalId: proposal.Id, + Voter: addrs[0].String(), + } + + expRes = &v1beta1.QueryVoteResponse{} + }, + false, + }, + { + "valid request", + func() { + proposal.Status = v1.StatusVotingPeriod + suite.govKeeper.SetProposal(ctx, proposal) + suite.Require().NoError(suite.govKeeper.AddVote(ctx, proposal.Id, addrs[0], v1.NewNonSplitVoteOption(v1.OptionAbstain), "")) + + req = &v1beta1.QueryVoteRequest{ + ProposalId: proposal.Id, + Voter: addrs[0].String(), + } + + expRes = &v1beta1.QueryVoteResponse{Vote: v1beta1.Vote{ProposalId: proposal.Id, Voter: addrs[0].String(), Options: []v1beta1.WeightedVoteOption{{Option: v1beta1.OptionAbstain, Weight: sdk.MustNewDecFromStr("1.0")}}}} + }, + true, + }, + { + "wrong voter id request", + func() { + req = &v1beta1.QueryVoteRequest{ + ProposalId: proposal.Id, + Voter: addrs[1].String(), + } + + expRes = &v1beta1.QueryVoteResponse{} + }, + false, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + vote, err := queryClient.Vote(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expRes, vote) + } else { + suite.Require().Error(err) + suite.Require().Nil(vote) + } + }) + } +} + +func (suite *KeeperTestSuite) TestGRPCQueryVotes() { + suite.reset() + ctx, queryClient := suite.ctx, suite.queryClient + + addrs := simtestutil.AddTestAddrsIncremental(suite.bankKeeper, suite.stakingKeeper, ctx, 2, sdk.NewInt(30000000)) + + var ( + req *v1.QueryVotesRequest + expRes *v1.QueryVotesResponse + proposal v1.Proposal + votes v1.Votes + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1.QueryVotesRequest{} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1.QueryVotesRequest{ + ProposalId: 0, + } + }, + false, + }, + { + "non existed proposals", + func() { + req = &v1.QueryVotesRequest{ + ProposalId: 2, + } + }, + true, + }, + { + "create a proposal and get votes", + func() { + var err error + proposal, err = suite.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "summary", addrs[0]) + suite.Require().NoError(err) + + req = &v1.QueryVotesRequest{ + ProposalId: proposal.Id, + } + }, + true, + }, + { + "request after adding 2 votes", + func() { + proposal.Status = v1.StatusVotingPeriod + suite.govKeeper.SetProposal(ctx, proposal) + + votes = []*v1.Vote{ + {ProposalId: proposal.Id, Voter: addrs[0].String(), Options: v1.NewNonSplitVoteOption(v1.OptionAbstain)}, + {ProposalId: proposal.Id, Voter: addrs[1].String(), Options: v1.NewNonSplitVoteOption(v1.OptionYes)}, + } + accAddr1, err1 := sdk.AccAddressFromBech32(votes[0].Voter) + accAddr2, err2 := sdk.AccAddressFromBech32(votes[1].Voter) + suite.Require().NoError(err1) + suite.Require().NoError(err2) + suite.Require().NoError(suite.govKeeper.AddVote(ctx, proposal.Id, accAddr1, votes[0].Options, "")) + suite.Require().NoError(suite.govKeeper.AddVote(ctx, proposal.Id, accAddr2, votes[1].Options, "")) + + req = &v1.QueryVotesRequest{ + ProposalId: proposal.Id, + } + + expRes = &v1.QueryVotesResponse{ + Votes: votes, + } + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + votes, err := queryClient.Votes(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expRes.GetVotes(), votes.GetVotes()) + } else { + suite.Require().Error(err) + suite.Require().Nil(votes) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyGRPCQueryVotes() { + suite.reset() + ctx, queryClient := suite.ctx, suite.legacyQueryClient + + addrs := simtestutil.AddTestAddrsIncremental(suite.bankKeeper, suite.stakingKeeper, ctx, 2, sdk.NewInt(30000000)) + + var ( + req *v1beta1.QueryVotesRequest + expRes *v1beta1.QueryVotesResponse + proposal v1.Proposal + votes v1beta1.Votes + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1beta1.QueryVotesRequest{} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1beta1.QueryVotesRequest{ + ProposalId: 0, + } + }, + false, + }, + { + "non existed proposals", + func() { + req = &v1beta1.QueryVotesRequest{ + ProposalId: 2, + } + }, + true, + }, + { + "create a proposal and get votes", + func() { + var err error + proposal, err = suite.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "summary", addrs[0]) + suite.Require().NoError(err) + + req = &v1beta1.QueryVotesRequest{ + ProposalId: proposal.Id, + } + }, + true, + }, + { + "request after adding 2 votes", + func() { + proposal.Status = v1.StatusVotingPeriod + suite.govKeeper.SetProposal(ctx, proposal) + + votes = []v1beta1.Vote{ + {ProposalId: proposal.Id, Voter: addrs[0].String(), Options: v1beta1.NewNonSplitVoteOption(v1beta1.OptionAbstain)}, + {ProposalId: proposal.Id, Voter: addrs[1].String(), Options: v1beta1.NewNonSplitVoteOption(v1beta1.OptionYes)}, + } + accAddr1, err1 := sdk.AccAddressFromBech32(votes[0].Voter) + accAddr2, err2 := sdk.AccAddressFromBech32(votes[1].Voter) + suite.Require().NoError(err1) + suite.Require().NoError(err2) + suite.Require().NoError(suite.govKeeper.AddVote(ctx, proposal.Id, accAddr1, v1.NewNonSplitVoteOption(v1.OptionAbstain), "")) + suite.Require().NoError(suite.govKeeper.AddVote(ctx, proposal.Id, accAddr2, v1.NewNonSplitVoteOption(v1.OptionYes), "")) + + req = &v1beta1.QueryVotesRequest{ + ProposalId: proposal.Id, + } + + expRes = &v1beta1.QueryVotesResponse{ + Votes: votes, + } + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + votes, err := queryClient.Votes(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expRes.GetVotes(), votes.GetVotes()) + } else { + suite.Require().Error(err) + suite.Require().Nil(votes) + } + }) + } +} + +func (suite *KeeperTestSuite) TestGRPCQueryParams() { + queryClient := suite.queryClient + + params := v1.DefaultParams() + + var ( + req *v1.QueryParamsRequest + expRes *v1.QueryParamsResponse + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1.QueryParamsRequest{} + }, + false, + }, + { + "deposit params request", + func() { + req = &v1.QueryParamsRequest{ParamsType: v1.ParamDeposit} + depositParams := v1.NewDepositParams(params.MinDeposit, params.MaxDepositPeriod) + expRes = &v1.QueryParamsResponse{ + DepositParams: &depositParams, + } + }, + true, + }, + { + "voting params request", + func() { + req = &v1.QueryParamsRequest{ParamsType: v1.ParamVoting} + votingParams := v1.NewVotingParams(params.VotingPeriod) + expRes = &v1.QueryParamsResponse{ + VotingParams: &votingParams, + } + }, + true, + }, + { + "tally params request", + func() { + req = &v1.QueryParamsRequest{ParamsType: v1.ParamTallying} + tallyParams := v1.NewTallyParams(params.Quorum, params.Threshold, params.VetoThreshold) + expRes = &v1.QueryParamsResponse{ + TallyParams: &tallyParams, + } + }, + true, + }, + { + "invalid request", + func() { + req = &v1.QueryParamsRequest{ParamsType: "wrongPath"} + expRes = &v1.QueryParamsResponse{} + }, + false, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + params, err := queryClient.Params(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expRes.GetDepositParams(), params.GetDepositParams()) + suite.Require().Equal(expRes.GetVotingParams(), params.GetVotingParams()) + suite.Require().Equal(expRes.GetTallyParams(), params.GetTallyParams()) + } else { + suite.Require().Error(err) + suite.Require().Nil(params) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyGRPCQueryParams() { + queryClient := suite.legacyQueryClient + + var ( + req *v1beta1.QueryParamsRequest + expRes *v1beta1.QueryParamsResponse + ) + + defaultTallyParams := v1beta1.TallyParams{ + Quorum: math.LegacyNewDec(0), + Threshold: math.LegacyNewDec(0), + VetoThreshold: math.LegacyNewDec(0), + } + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1beta1.QueryParamsRequest{} + }, + false, + }, + { + "deposit params request", + func() { + req = &v1beta1.QueryParamsRequest{ParamsType: v1beta1.ParamDeposit} + depositParams := v1beta1.DefaultDepositParams() + expRes = &v1beta1.QueryParamsResponse{ + DepositParams: depositParams, + TallyParams: defaultTallyParams, + } + }, + true, + }, + { + "voting params request", + func() { + req = &v1beta1.QueryParamsRequest{ParamsType: v1beta1.ParamVoting} + votingParams := v1beta1.DefaultVotingParams() + expRes = &v1beta1.QueryParamsResponse{ + VotingParams: votingParams, + TallyParams: defaultTallyParams, + } + }, + true, + }, + { + "tally params request", + func() { + req = &v1beta1.QueryParamsRequest{ParamsType: v1beta1.ParamTallying} + tallyParams := v1beta1.DefaultTallyParams() + expRes = &v1beta1.QueryParamsResponse{ + TallyParams: tallyParams, + } + }, + true, + }, + { + "invalid request", + func() { + req = &v1beta1.QueryParamsRequest{ParamsType: "wrongPath"} + expRes = &v1beta1.QueryParamsResponse{} + }, + false, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + params, err := queryClient.Params(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expRes.GetDepositParams(), params.GetDepositParams()) + suite.Require().Equal(expRes.GetVotingParams(), params.GetVotingParams()) + suite.Require().Equal(expRes.GetTallyParams(), params.GetTallyParams()) + } else { + suite.Require().Error(err) + suite.Require().Nil(params) + } + }) + } +} + +func (suite *KeeperTestSuite) TestGRPCQueryDeposit() { + suite.reset() + ctx, queryClient, addrs := suite.ctx, suite.queryClient, suite.addrs + + var ( + req *v1.QueryDepositRequest + expRes *v1.QueryDepositResponse + proposal v1.Proposal + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1.QueryDepositRequest{} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1.QueryDepositRequest{ + ProposalId: 0, + Depositor: addrs[0].String(), + } + }, + false, + }, + { + "empty deposit address request", + func() { + req = &v1.QueryDepositRequest{ + ProposalId: 1, + Depositor: "", + } + }, + false, + }, + { + "non existed proposal", + func() { + req = &v1.QueryDepositRequest{ + ProposalId: 2, + Depositor: addrs[0].String(), + } + }, + false, + }, + { + "no deposits proposal", + func() { + var err error + proposal, err = suite.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "summary", addrs[0]) + suite.Require().NoError(err) + suite.Require().NotNil(proposal) + + req = &v1.QueryDepositRequest{ + ProposalId: proposal.Id, + Depositor: addrs[0].String(), + } + }, + false, + }, + { + "valid request", + func() { + depositCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, suite.stakingKeeper.TokensFromConsensusPower(ctx, 20))) + deposit := v1.NewDeposit(proposal.Id, addrs[0], depositCoins) + suite.govKeeper.SetDeposit(ctx, deposit) + + req = &v1.QueryDepositRequest{ + ProposalId: proposal.Id, + Depositor: addrs[0].String(), + } + + expRes = &v1.QueryDepositResponse{Deposit: &deposit} + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + deposit, err := queryClient.Deposit(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(deposit.GetDeposit(), expRes.GetDeposit()) + } else { + suite.Require().Error(err) + suite.Require().Nil(expRes) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyGRPCQueryDeposit() { + ctx, queryClient, addrs := suite.ctx, suite.legacyQueryClient, suite.addrs + + var ( + req *v1beta1.QueryDepositRequest + expRes *v1beta1.QueryDepositResponse + proposal v1.Proposal + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1beta1.QueryDepositRequest{} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1beta1.QueryDepositRequest{ + ProposalId: 0, + Depositor: addrs[0].String(), + } + }, + false, + }, + { + "empty deposit address request", + func() { + req = &v1beta1.QueryDepositRequest{ + ProposalId: 1, + Depositor: "", + } + }, + false, + }, + { + "non existed proposal", + func() { + req = &v1beta1.QueryDepositRequest{ + ProposalId: 2, + Depositor: addrs[0].String(), + } + }, + false, + }, + { + "no deposits proposal", + func() { + var err error + proposal, err = suite.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "summary", addrs[0]) + suite.Require().NoError(err) + suite.Require().NotNil(proposal) + + req = &v1beta1.QueryDepositRequest{ + ProposalId: proposal.Id, + Depositor: addrs[0].String(), + } + }, + false, + }, + { + "valid request", + func() { + depositCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, suite.stakingKeeper.TokensFromConsensusPower(ctx, 20))) + deposit := v1beta1.NewDeposit(proposal.Id, addrs[0], depositCoins) + v1deposit := v1.NewDeposit(proposal.Id, addrs[0], depositCoins) + suite.govKeeper.SetDeposit(ctx, v1deposit) + + req = &v1beta1.QueryDepositRequest{ + ProposalId: proposal.Id, + Depositor: addrs[0].String(), + } + + expRes = &v1beta1.QueryDepositResponse{Deposit: deposit} + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + deposit, err := queryClient.Deposit(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(deposit.GetDeposit(), expRes.GetDeposit()) + } else { + suite.Require().Error(err) + suite.Require().Nil(expRes) + } + }) + } +} + +func (suite *KeeperTestSuite) TestGRPCQueryDeposits() { + ctx, queryClient, addrs := suite.ctx, suite.queryClient, suite.addrs + + var ( + req *v1.QueryDepositsRequest + expRes *v1.QueryDepositsResponse + proposal v1.Proposal + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1.QueryDepositsRequest{} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1.QueryDepositsRequest{ + ProposalId: 0, + } + }, + false, + }, + { + "non existed proposal", + func() { + req = &v1.QueryDepositsRequest{ + ProposalId: 2, + } + }, + true, + }, + { + "create a proposal and get deposits", + func() { + var err error + proposal, err = suite.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "summary", addrs[0]) + suite.Require().NoError(err) + + req = &v1.QueryDepositsRequest{ + ProposalId: proposal.Id, + } + }, + true, + }, + { + "get deposits with default limit", + func() { + depositAmount1 := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, suite.stakingKeeper.TokensFromConsensusPower(ctx, 20))) + deposit1 := v1.NewDeposit(proposal.Id, addrs[0], depositAmount1) + suite.govKeeper.SetDeposit(ctx, deposit1) + + depositAmount2 := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, suite.stakingKeeper.TokensFromConsensusPower(ctx, 30))) + deposit2 := v1.NewDeposit(proposal.Id, addrs[1], depositAmount2) + suite.govKeeper.SetDeposit(ctx, deposit2) + + deposits := v1.Deposits{&deposit1, &deposit2} + + req = &v1.QueryDepositsRequest{ + ProposalId: proposal.Id, + } + + expRes = &v1.QueryDepositsResponse{ + Deposits: deposits, + } + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + deposits, err := queryClient.Deposits(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expRes.GetDeposits(), deposits.GetDeposits()) + } else { + suite.Require().Error(err) + suite.Require().Nil(deposits) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyGRPCQueryDeposits() { + ctx, queryClient, addrs := suite.ctx, suite.legacyQueryClient, suite.addrs + + var ( + req *v1beta1.QueryDepositsRequest + expRes *v1beta1.QueryDepositsResponse + proposal v1.Proposal + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1beta1.QueryDepositsRequest{} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1beta1.QueryDepositsRequest{ + ProposalId: 0, + } + }, + false, + }, + { + "non existed proposal", + func() { + req = &v1beta1.QueryDepositsRequest{ + ProposalId: 2, + } + }, + true, + }, + { + "create a proposal and get deposits", + func() { + var err error + proposal, err = suite.govKeeper.SubmitProposal(ctx, TestProposal, "", "test", "summary", addrs[0]) + suite.Require().NoError(err) + + req = &v1beta1.QueryDepositsRequest{ + ProposalId: proposal.Id, + } + }, + true, + }, + { + "get deposits with default limit", + func() { + depositAmount1 := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, suite.stakingKeeper.TokensFromConsensusPower(ctx, 20))) + deposit1 := v1beta1.NewDeposit(proposal.Id, addrs[0], depositAmount1) + v1deposit1 := v1.NewDeposit(proposal.Id, addrs[0], depositAmount1) + suite.govKeeper.SetDeposit(ctx, v1deposit1) + + depositAmount2 := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, suite.stakingKeeper.TokensFromConsensusPower(ctx, 30))) + deposit2 := v1beta1.NewDeposit(proposal.Id, addrs[1], depositAmount2) + v1deposit2 := v1.NewDeposit(proposal.Id, addrs[1], depositAmount2) + suite.govKeeper.SetDeposit(ctx, v1deposit2) + + deposits := v1beta1.Deposits{deposit1, deposit2} + + req = &v1beta1.QueryDepositsRequest{ + ProposalId: proposal.Id, + } + + expRes = &v1beta1.QueryDepositsResponse{ + Deposits: deposits, + } + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + deposits, err := queryClient.Deposits(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expRes.GetDeposits(), deposits.GetDeposits()) + } else { + suite.Require().Error(err) + suite.Require().Nil(deposits) + } + }) + } +} + +func (suite *KeeperTestSuite) TestGRPCQueryTallyResult() { + suite.reset() + ctx, queryClient := suite.ctx, suite.queryClient + + var ( + req *v1.QueryTallyResultRequest + expTally *v1.TallyResult + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1.QueryTallyResultRequest{} + }, + false, + }, + { + "non existing proposal request", + func() { + req = &v1.QueryTallyResultRequest{ProposalId: 2} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1.QueryTallyResultRequest{ProposalId: 0} + }, + false, + }, + { + "valid request with proposal status passed", + func() { + propTime := time.Now() + proposal := v1.Proposal{ + Id: 1, + Status: v1.StatusPassed, + FinalTallyResult: &v1.TallyResult{ + YesCount: "4", + AbstainCount: "1", + NoCount: "0", + NoWithVetoCount: "0", + }, + SubmitTime: &propTime, + VotingStartTime: &propTime, + VotingEndTime: &propTime, + Metadata: "proposal metadata", + } + suite.govKeeper.SetProposal(ctx, proposal) + + req = &v1.QueryTallyResultRequest{ProposalId: proposal.Id} + + expTally = &v1.TallyResult{ + YesCount: "4", + AbstainCount: "1", + NoCount: "0", + NoWithVetoCount: "0", + } + }, + true, + }, + { + "proposal status deposit", + func() { + propTime := time.Now() + proposal := v1.Proposal{ + Id: 1, + Status: v1.StatusDepositPeriod, + SubmitTime: &propTime, + VotingStartTime: &propTime, + VotingEndTime: &propTime, + Metadata: "proposal metadata", + } + suite.govKeeper.SetProposal(ctx, proposal) + + req = &v1.QueryTallyResultRequest{ProposalId: proposal.Id} + + expTally = &v1.TallyResult{ + YesCount: "0", + AbstainCount: "0", + NoCount: "0", + NoWithVetoCount: "0", + } + }, + true, + }, + { + "proposal is in voting period", + func() { + propTime := time.Now() + proposal := v1.Proposal{ + Id: 1, + Status: v1.StatusVotingPeriod, + SubmitTime: &propTime, + VotingStartTime: &propTime, + VotingEndTime: &propTime, + Metadata: "proposal metadata", + } + suite.govKeeper.SetProposal(ctx, proposal) + + req = &v1.QueryTallyResultRequest{ProposalId: proposal.Id} + + expTally = &v1.TallyResult{ + YesCount: "0", + AbstainCount: "0", + NoCount: "0", + NoWithVetoCount: "0", + } + }, + true, + }, + { + "proposal status failed", + func() { + propTime := time.Now() + proposal := v1.Proposal{ + Id: 1, + Status: v1.StatusFailed, + FinalTallyResult: &v1.TallyResult{ + YesCount: "4", + AbstainCount: "1", + NoCount: "0", + NoWithVetoCount: "0", + }, + SubmitTime: &propTime, + VotingStartTime: &propTime, + VotingEndTime: &propTime, + Metadata: "proposal metadata", + } + suite.govKeeper.SetProposal(suite.ctx, proposal) + + req = &v1.QueryTallyResultRequest{ProposalId: proposal.Id} + + expTally = &v1.TallyResult{ + YesCount: "4", + AbstainCount: "1", + NoCount: "0", + NoWithVetoCount: "0", + } + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + tallyRes, err := queryClient.TallyResult(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().NotEmpty(tallyRes.Tally.String()) + suite.Require().Equal(expTally.String(), tallyRes.Tally.String()) + } else { + suite.Require().Error(err) + suite.Require().Nil(tallyRes) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyGRPCQueryTallyResult() { + suite.reset() + ctx, queryClient := suite.ctx, suite.legacyQueryClient + + var ( + req *v1beta1.QueryTallyResultRequest + expTally *v1beta1.TallyResult + ) + + testCases := []struct { + msg string + malleate func() + expPass bool + }{ + { + "empty request", + func() { + req = &v1beta1.QueryTallyResultRequest{} + }, + false, + }, + { + "non existing proposal request", + func() { + req = &v1beta1.QueryTallyResultRequest{ProposalId: 2} + }, + false, + }, + { + "zero proposal id request", + func() { + req = &v1beta1.QueryTallyResultRequest{ProposalId: 0} + }, + false, + }, + { + "valid request with proposal status passed", + func() { + propTime := time.Now() + proposal := v1.Proposal{ + Id: 1, + Status: v1.StatusPassed, + FinalTallyResult: &v1.TallyResult{ + YesCount: "4", + AbstainCount: "1", + NoCount: "0", + NoWithVetoCount: "0", + }, + SubmitTime: &propTime, + VotingStartTime: &propTime, + VotingEndTime: &propTime, + Metadata: "proposal metadata", + } + suite.govKeeper.SetProposal(ctx, proposal) + + req = &v1beta1.QueryTallyResultRequest{ProposalId: proposal.Id} + + expTally = &v1beta1.TallyResult{ + Yes: math.NewInt(4), + Abstain: math.NewInt(1), + No: math.NewInt(0), + NoWithVeto: math.NewInt(0), + } + }, + true, + }, + { + "proposal status deposit", + func() { + propTime := time.Now() + proposal := v1.Proposal{ + Id: 1, + Status: v1.StatusDepositPeriod, + SubmitTime: &propTime, + VotingStartTime: &propTime, + VotingEndTime: &propTime, + Metadata: "proposal metadata", + } + suite.govKeeper.SetProposal(ctx, proposal) + + req = &v1beta1.QueryTallyResultRequest{ProposalId: proposal.Id} + + expTally = &v1beta1.TallyResult{ + Yes: math.NewInt(0), + Abstain: math.NewInt(0), + No: math.NewInt(0), + NoWithVeto: math.NewInt(0), + } + }, + true, + }, + { + "proposal is in voting period", + func() { + propTime := time.Now() + proposal := v1.Proposal{ + Id: 1, + Status: v1.StatusVotingPeriod, + SubmitTime: &propTime, + VotingStartTime: &propTime, + VotingEndTime: &propTime, + Metadata: "proposal metadata", + } + suite.govKeeper.SetProposal(ctx, proposal) + + req = &v1beta1.QueryTallyResultRequest{ProposalId: proposal.Id} + + expTally = &v1beta1.TallyResult{ + Yes: math.NewInt(0), + Abstain: math.NewInt(0), + No: math.NewInt(0), + NoWithVeto: math.NewInt(0), + } + }, + true, + }, + { + "proposal status failed", + func() { + propTime := time.Now() + proposal := v1.Proposal{ + Id: 1, + Status: v1.StatusFailed, + FinalTallyResult: &v1.TallyResult{ + YesCount: "4", + AbstainCount: "1", + NoCount: "0", + NoWithVetoCount: "0", + }, + SubmitTime: &propTime, + VotingStartTime: &propTime, + VotingEndTime: &propTime, + Metadata: "proposal metadata", + } + suite.govKeeper.SetProposal(suite.ctx, proposal) + + req = &v1beta1.QueryTallyResultRequest{ProposalId: proposal.Id} + + expTally = &v1beta1.TallyResult{ + Yes: math.NewInt(4), + Abstain: math.NewInt(1), + No: math.NewInt(0), + NoWithVeto: math.NewInt(0), + } + }, + true, + }, + } + + for _, testCase := range testCases { + suite.Run(fmt.Sprintf("Case %s", testCase.msg), func() { + testCase.malleate() + + tallyRes, err := queryClient.TallyResult(gocontext.Background(), req) + + if testCase.expPass { + suite.Require().NoError(err) + suite.Require().NotEmpty(tallyRes.Tally.String()) + suite.Require().Equal(expTally.String(), tallyRes.Tally.String()) + } else { + suite.Require().Error(err) + suite.Require().Nil(tallyRes) + } + }) + } +} diff --git a/x/gov/keeper/hooks_test.go b/x/gov/keeper/hooks_test.go new file mode 100644 index 00000000..188eb204 --- /dev/null +++ b/x/gov/keeper/hooks_test.go @@ -0,0 +1,95 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/atomone-hub/atomone/x/gov" + "github.com/atomone-hub/atomone/x/gov/keeper" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +var _ types.GovHooks = &MockGovHooksReceiver{} + +// GovHooks event hooks for governance proposal object (noalias) +type MockGovHooksReceiver struct { + AfterProposalSubmissionValid bool + AfterProposalDepositValid bool + AfterProposalVoteValid bool + AfterProposalFailedMinDepositValid bool + AfterProposalVotingPeriodEndedValid bool +} + +func (h *MockGovHooksReceiver) AfterProposalSubmission(ctx sdk.Context, proposalID uint64) { + h.AfterProposalSubmissionValid = true +} + +func (h *MockGovHooksReceiver) AfterProposalDeposit(ctx sdk.Context, proposalID uint64, depositorAddr sdk.AccAddress) { + h.AfterProposalDepositValid = true +} + +func (h *MockGovHooksReceiver) AfterProposalVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { + h.AfterProposalVoteValid = true +} + +func (h *MockGovHooksReceiver) AfterProposalFailedMinDeposit(ctx sdk.Context, proposalID uint64) { + h.AfterProposalFailedMinDepositValid = true +} + +func (h *MockGovHooksReceiver) AfterProposalVotingPeriodEnded(ctx sdk.Context, proposalID uint64) { + h.AfterProposalVotingPeriodEndedValid = true +} + +func TestHooks(t *testing.T) { + minDeposit := v1.DefaultParams().MinDeposit + govKeeper, _, bankKeeper, stakingKeeper, _, ctx := setupGovKeeper(t) + addrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 1, minDeposit[0].Amount) + + govHooksReceiver := MockGovHooksReceiver{} + + keeper.UnsafeSetHooks( + govKeeper, types.NewMultiGovHooks(&govHooksReceiver), + ) + + require.False(t, govHooksReceiver.AfterProposalSubmissionValid) + require.False(t, govHooksReceiver.AfterProposalDepositValid) + require.False(t, govHooksReceiver.AfterProposalVoteValid) + require.False(t, govHooksReceiver.AfterProposalFailedMinDepositValid) + require.False(t, govHooksReceiver.AfterProposalVotingPeriodEndedValid) + + tp := TestProposal + _, err := govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + require.True(t, govHooksReceiver.AfterProposalSubmissionValid) + + newHeader := ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(*govKeeper.GetParams(ctx).MaxDepositPeriod).Add(time.Duration(1) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + gov.EndBlocker(ctx, govKeeper) + + require.True(t, govHooksReceiver.AfterProposalFailedMinDepositValid) + + p2, err := govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + + activated, err := govKeeper.AddDeposit(ctx, p2.Id, addrs[0], minDeposit) + require.True(t, activated) + require.NoError(t, err) + require.True(t, govHooksReceiver.AfterProposalDepositValid) + + err = govKeeper.AddVote(ctx, p2.Id, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), "") + require.NoError(t, err) + require.True(t, govHooksReceiver.AfterProposalVoteValid) + + newHeader = ctx.BlockHeader() + newHeader.Time = ctx.BlockHeader().Time.Add(*govKeeper.GetParams(ctx).VotingPeriod).Add(time.Duration(1) * time.Second) + ctx = ctx.WithBlockHeader(newHeader) + gov.EndBlocker(ctx, govKeeper) + require.True(t, govHooksReceiver.AfterProposalVotingPeriodEndedValid) +} diff --git a/x/gov/keeper/internal_test.go b/x/gov/keeper/internal_test.go new file mode 100644 index 00000000..fe01d08e --- /dev/null +++ b/x/gov/keeper/internal_test.go @@ -0,0 +1,10 @@ +package keeper + +import "github.com/cosmos/cosmos-sdk/x/gov/types" + +// UnsafeSetHooks updates the gov keeper's hooks, overriding any potential +// pre-existing hooks. +// WARNING: this function should only be used in tests. +func UnsafeSetHooks(k *Keeper, h types.GovHooks) { + k.hooks = h +} diff --git a/x/gov/keeper/invariants.go b/x/gov/keeper/invariants.go new file mode 100644 index 00000000..69d7ad6f --- /dev/null +++ b/x/gov/keeper/invariants.go @@ -0,0 +1,48 @@ +package keeper + +// DONTCOVER + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// RegisterInvariants registers all governance invariants +func RegisterInvariants(ir sdk.InvariantRegistry, keeper *Keeper, bk types.BankKeeper) { + ir.RegisterRoute(types.ModuleName, "module-account", ModuleAccountInvariant(keeper, bk)) +} + +// AllInvariants runs all invariants of the governance module +func AllInvariants(keeper *Keeper, bk types.BankKeeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + return ModuleAccountInvariant(keeper, bk)(ctx) + } +} + +// ModuleAccountInvariant checks that the module account coins reflects the sum of +// deposit amounts held on store. +func ModuleAccountInvariant(keeper *Keeper, bk types.BankKeeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var expectedDeposits sdk.Coins + + keeper.IterateAllDeposits(ctx, func(deposit v1.Deposit) bool { + expectedDeposits = expectedDeposits.Add(deposit.Amount...) + return false + }) + + macc := keeper.GetGovernanceAccount(ctx) + balances := bk.GetAllBalances(ctx, macc.GetAddress()) + + // Require that the deposit balances are <= than the x/gov module's total + // balances. We use the <= operator since external funds can be sent to x/gov + // module's account and so the balance can be larger. + broken := !balances.IsAllGTE(expectedDeposits) + + return sdk.FormatInvariant(types.ModuleName, "deposits", + fmt.Sprintf("\tgov ModuleAccount coins: %s\n\tsum of deposit amounts: %s\n", + balances, expectedDeposits)), broken + } +} diff --git a/x/gov/keeper/keeper.go b/x/gov/keeper/keeper.go new file mode 100644 index 00000000..ecd8c3de --- /dev/null +++ b/x/gov/keeper/keeper.go @@ -0,0 +1,229 @@ +package keeper + +import ( + "fmt" + "time" + + "github.com/cometbft/cometbft/libs/log" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +// Keeper defines the governance module Keeper +type Keeper struct { + authKeeper types.AccountKeeper + bankKeeper types.BankKeeper + + // The reference to the DelegationSet and ValidatorSet to get information about validators and delegators + sk types.StakingKeeper + + // GovHooks + hooks types.GovHooks + + // The (unexposed) keys used to access the stores from the Context. + storeKey storetypes.StoreKey + + // The codec for binary encoding/decoding. + cdc codec.BinaryCodec + + // Legacy Proposal router + legacyRouter v1beta1.Router + + // Msg server router + router *baseapp.MsgServiceRouter + + config types.Config + + // the address capable of executing a MsgUpdateParams message. Typically, this + // should be the x/gov module account. + authority string +} + +// GetAuthority returns the x/gov module's authority. +func (k Keeper) GetAuthority() string { + return k.authority +} + +// NewKeeper returns a governance keeper. It handles: +// - submitting governance proposals +// - depositing funds into proposals, and activating upon sufficient funds being deposited +// - users voting on proposals, with weight proportional to stake in the system +// - and tallying the result of the vote. +// +// CONTRACT: the parameter Subspace must have the param key table already initialized +func NewKeeper( + cdc codec.BinaryCodec, key storetypes.StoreKey, authKeeper types.AccountKeeper, + bankKeeper types.BankKeeper, sk types.StakingKeeper, + router *baseapp.MsgServiceRouter, config types.Config, authority string, +) *Keeper { + // ensure governance module account is set + if addr := authKeeper.GetModuleAddress(types.ModuleName); addr == nil { + panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) + } + + if _, err := sdk.AccAddressFromBech32(authority); err != nil { + panic(fmt.Sprintf("invalid authority address: %s", authority)) + } + + // If MaxMetadataLen not set by app developer, set to default value. + if config.MaxMetadataLen == 0 { + config.MaxMetadataLen = types.DefaultConfig().MaxMetadataLen + } + + return &Keeper{ + storeKey: key, + authKeeper: authKeeper, + bankKeeper: bankKeeper, + sk: sk, + cdc: cdc, + router: router, + config: config, + authority: authority, + } +} + +// Hooks gets the hooks for governance *Keeper { +func (keeper *Keeper) Hooks() types.GovHooks { + if keeper.hooks == nil { + // return a no-op implementation if no hooks are set + return types.MultiGovHooks{} + } + + return keeper.hooks +} + +// SetHooks sets the hooks for governance +func (keeper *Keeper) SetHooks(gh types.GovHooks) *Keeper { + if keeper.hooks != nil { + panic("cannot set governance hooks twice") + } + + keeper.hooks = gh + + return keeper +} + +// SetLegacyRouter sets the legacy router for governance +func (keeper *Keeper) SetLegacyRouter(router v1beta1.Router) { + // It is vital to seal the governance proposal router here as to not allow + // further handlers to be registered after the keeper is created since this + // could create invalid or non-deterministic behavior. + router.Seal() + keeper.legacyRouter = router +} + +// Logger returns a module-specific logger. +func (keeper Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", "x/"+types.ModuleName) +} + +// Router returns the gov keeper's router +func (keeper Keeper) Router() *baseapp.MsgServiceRouter { + return keeper.router +} + +// LegacyRouter returns the gov keeper's legacy router +func (keeper Keeper) LegacyRouter() v1beta1.Router { + return keeper.legacyRouter +} + +// GetGovernanceAccount returns the governance ModuleAccount +func (keeper Keeper) GetGovernanceAccount(ctx sdk.Context) authtypes.ModuleAccountI { + return keeper.authKeeper.GetModuleAccount(ctx, types.ModuleName) +} + +// ProposalQueues + +// InsertActiveProposalQueue inserts a proposalID into the active proposal queue at endTime +func (keeper Keeper) InsertActiveProposalQueue(ctx sdk.Context, proposalID uint64, endTime time.Time) { + store := ctx.KVStore(keeper.storeKey) + bz := types.GetProposalIDBytes(proposalID) + store.Set(types.ActiveProposalQueueKey(proposalID, endTime), bz) +} + +// RemoveFromActiveProposalQueue removes a proposalID from the Active Proposal Queue +func (keeper Keeper) RemoveFromActiveProposalQueue(ctx sdk.Context, proposalID uint64, endTime time.Time) { + store := ctx.KVStore(keeper.storeKey) + store.Delete(types.ActiveProposalQueueKey(proposalID, endTime)) +} + +// InsertInactiveProposalQueue inserts a proposalID into the inactive proposal queue at endTime +func (keeper Keeper) InsertInactiveProposalQueue(ctx sdk.Context, proposalID uint64, endTime time.Time) { + store := ctx.KVStore(keeper.storeKey) + bz := types.GetProposalIDBytes(proposalID) + store.Set(types.InactiveProposalQueueKey(proposalID, endTime), bz) +} + +// RemoveFromInactiveProposalQueue removes a proposalID from the Inactive Proposal Queue +func (keeper Keeper) RemoveFromInactiveProposalQueue(ctx sdk.Context, proposalID uint64, endTime time.Time) { + store := ctx.KVStore(keeper.storeKey) + store.Delete(types.InactiveProposalQueueKey(proposalID, endTime)) +} + +// Iterators + +// IterateActiveProposalsQueue iterates over the proposals in the active proposal queue +// and performs a callback function +func (keeper Keeper) IterateActiveProposalsQueue(ctx sdk.Context, endTime time.Time, cb func(proposal v1.Proposal) (stop bool)) { + iterator := keeper.ActiveProposalQueueIterator(ctx, endTime) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + proposalID, _ := types.SplitActiveProposalQueueKey(iterator.Key()) + proposal, found := keeper.GetProposal(ctx, proposalID) + if !found { + panic(fmt.Sprintf("proposal %d does not exist", proposalID)) + } + + if cb(proposal) { + break + } + } +} + +// IterateInactiveProposalsQueue iterates over the proposals in the inactive proposal queue +// and performs a callback function +func (keeper Keeper) IterateInactiveProposalsQueue(ctx sdk.Context, endTime time.Time, cb func(proposal v1.Proposal) (stop bool)) { + iterator := keeper.InactiveProposalQueueIterator(ctx, endTime) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + proposalID, _ := types.SplitInactiveProposalQueueKey(iterator.Key()) + proposal, found := keeper.GetProposal(ctx, proposalID) + if !found { + panic(fmt.Sprintf("proposal %d does not exist", proposalID)) + } + + if cb(proposal) { + break + } + } +} + +// ActiveProposalQueueIterator returns an sdk.Iterator for all the proposals in the Active Queue that expire by endTime +func (keeper Keeper) ActiveProposalQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { + store := ctx.KVStore(keeper.storeKey) + return store.Iterator(types.ActiveProposalQueuePrefix, sdk.PrefixEndBytes(types.ActiveProposalByTimeKey(endTime))) +} + +// InactiveProposalQueueIterator returns an sdk.Iterator for all the proposals in the Inactive Queue that expire by endTime +func (keeper Keeper) InactiveProposalQueueIterator(ctx sdk.Context, endTime time.Time) sdk.Iterator { + store := ctx.KVStore(keeper.storeKey) + return store.Iterator(types.InactiveProposalQueuePrefix, sdk.PrefixEndBytes(types.InactiveProposalByTimeKey(endTime))) +} + +// assertMetadataLength returns an error if given metadata length +// is greater than a pre-defined MaxMetadataLen. +func (keeper Keeper) assertMetadataLength(metadata string) error { + if metadata != "" && uint64(len(metadata)) > keeper.config.MaxMetadataLen { + return types.ErrMetadataTooLong.Wrapf("got metadata with length %d", len(metadata)) + } + return nil +} diff --git a/x/gov/keeper/keeper_test.go b/x/gov/keeper/keeper_test.go new file mode 100644 index 00000000..bf5c30a0 --- /dev/null +++ b/x/gov/keeper/keeper_test.go @@ -0,0 +1,125 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/atomone-hub/atomone/x/gov/keeper" + govtestutil "github.com/atomone-hub/atomone/x/gov/testutil" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" +) + +type KeeperTestSuite struct { + suite.Suite + + cdc codec.Codec + ctx sdk.Context + govKeeper *keeper.Keeper + acctKeeper *govtestutil.MockAccountKeeper + bankKeeper *govtestutil.MockBankKeeper + stakingKeeper *govtestutil.MockStakingKeeper + queryClient v1.QueryClient + legacyQueryClient v1beta1.QueryClient + addrs []sdk.AccAddress + msgSrvr v1.MsgServer + legacyMsgSrvr v1beta1.MsgServer +} + +func (suite *KeeperTestSuite) SetupSuite() { + suite.reset() +} + +func (suite *KeeperTestSuite) reset() { + govKeeper, acctKeeper, bankKeeper, stakingKeeper, encCfg, ctx := setupGovKeeper(suite.T()) + + // Populate the gov account with some coins, as the TestProposal we have + // is a MsgSend from the gov account. + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100000))) + err := bankKeeper.MintCoins(suite.ctx, minttypes.ModuleName, coins) + suite.NoError(err) + err = bankKeeper.SendCoinsFromModuleToModule(ctx, minttypes.ModuleName, types.ModuleName, coins) + suite.NoError(err) + + queryHelper := baseapp.NewQueryServerTestHelper(ctx, encCfg.InterfaceRegistry) + v1.RegisterQueryServer(queryHelper, govKeeper) + legacyQueryHelper := baseapp.NewQueryServerTestHelper(ctx, encCfg.InterfaceRegistry) + v1beta1.RegisterQueryServer(legacyQueryHelper, keeper.NewLegacyQueryServer(govKeeper)) + queryClient := v1.NewQueryClient(queryHelper) + legacyQueryClient := v1beta1.NewQueryClient(legacyQueryHelper) + + suite.ctx = ctx + suite.govKeeper = govKeeper + suite.acctKeeper = acctKeeper + suite.bankKeeper = bankKeeper + suite.stakingKeeper = stakingKeeper + suite.cdc = encCfg.Codec + suite.queryClient = queryClient + suite.legacyQueryClient = legacyQueryClient + suite.msgSrvr = keeper.NewMsgServerImpl(suite.govKeeper) + + suite.legacyMsgSrvr = keeper.NewLegacyMsgServerImpl(govAcct.String(), suite.msgSrvr) + suite.addrs = simtestutil.AddTestAddrsIncremental(bankKeeper, stakingKeeper, ctx, 3, sdk.NewInt(30000000)) +} + +func TestIncrementProposalNumber(t *testing.T) { + govKeeper, _, _, _, _, ctx := setupGovKeeper(t) + + tp := TestProposal + _, err := govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + _, err = govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + _, err = govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + _, err = govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + _, err = govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + proposal6, err := govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + + require.Equal(t, uint64(6), proposal6.Id) +} + +func TestProposalQueues(t *testing.T) { + govKeeper, _, _, _, _, ctx := setupGovKeeper(t) + + // create test proposals + tp := TestProposal + proposal, err := govKeeper.SubmitProposal(ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + + inactiveIterator := govKeeper.InactiveProposalQueueIterator(ctx, *proposal.DepositEndTime) + require.True(t, inactiveIterator.Valid()) + + proposalID := types.GetProposalIDFromBytes(inactiveIterator.Value()) + require.Equal(t, proposalID, proposal.Id) + inactiveIterator.Close() + + govKeeper.ActivateVotingPeriod(ctx, proposal) + + proposal, ok := govKeeper.GetProposal(ctx, proposal.Id) + require.True(t, ok) + + activeIterator := govKeeper.ActiveProposalQueueIterator(ctx, *proposal.VotingEndTime) + require.True(t, activeIterator.Valid()) + + proposalID, _ = types.SplitActiveProposalQueueKey(activeIterator.Key()) + require.Equal(t, proposalID, proposal.Id) + + activeIterator.Close() +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} diff --git a/x/gov/keeper/migrations.go b/x/gov/keeper/migrations.go new file mode 100644 index 00000000..d59ce554 --- /dev/null +++ b/x/gov/keeper/migrations.go @@ -0,0 +1,19 @@ +package keeper + +import ( + "github.com/atomone-hub/atomone/x/gov/exported" +) + +// Migrator is a struct for handling in-place store migrations. +type Migrator struct { + keeper *Keeper + legacySubspace exported.ParamSubspace +} + +// NewMigrator returns a new Migrator. +func NewMigrator(keeper *Keeper, legacySubspace exported.ParamSubspace) Migrator { + return Migrator{ + keeper: keeper, + legacySubspace: legacySubspace, + } +} diff --git a/x/gov/keeper/msg_server.go b/x/gov/keeper/msg_server.go new file mode 100644 index 00000000..00a7186a --- /dev/null +++ b/x/gov/keeper/msg_server.go @@ -0,0 +1,256 @@ +package keeper + +import ( + "context" + "fmt" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +type msgServer struct { + *Keeper +} + +// NewMsgServerImpl returns an implementation of the gov MsgServer interface +// for the provided Keeper. +func NewMsgServerImpl(keeper *Keeper) v1.MsgServer { + return &msgServer{Keeper: keeper} +} + +var _ v1.MsgServer = msgServer{} + +// SubmitProposal implements the MsgServer.SubmitProposal method. +func (k msgServer) SubmitProposal(goCtx context.Context, msg *v1.MsgSubmitProposal) (*v1.MsgSubmitProposalResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + initialDeposit := msg.GetInitialDeposit() + + if err := k.validateInitialDeposit(ctx, initialDeposit); err != nil { + return nil, err + } + + proposalMsgs, err := msg.GetMsgs() + if err != nil { + return nil, err + } + + proposer, err := sdk.AccAddressFromBech32(msg.GetProposer()) + if err != nil { + return nil, err + } + + proposal, err := k.Keeper.SubmitProposal(ctx, proposalMsgs, msg.Metadata, msg.Title, msg.Summary, proposer) + if err != nil { + return nil, err + } + + bytes, err := proposal.Marshal() + if err != nil { + return nil, err + } + + // ref: https://github.com/cosmos/cosmos-sdk/issues/9683 + ctx.GasMeter().ConsumeGas( + 3*ctx.KVGasConfig().WriteCostPerByte*uint64(len(bytes)), + "submit proposal", + ) + + votingStarted, err := k.Keeper.AddDeposit(ctx, proposal.Id, proposer, msg.GetInitialDeposit()) + if err != nil { + return nil, err + } + + if votingStarted { + ctx.EventManager().EmitEvent( + sdk.NewEvent(govtypes.EventTypeSubmitProposal, + sdk.NewAttribute(govtypes.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", proposal.Id)), + ), + ) + } + + return &v1.MsgSubmitProposalResponse{ + ProposalId: proposal.Id, + }, nil +} + +// ExecLegacyContent implements the MsgServer.ExecLegacyContent method. +func (k msgServer) ExecLegacyContent(goCtx context.Context, msg *v1.MsgExecLegacyContent) (*v1.MsgExecLegacyContentResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + govAcct := k.GetGovernanceAccount(ctx).GetAddress().String() + if govAcct != msg.Authority { + return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "expected %s got %s", govAcct, msg.Authority) + } + + content, err := v1.LegacyContentFromMessage(msg) + if err != nil { + return nil, errors.Wrapf(govtypes.ErrInvalidProposalContent, "%+v", err) + } + + // Ensure that the content has a respective handler + if !k.Keeper.legacyRouter.HasRoute(content.ProposalRoute()) { + return nil, errors.Wrap(govtypes.ErrNoProposalHandlerExists, content.ProposalRoute()) + } + + handler := k.Keeper.legacyRouter.GetRoute(content.ProposalRoute()) + if err := handler(ctx, content); err != nil { + return nil, errors.Wrapf(govtypes.ErrInvalidProposalContent, "failed to run legacy handler %s, %+v", content.ProposalRoute(), err) + } + + return &v1.MsgExecLegacyContentResponse{}, nil +} + +// Vote implements the MsgServer.Vote method. +func (k msgServer) Vote(goCtx context.Context, msg *v1.MsgVote) (*v1.MsgVoteResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + accAddr, err := sdk.AccAddressFromBech32(msg.Voter) + if err != nil { + return nil, err + } + err = k.Keeper.AddVote(ctx, msg.ProposalId, accAddr, v1.NewNonSplitVoteOption(msg.Option), msg.Metadata) + if err != nil { + return nil, err + } + + return &v1.MsgVoteResponse{}, nil +} + +// VoteWeighted implements the MsgServer.VoteWeighted method. +func (k msgServer) VoteWeighted(goCtx context.Context, msg *v1.MsgVoteWeighted) (*v1.MsgVoteWeightedResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + accAddr, accErr := sdk.AccAddressFromBech32(msg.Voter) + if accErr != nil { + return nil, accErr + } + err := k.Keeper.AddVote(ctx, msg.ProposalId, accAddr, msg.Options, msg.Metadata) + if err != nil { + return nil, err + } + + return &v1.MsgVoteWeightedResponse{}, nil +} + +// Deposit implements the MsgServer.Deposit method. +func (k msgServer) Deposit(goCtx context.Context, msg *v1.MsgDeposit) (*v1.MsgDepositResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + accAddr, err := sdk.AccAddressFromBech32(msg.Depositor) + if err != nil { + return nil, err + } + votingStarted, err := k.Keeper.AddDeposit(ctx, msg.ProposalId, accAddr, msg.Amount) + if err != nil { + return nil, err + } + + if votingStarted { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + govtypes.EventTypeProposalDeposit, + sdk.NewAttribute(govtypes.AttributeKeyVotingPeriodStart, fmt.Sprintf("%d", msg.ProposalId)), + ), + ) + } + + return &v1.MsgDepositResponse{}, nil +} + +// UpdateParams implements the MsgServer.UpdateParams method. +func (k msgServer) UpdateParams(goCtx context.Context, msg *v1.MsgUpdateParams) (*v1.MsgUpdateParamsResponse, error) { + if k.authority != msg.Authority { + return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.authority, msg.Authority) + } + + ctx := sdk.UnwrapSDKContext(goCtx) + if err := k.SetParams(ctx, msg.Params); err != nil { + return nil, err + } + + return &v1.MsgUpdateParamsResponse{}, nil +} + +type legacyMsgServer struct { + govAcct string + server v1.MsgServer +} + +// NewLegacyMsgServerImpl returns an implementation of the v1beta1 legacy MsgServer interface. It wraps around +// the current MsgServer +func NewLegacyMsgServerImpl(govAcct string, v1Server v1.MsgServer) v1beta1.MsgServer { + return &legacyMsgServer{govAcct: govAcct, server: v1Server} +} + +var _ v1beta1.MsgServer = legacyMsgServer{} + +func (k legacyMsgServer) SubmitProposal(goCtx context.Context, msg *v1beta1.MsgSubmitProposal) (*v1beta1.MsgSubmitProposalResponse, error) { + contentMsg, err := v1.NewLegacyContent(msg.GetContent(), k.govAcct) + if err != nil { + return nil, fmt.Errorf("error converting legacy content into proposal message: %w", err) + } + + proposal, err := v1.NewMsgSubmitProposal( + []sdk.Msg{contentMsg}, + msg.InitialDeposit, + msg.Proposer, + "", + msg.GetContent().GetTitle(), + msg.GetContent().GetDescription(), + ) + if err != nil { + return nil, err + } + + resp, err := k.server.SubmitProposal(goCtx, proposal) + if err != nil { + return nil, err + } + + return &v1beta1.MsgSubmitProposalResponse{ProposalId: resp.ProposalId}, nil +} + +func (k legacyMsgServer) Vote(goCtx context.Context, msg *v1beta1.MsgVote) (*v1beta1.MsgVoteResponse, error) { + _, err := k.server.Vote(goCtx, &v1.MsgVote{ + ProposalId: msg.ProposalId, + Voter: msg.Voter, + Option: v1.VoteOption(msg.Option), + }) + if err != nil { + return nil, err + } + return &v1beta1.MsgVoteResponse{}, nil +} + +func (k legacyMsgServer) VoteWeighted(goCtx context.Context, msg *v1beta1.MsgVoteWeighted) (*v1beta1.MsgVoteWeightedResponse, error) { + opts := make([]*v1.WeightedVoteOption, len(msg.Options)) + for idx, opt := range msg.Options { + opts[idx] = &v1.WeightedVoteOption{ + Option: v1.VoteOption(opt.Option), + Weight: opt.Weight.String(), + } + } + + _, err := k.server.VoteWeighted(goCtx, &v1.MsgVoteWeighted{ + ProposalId: msg.ProposalId, + Voter: msg.Voter, + Options: opts, + }) + if err != nil { + return nil, err + } + return &v1beta1.MsgVoteWeightedResponse{}, nil +} + +func (k legacyMsgServer) Deposit(goCtx context.Context, msg *v1beta1.MsgDeposit) (*v1beta1.MsgDepositResponse, error) { + _, err := k.server.Deposit(goCtx, &v1.MsgDeposit{ + ProposalId: msg.ProposalId, + Depositor: msg.Depositor, + Amount: msg.Amount, + }) + if err != nil { + return nil, err + } + return &v1beta1.MsgDepositResponse{}, nil +} diff --git a/x/gov/keeper/msg_server_test.go b/x/gov/keeper/msg_server_test.go new file mode 100644 index 00000000..613896ab --- /dev/null +++ b/x/gov/keeper/msg_server_test.go @@ -0,0 +1,1124 @@ +package keeper_test + +import ( + "strings" + "time" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +func (suite *KeeperTestSuite) TestSubmitProposalReq() { + suite.reset() + govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress() + addrs := suite.addrs + proposer := addrs[0] + + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))) + initialDeposit := coins + minDeposit := suite.govKeeper.GetParams(suite.ctx).MinDeposit + bankMsg := &banktypes.MsgSend{ + FromAddress: govAcct.String(), + ToAddress: proposer.String(), + Amount: coins, + } + + cases := map[string]struct { + preRun func() (*v1.MsgSubmitProposal, error) + expErr bool + expErrMsg string + }{ + "metadata too long": { + preRun: func() (*v1.MsgSubmitProposal, error) { + return v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + initialDeposit, + proposer.String(), + strings.Repeat("1", 300), + "Proposal", + "description of proposal", + ) + }, + expErr: true, + expErrMsg: "metadata too long", + }, + "many signers": { + preRun: func() (*v1.MsgSubmitProposal, error) { + return v1.NewMsgSubmitProposal( + []sdk.Msg{testdata.NewTestMsg(govAcct, addrs[0])}, + initialDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + }, + expErr: true, + expErrMsg: "expected gov account as only signer for proposal message", + }, + "signer isn't gov account": { + preRun: func() (*v1.MsgSubmitProposal, error) { + return v1.NewMsgSubmitProposal( + []sdk.Msg{testdata.NewTestMsg(addrs[0])}, + initialDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + }, + expErr: true, + expErrMsg: "expected gov account as only signer for proposal message", + }, + "invalid msg handler": { + preRun: func() (*v1.MsgSubmitProposal, error) { + return v1.NewMsgSubmitProposal( + []sdk.Msg{testdata.NewTestMsg(govAcct)}, + initialDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + }, + expErr: true, + expErrMsg: "proposal message not recognized by router", + }, + "all good": { + preRun: func() (*v1.MsgSubmitProposal, error) { + return v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + initialDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + }, + expErr: false, + }, + "all good with min deposit": { + preRun: func() (*v1.MsgSubmitProposal, error) { + return v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + }, + expErr: false, + }, + } + + for name, tc := range cases { + suite.Run(name, func() { + msg, err := tc.preRun() + suite.Require().NoError(err) + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + if tc.expErr { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expErrMsg) + } else { + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + } + }) + } +} + +func (suite *KeeperTestSuite) TestVoteReq() { + suite.reset() + govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress() + addrs := suite.addrs + proposer := addrs[0] + + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))) + minDeposit := suite.govKeeper.GetParams(suite.ctx).MinDeposit + bankMsg := &banktypes.MsgSend{ + FromAddress: govAcct.String(), + ToAddress: proposer.String(), + Amount: coins, + } + + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + proposalId := res.ProposalId + + cases := map[string]struct { + preRun func() uint64 + expErr bool + expErrMsg string + option v1.VoteOption + metadata string + voter sdk.AccAddress + }{ + "vote on inactive proposal": { + preRun: func() uint64 { + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + coins, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + return res.ProposalId + }, + option: v1.VoteOption_VOTE_OPTION_YES, + voter: proposer, + metadata: "", + expErr: true, + expErrMsg: "inactive proposal", + }, + "metadata too long": { + preRun: func() uint64 { + return proposalId + }, + option: v1.VoteOption_VOTE_OPTION_YES, + voter: proposer, + metadata: strings.Repeat("a", 300), + expErr: true, + expErrMsg: "metadata too long", + }, + "voter error": { + preRun: func() uint64 { + return proposalId + }, + option: v1.VoteOption_VOTE_OPTION_YES, + voter: sdk.AccAddress(strings.Repeat("a", 300)), + metadata: "", + expErr: true, + expErrMsg: "address max length is 255", + }, + "all good": { + preRun: func() uint64 { + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + return res.ProposalId + }, + option: v1.VoteOption_VOTE_OPTION_YES, + voter: proposer, + metadata: "", + expErr: false, + }, + } + + for name, tc := range cases { + suite.Run(name, func() { + pId := tc.preRun() + voteReq := v1.NewMsgVote(tc.voter, pId, tc.option, tc.metadata) + _, err := suite.msgSrvr.Vote(suite.ctx, voteReq) + if tc.expErr { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestVoteWeightedReq() { + suite.reset() + govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress() + addrs := suite.addrs + proposer := addrs[0] + + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))) + minDeposit := suite.govKeeper.GetParams(suite.ctx).MinDeposit + bankMsg := &banktypes.MsgSend{ + FromAddress: govAcct.String(), + ToAddress: proposer.String(), + Amount: coins, + } + + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + proposalId := res.ProposalId + + cases := map[string]struct { + preRun func() uint64 + vote *v1.MsgVote + expErr bool + expErrMsg string + option v1.VoteOption + metadata string + voter sdk.AccAddress + }{ + "vote on inactive proposal": { + preRun: func() uint64 { + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + coins, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + return res.ProposalId + }, + option: v1.VoteOption_VOTE_OPTION_YES, + voter: proposer, + metadata: "", + expErr: true, + expErrMsg: "inactive proposal", + }, + "metadata too long": { + preRun: func() uint64 { + return proposalId + }, + option: v1.VoteOption_VOTE_OPTION_YES, + voter: proposer, + metadata: strings.Repeat("a", 300), + expErr: true, + expErrMsg: "metadata too long", + }, + "voter error": { + preRun: func() uint64 { + return proposalId + }, + option: v1.VoteOption_VOTE_OPTION_YES, + voter: sdk.AccAddress(strings.Repeat("a", 300)), + metadata: "", + expErr: true, + expErrMsg: "address max length is 255", + }, + "all good": { + preRun: func() uint64 { + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + return res.ProposalId + }, + option: v1.VoteOption_VOTE_OPTION_YES, + voter: proposer, + metadata: "", + expErr: false, + }, + } + + for name, tc := range cases { + suite.Run(name, func() { + pId := tc.preRun() + voteReq := v1.NewMsgVoteWeighted(tc.voter, pId, v1.NewNonSplitVoteOption(tc.option), tc.metadata) + _, err := suite.msgSrvr.VoteWeighted(suite.ctx, voteReq) + if tc.expErr { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestDepositReq() { + govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress() + addrs := suite.addrs + proposer := addrs[0] + + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))) + minDeposit := suite.govKeeper.GetParams(suite.ctx).MinDeposit + bankMsg := &banktypes.MsgSend{ + FromAddress: govAcct.String(), + ToAddress: proposer.String(), + Amount: coins, + } + + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + coins, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + pId := res.ProposalId + + cases := map[string]struct { + preRun func() uint64 + expErr bool + proposalId uint64 + depositor sdk.AccAddress + deposit sdk.Coins + options v1.WeightedVoteOptions + }{ + "wrong proposal id": { + preRun: func() uint64 { + return 0 + }, + depositor: proposer, + deposit: coins, + expErr: true, + options: v1.NewNonSplitVoteOption(v1.OptionYes), + }, + "all good": { + preRun: func() uint64 { + return pId + }, + depositor: proposer, + deposit: minDeposit, + expErr: false, + options: v1.NewNonSplitVoteOption(v1.OptionYes), + }, + } + + for name, tc := range cases { + suite.Run(name, func() { + proposalId := tc.preRun() + depositReq := v1.NewMsgDeposit(tc.depositor, proposalId, tc.deposit) + _, err := suite.msgSrvr.Deposit(suite.ctx, depositReq) + if tc.expErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + } + }) + } +} + +// legacy msg server tests +func (suite *KeeperTestSuite) TestLegacyMsgSubmitProposal() { + addrs := suite.addrs + proposer := addrs[0] + + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))) + initialDeposit := coins + minDeposit := suite.govKeeper.GetParams(suite.ctx).MinDeposit + + cases := map[string]struct { + preRun func() (*v1beta1.MsgSubmitProposal, error) + expErr bool + }{ + "all good": { + preRun: func() (*v1beta1.MsgSubmitProposal, error) { + return v1beta1.NewMsgSubmitProposal( + v1beta1.NewTextProposal("test", "I am test"), + initialDeposit, + proposer, + ) + }, + expErr: false, + }, + "all good with min deposit": { + preRun: func() (*v1beta1.MsgSubmitProposal, error) { + return v1beta1.NewMsgSubmitProposal( + v1beta1.NewTextProposal("test", "I am test"), + minDeposit, + proposer, + ) + }, + expErr: false, + }, + } + + for name, c := range cases { + suite.Run(name, func() { + msg, err := c.preRun() + suite.Require().NoError(err) + res, err := suite.legacyMsgSrvr.SubmitProposal(suite.ctx, msg) + if c.expErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyMsgVote() { + govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress() + addrs := suite.addrs + proposer := addrs[0] + + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))) + minDeposit := suite.govKeeper.GetParams(suite.ctx).MinDeposit + bankMsg := &banktypes.MsgSend{ + FromAddress: govAcct.String(), + ToAddress: proposer.String(), + Amount: coins, + } + + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + proposalId := res.ProposalId + + cases := map[string]struct { + preRun func() uint64 + expErr bool + expErrMsg string + option v1beta1.VoteOption + metadata string + voter sdk.AccAddress + }{ + "vote on inactive proposal": { + preRun: func() uint64 { + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + coins, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + return res.ProposalId + }, + option: v1beta1.OptionYes, + voter: proposer, + metadata: "", + expErr: true, + expErrMsg: "inactive proposal", + }, + "voter error": { + preRun: func() uint64 { + return proposalId + }, + option: v1beta1.OptionYes, + voter: sdk.AccAddress(strings.Repeat("a", 300)), + metadata: "", + expErr: true, + expErrMsg: "address max length is 255", + }, + "all good": { + preRun: func() uint64 { + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + return res.ProposalId + }, + option: v1beta1.OptionYes, + voter: proposer, + metadata: "", + expErr: false, + }, + } + + for name, tc := range cases { + suite.Run(name, func() { + pId := tc.preRun() + voteReq := v1beta1.NewMsgVote(tc.voter, pId, tc.option) + _, err := suite.legacyMsgSrvr.Vote(suite.ctx, voteReq) + if tc.expErr { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyVoteWeighted() { + suite.reset() + govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress() + addrs := suite.addrs + proposer := addrs[0] + + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))) + minDeposit := suite.govKeeper.GetParams(suite.ctx).MinDeposit + bankMsg := &banktypes.MsgSend{ + FromAddress: govAcct.String(), + ToAddress: proposer.String(), + Amount: coins, + } + + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + proposalId := res.ProposalId + + cases := map[string]struct { + preRun func() uint64 + vote *v1beta1.MsgVote + expErr bool + expErrMsg string + option v1beta1.VoteOption + metadata string + voter sdk.AccAddress + }{ + "vote on inactive proposal": { + preRun: func() uint64 { + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + coins, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + return res.ProposalId + }, + option: v1beta1.OptionYes, + voter: proposer, + metadata: "", + expErr: true, + expErrMsg: "inactive proposal", + }, + "voter error": { + preRun: func() uint64 { + return proposalId + }, + option: v1beta1.OptionYes, + voter: sdk.AccAddress(strings.Repeat("a", 300)), + metadata: "", + expErr: true, + expErrMsg: "address max length is 255", + }, + "all good": { + preRun: func() uint64 { + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + minDeposit, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + return res.ProposalId + }, + option: v1beta1.OptionYes, + voter: proposer, + metadata: "", + expErr: false, + }, + } + + for name, tc := range cases { + suite.Run(name, func() { + pId := tc.preRun() + voteReq := v1beta1.NewMsgVoteWeighted(tc.voter, pId, v1beta1.NewNonSplitVoteOption(v1beta1.VoteOption(tc.option))) + _, err := suite.legacyMsgSrvr.VoteWeighted(suite.ctx, voteReq) + if tc.expErr { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestLegacyMsgDeposit() { + govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress() + addrs := suite.addrs + proposer := addrs[0] + + coins := sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(100))) + minDeposit := suite.govKeeper.GetParams(suite.ctx).MinDeposit + bankMsg := &banktypes.MsgSend{ + FromAddress: govAcct.String(), + ToAddress: proposer.String(), + Amount: coins, + } + + msg, err := v1.NewMsgSubmitProposal( + []sdk.Msg{bankMsg}, + coins, + proposer.String(), + "", + "Proposal", + "description of proposal", + ) + suite.Require().NoError(err) + + res, err := suite.msgSrvr.SubmitProposal(suite.ctx, msg) + suite.Require().NoError(err) + suite.Require().NotNil(res.ProposalId) + pId := res.ProposalId + + cases := map[string]struct { + preRun func() uint64 + expErr bool + proposalId uint64 + depositor sdk.AccAddress + deposit sdk.Coins + options v1beta1.WeightedVoteOptions + }{ + "wrong proposal id": { + preRun: func() uint64 { + return 0 + }, + depositor: proposer, + deposit: coins, + expErr: true, + options: v1beta1.NewNonSplitVoteOption(v1beta1.OptionYes), + }, + "all good": { + preRun: func() uint64 { + return pId + }, + depositor: proposer, + deposit: minDeposit, + expErr: false, + options: v1beta1.NewNonSplitVoteOption(v1beta1.OptionYes), + }, + } + + for name, tc := range cases { + suite.Run(name, func() { + proposalId := tc.preRun() + depositReq := v1beta1.NewMsgDeposit(tc.depositor, proposalId, tc.deposit) + _, err := suite.legacyMsgSrvr.Deposit(suite.ctx, depositReq) + if tc.expErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestMsgUpdateParams() { + authority := suite.govKeeper.GetAuthority() + params := v1.DefaultParams() + testCases := []struct { + name string + input func() *v1.MsgUpdateParams + expErr bool + expErrMsg string + }{ + { + name: "valid", + input: func() *v1.MsgUpdateParams { + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params, + } + }, + expErr: false, + }, + { + name: "invalid authority", + input: func() *v1.MsgUpdateParams { + return &v1.MsgUpdateParams{ + Authority: "authority", + Params: params, + } + }, + expErr: true, + expErrMsg: "invalid authority address", + }, + { + name: "invalid min deposit", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.MinDeposit = nil + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "invalid minimum deposit", + }, + { + name: "negative deposit", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.MinDeposit = sdk.Coins{{ + Denom: sdk.DefaultBondDenom, + Amount: sdk.NewInt(-100), + }} + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "invalid minimum deposit", + }, + { + name: "invalid max deposit period", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.MaxDepositPeriod = nil + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "maximum deposit period must not be nil", + }, + { + name: "zero max deposit period", + input: func() *v1.MsgUpdateParams { + params1 := params + duration := time.Duration(0) + params1.MaxDepositPeriod = &duration + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "maximum deposit period must be positive", + }, + { + name: "invalid quorum", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.Quorum = "abc" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "invalid quorum string", + }, + { + name: "negative quorum", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.Quorum = "-0.1" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "quorom cannot be negative", + }, + { + name: "quorum > 1", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.Quorum = "2" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "quorom too large", + }, + { + name: "invalid threshold", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.Threshold = "abc" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "invalid threshold string", + }, + { + name: "negative threshold", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.Threshold = "-0.1" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "vote threshold must be positive", + }, + { + name: "threshold > 1", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.Threshold = "2" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "vote threshold too large", + }, + { + name: "invalid veto threshold", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.VetoThreshold = "abc" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "invalid vetoThreshold string", + }, + { + name: "negative veto threshold", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.VetoThreshold = "-0.1" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "veto threshold must be positive", + }, + { + name: "veto threshold > 1", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.VetoThreshold = "2" + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "veto threshold too large", + }, + { + name: "invalid voting period", + input: func() *v1.MsgUpdateParams { + params1 := params + params1.VotingPeriod = nil + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "voting period must not be nil", + }, + { + name: "zero voting period", + input: func() *v1.MsgUpdateParams { + params1 := params + duration := time.Duration(0) + params1.VotingPeriod = &duration + + return &v1.MsgUpdateParams{ + Authority: authority, + Params: params1, + } + }, + expErr: true, + expErrMsg: "voting period must be positive", + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + msg := tc.input() + exec := func(updateParams *v1.MsgUpdateParams) error { + if err := msg.ValidateBasic(); err != nil { + return err + } + + if _, err := suite.msgSrvr.UpdateParams(suite.ctx, updateParams); err != nil { + return err + } + return nil + } + + err := exec(msg) + if tc.expErr { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} + +func (suite *KeeperTestSuite) TestSubmitProposal_InitialDeposit() { + const meetsDepositValue = baseDepositTestAmount * baseDepositTestPercent / 100 + baseDepositRatioDec := sdk.NewDec(baseDepositTestPercent).Quo(sdk.NewDec(100)) + + testcases := map[string]struct { + minDeposit sdk.Coins + minInitialDepositRatio sdk.Dec + initialDeposit sdk.Coins + accountBalance sdk.Coins + + expectError bool + }{ + "meets initial deposit, enough balance - success": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositRatio: baseDepositRatioDec, + initialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(meetsDepositValue))), + accountBalance: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(meetsDepositValue))), + }, + "does not meet initial deposit, enough balance - error": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositRatio: baseDepositRatioDec, + initialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(meetsDepositValue-1))), + accountBalance: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(meetsDepositValue))), + + expectError: true, + }, + "meets initial deposit, not enough balance - error": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositRatio: baseDepositRatioDec, + initialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(meetsDepositValue))), + accountBalance: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(meetsDepositValue-1))), + + expectError: true, + }, + "does not meet initial deposit and not enough balance - error": { + minDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseDepositTestAmount))), + minInitialDepositRatio: baseDepositRatioDec, + initialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(meetsDepositValue-1))), + accountBalance: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(meetsDepositValue-1))), + + expectError: true, + }, + } + + for name, tc := range testcases { + suite.Run(name, func() { + // Setup + govKeeper, ctx := suite.govKeeper, suite.ctx + address := simtestutil.AddTestAddrs(suite.bankKeeper, suite.stakingKeeper, ctx, 1, tc.accountBalance[0].Amount)[0] + + params := v1.DefaultParams() + params.MinDeposit = tc.minDeposit + params.MinInitialDepositRatio = tc.minInitialDepositRatio.String() + govKeeper.SetParams(ctx, params) + + msg, err := v1.NewMsgSubmitProposal(TestProposal, tc.initialDeposit, address.String(), "test", "Proposal", "description of proposal") + suite.Require().NoError(err) + + // System under test + _, err = suite.msgSrvr.SubmitProposal(sdk.WrapSDKContext(ctx), msg) + + // Assertions + if tc.expectError { + suite.Require().Error(err) + return + } + suite.Require().NoError(err) + }) + } +} diff --git a/x/gov/keeper/params.go b/x/gov/keeper/params.go new file mode 100644 index 00000000..9e4aef9d --- /dev/null +++ b/x/gov/keeper/params.go @@ -0,0 +1,31 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// SetParams sets the gov module's parameters. +func (k Keeper) SetParams(ctx sdk.Context, params v1.Params) error { + store := ctx.KVStore(k.storeKey) + bz, err := k.cdc.Marshal(¶ms) + if err != nil { + return err + } + store.Set(types.ParamsKey, bz) + + return nil +} + +// GetParams gets the gov module's parameters. +func (k Keeper) GetParams(clientCtx sdk.Context) (params v1.Params) { + store := clientCtx.KVStore(k.storeKey) + bz := store.Get(types.ParamsKey) + if bz == nil { + return params + } + + k.cdc.MustUnmarshal(bz, ¶ms) + return params +} diff --git a/x/gov/keeper/proposal.go b/x/gov/keeper/proposal.go new file mode 100644 index 00000000..b050b51f --- /dev/null +++ b/x/gov/keeper/proposal.go @@ -0,0 +1,291 @@ +package keeper + +import ( + "errors" + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// SubmitProposal creates a new proposal given an array of messages +func (keeper Keeper) SubmitProposal(ctx sdk.Context, messages []sdk.Msg, metadata, title, summary string, proposer sdk.AccAddress) (v1.Proposal, error) { + err := keeper.assertMetadataLength(metadata) + if err != nil { + return v1.Proposal{}, err + } + + // assert summary is no longer than predefined max length of metadata + err = keeper.assertMetadataLength(summary) + if err != nil { + return v1.Proposal{}, err + } + + // assert title is no longer than predefined max length of metadata + err = keeper.assertMetadataLength(title) + if err != nil { + return v1.Proposal{}, err + } + + // Will hold a comma-separated string of all Msg type URLs. + msgsStr := "" + + // Loop through all messages and confirm that each has a handler and the gov module account + // as the only signer + for _, msg := range messages { + msgsStr += fmt.Sprintf(",%s", sdk.MsgTypeURL(msg)) + + // perform a basic validation of the message + if err := msg.ValidateBasic(); err != nil { + return v1.Proposal{}, sdkerrors.Wrap(types.ErrInvalidProposalMsg, err.Error()) + } + + signers := msg.GetSigners() + if len(signers) != 1 { + return v1.Proposal{}, types.ErrInvalidSigner + } + + // assert that the governance module account is the only signer of the messages + if !signers[0].Equals(keeper.GetGovernanceAccount(ctx).GetAddress()) { + return v1.Proposal{}, sdkerrors.Wrapf(types.ErrInvalidSigner, signers[0].String()) + } + + // use the msg service router to see that there is a valid route for that message. + handler := keeper.router.Handler(msg) + if handler == nil { + return v1.Proposal{}, sdkerrors.Wrap(types.ErrUnroutableProposalMsg, sdk.MsgTypeURL(msg)) + } + + // Only if it's a MsgExecLegacyContent do we try to execute the + // proposal in a cached context. + // For other Msgs, we do not verify the proposal messages any further. + // They may fail upon execution. + // ref: https://github.com/cosmos/cosmos-sdk/pull/10868#discussion_r784872842 + if msg, ok := msg.(*v1.MsgExecLegacyContent); ok { + cacheCtx, _ := ctx.CacheContext() + if _, err := handler(cacheCtx, msg); err != nil { + if errors.Is(types.ErrNoProposalHandlerExists, err) { + return v1.Proposal{}, err + } + return v1.Proposal{}, sdkerrors.Wrap(types.ErrInvalidProposalContent, err.Error()) + } + } + + } + + proposalID, err := keeper.GetProposalID(ctx) + if err != nil { + return v1.Proposal{}, err + } + + submitTime := ctx.BlockHeader().Time + depositPeriod := keeper.GetParams(ctx).MaxDepositPeriod + + proposal, err := v1.NewProposal(messages, proposalID, submitTime, submitTime.Add(*depositPeriod), metadata, title, summary, proposer) + if err != nil { + return v1.Proposal{}, err + } + + keeper.SetProposal(ctx, proposal) + keeper.InsertInactiveProposalQueue(ctx, proposalID, *proposal.DepositEndTime) + keeper.SetProposalID(ctx, proposalID+1) + + // called right after a proposal is submitted + keeper.Hooks().AfterProposalSubmission(ctx, proposalID) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeSubmitProposal, + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)), + sdk.NewAttribute(types.AttributeKeyProposalMessages, msgsStr), + ), + ) + + return proposal, nil +} + +// GetProposal gets a proposal from store by ProposalID. +// Panics if can't unmarshal the proposal. +func (keeper Keeper) GetProposal(ctx sdk.Context, proposalID uint64) (v1.Proposal, bool) { + store := ctx.KVStore(keeper.storeKey) + + bz := store.Get(types.ProposalKey(proposalID)) + if bz == nil { + return v1.Proposal{}, false + } + + var proposal v1.Proposal + if err := keeper.UnmarshalProposal(bz, &proposal); err != nil { + panic(err) + } + + return proposal, true +} + +// SetProposal sets a proposal to store. +// Panics if can't marshal the proposal. +func (keeper Keeper) SetProposal(ctx sdk.Context, proposal v1.Proposal) { + bz, err := keeper.MarshalProposal(proposal) + if err != nil { + panic(err) + } + + store := ctx.KVStore(keeper.storeKey) + + if proposal.Status == v1.StatusVotingPeriod { + store.Set(types.VotingPeriodProposalKey(proposal.Id), []byte{1}) + } else { + store.Delete(types.VotingPeriodProposalKey(proposal.Id)) + } + + store.Set(types.ProposalKey(proposal.Id), bz) +} + +// DeleteProposal deletes a proposal from store. +// Panics if the proposal doesn't exist. +func (keeper Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) { + store := ctx.KVStore(keeper.storeKey) + proposal, ok := keeper.GetProposal(ctx, proposalID) + if !ok { + panic(fmt.Sprintf("couldn't find proposal with id#%d", proposalID)) + } + + if proposal.DepositEndTime != nil { + keeper.RemoveFromInactiveProposalQueue(ctx, proposalID, *proposal.DepositEndTime) + } + if proposal.VotingEndTime != nil { + keeper.RemoveFromActiveProposalQueue(ctx, proposalID, *proposal.VotingEndTime) + store.Delete(types.VotingPeriodProposalKey(proposalID)) + } + + store.Delete(types.ProposalKey(proposalID)) +} + +// IterateProposals iterates over all the proposals and performs a callback function. +// Panics when the iterator encounters a proposal which can't be unmarshaled. +func (keeper Keeper) IterateProposals(ctx sdk.Context, cb func(proposal v1.Proposal) (stop bool)) { + store := ctx.KVStore(keeper.storeKey) + + iterator := sdk.KVStorePrefixIterator(store, types.ProposalsKeyPrefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var proposal v1.Proposal + err := keeper.UnmarshalProposal(iterator.Value(), &proposal) + if err != nil { + panic(err) + } + + if cb(proposal) { + break + } + } +} + +// GetProposals returns all the proposals from store +func (keeper Keeper) GetProposals(ctx sdk.Context) (proposals v1.Proposals) { + keeper.IterateProposals(ctx, func(proposal v1.Proposal) bool { + proposals = append(proposals, &proposal) + return false + }) + return +} + +// GetProposalsFiltered retrieves proposals filtered by a given set of params which +// include pagination parameters along with voter and depositor addresses and a +// proposal status. The voter address will filter proposals by whether or not +// that address has voted on proposals. The depositor address will filter proposals +// by whether or not that address has deposited to them. Finally, status will filter +// proposals by status. +// +// NOTE: If no filters are provided, all proposals will be returned in paginated +// form. +func (keeper Keeper) GetProposalsFiltered(ctx sdk.Context, params v1.QueryProposalsParams) v1.Proposals { + proposals := keeper.GetProposals(ctx) + filteredProposals := make([]*v1.Proposal, 0, len(proposals)) + + for _, p := range proposals { + matchVoter, matchDepositor, matchStatus := true, true, true + + // match status (if supplied/valid) + if v1.ValidProposalStatus(params.ProposalStatus) { + matchStatus = p.Status == params.ProposalStatus + } + + // match voter address (if supplied) + if len(params.Voter) > 0 { + _, matchVoter = keeper.GetVote(ctx, p.Id, params.Voter) + } + + // match depositor (if supplied) + if len(params.Depositor) > 0 { + _, matchDepositor = keeper.GetDeposit(ctx, p.Id, params.Depositor) + } + + if matchVoter && matchDepositor && matchStatus { + filteredProposals = append(filteredProposals, p) + } + } + + start, end := client.Paginate(len(filteredProposals), params.Page, params.Limit, 100) + if start < 0 || end < 0 { + filteredProposals = []*v1.Proposal{} + } else { + filteredProposals = filteredProposals[start:end] + } + + return filteredProposals +} + +// GetProposalID gets the highest proposal ID +func (keeper Keeper) GetProposalID(ctx sdk.Context) (proposalID uint64, err error) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.ProposalIDKey) + if bz == nil { + return 0, sdkerrors.Wrap(types.ErrInvalidGenesis, "initial proposal ID hasn't been set") + } + + proposalID = types.GetProposalIDFromBytes(bz) + return proposalID, nil +} + +// SetProposalID sets the new proposal ID to the store +func (keeper Keeper) SetProposalID(ctx sdk.Context, proposalID uint64) { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.ProposalIDKey, types.GetProposalIDBytes(proposalID)) +} + +// ActivateVotingPeriod activates the voting period of a proposal +func (keeper Keeper) ActivateVotingPeriod(ctx sdk.Context, proposal v1.Proposal) { + startTime := ctx.BlockHeader().Time + proposal.VotingStartTime = &startTime + votingPeriod := keeper.GetParams(ctx).VotingPeriod + endTime := proposal.VotingStartTime.Add(*votingPeriod) + proposal.VotingEndTime = &endTime + proposal.Status = v1.StatusVotingPeriod + keeper.SetProposal(ctx, proposal) + + keeper.RemoveFromInactiveProposalQueue(ctx, proposal.Id, *proposal.DepositEndTime) + keeper.InsertActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime) +} + +// MarshalProposal marshals the proposal and returns binary encoded bytes. +func (keeper Keeper) MarshalProposal(proposal v1.Proposal) ([]byte, error) { + bz, err := keeper.cdc.Marshal(&proposal) + if err != nil { + return nil, err + } + return bz, nil +} + +// UnmarshalProposal unmarshals the proposal. +func (keeper Keeper) UnmarshalProposal(bz []byte, proposal *v1.Proposal) error { + err := keeper.cdc.Unmarshal(bz, proposal) + if err != nil { + return err + } + return nil +} diff --git a/x/gov/keeper/proposal_test.go b/x/gov/keeper/proposal_test.go new file mode 100644 index 00000000..876edd6f --- /dev/null +++ b/x/gov/keeper/proposal_test.go @@ -0,0 +1,210 @@ +package keeper_test + +import ( + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +func (suite *KeeperTestSuite) TestGetSetProposal() { + tp := TestProposal + proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NoError(err) + proposalID := proposal.Id + suite.govKeeper.SetProposal(suite.ctx, proposal) + + gotProposal, ok := suite.govKeeper.GetProposal(suite.ctx, proposalID) + suite.Require().True(ok) + suite.Require().Equal(proposal, gotProposal) +} + +func (suite *KeeperTestSuite) TestDeleteProposal() { + // delete non-existing proposal + suite.Require().PanicsWithValue(fmt.Sprintf("couldn't find proposal with id#%d", 10), + func() { + suite.govKeeper.DeleteProposal(suite.ctx, 10) + }, + ) + tp := TestProposal + proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NoError(err) + proposalID := proposal.Id + suite.govKeeper.SetProposal(suite.ctx, proposal) + suite.Require().NotPanics(func() { + suite.govKeeper.DeleteProposal(suite.ctx, proposalID) + }, "") +} + +func (suite *KeeperTestSuite) TestActivateVotingPeriod() { + tp := TestProposal + proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NoError(err) + + suite.Require().Nil(proposal.VotingStartTime) + + suite.govKeeper.ActivateVotingPeriod(suite.ctx, proposal) + + proposal, ok := suite.govKeeper.GetProposal(suite.ctx, proposal.Id) + suite.Require().True(ok) + suite.Require().True(proposal.VotingStartTime.Equal(suite.ctx.BlockHeader().Time)) + + activeIterator := suite.govKeeper.ActiveProposalQueueIterator(suite.ctx, *proposal.VotingEndTime) + suite.Require().True(activeIterator.Valid()) + + proposalID := types.GetProposalIDFromBytes(activeIterator.Value()) + suite.Require().Equal(proposalID, proposal.Id) + activeIterator.Close() + + // delete the proposal to avoid issues with other tests + suite.Require().NotPanics(func() { + suite.govKeeper.DeleteProposal(suite.ctx, proposalID) + }, "") +} + +func (suite *KeeperTestSuite) TestDeleteProposalInVotingPeriod() { + suite.reset() + tp := TestProposal + proposal, err := suite.govKeeper.SubmitProposal(suite.ctx, tp, "", "test", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NoError(err) + suite.Require().Nil(proposal.VotingStartTime) + + suite.govKeeper.ActivateVotingPeriod(suite.ctx, proposal) + + proposal, ok := suite.govKeeper.GetProposal(suite.ctx, proposal.Id) + suite.Require().True(ok) + suite.Require().True(proposal.VotingStartTime.Equal(suite.ctx.BlockHeader().Time)) + + activeIterator := suite.govKeeper.ActiveProposalQueueIterator(suite.ctx, *proposal.VotingEndTime) + suite.Require().True(activeIterator.Valid()) + + proposalID := types.GetProposalIDFromBytes(activeIterator.Value()) + suite.Require().Equal(proposalID, proposal.Id) + activeIterator.Close() + + // add vote + voteOptions := []*v1.WeightedVoteOption{{Option: v1.OptionYes, Weight: "1.0"}} + err = suite.govKeeper.AddVote(suite.ctx, proposal.Id, sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r"), voteOptions, "") + suite.Require().NoError(err) + + suite.Require().NotPanics(func() { + suite.govKeeper.DeleteProposal(suite.ctx, proposalID) + }, "") + + // add vote but proposal is deleted along with its VotingPeriodProposalKey + err = suite.govKeeper.AddVote(suite.ctx, proposal.Id, sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r"), voteOptions, "") + suite.Require().ErrorContains(err, ": inactive proposal") +} + +type invalidProposalRoute struct{ v1beta1.TextProposal } + +func (invalidProposalRoute) ProposalRoute() string { return "nonexistingroute" } + +func (suite *KeeperTestSuite) TestSubmitProposal() { + govAcct := suite.govKeeper.GetGovernanceAccount(suite.ctx).GetAddress().String() + _, _, randomAddr := testdata.KeyTestPubAddr() + tp := v1beta1.TextProposal{Title: "title", Description: "description"} + + testCases := []struct { + content v1beta1.Content + authority string + metadata string + expectedErr error + }{ + {&tp, govAcct, "", nil}, + // Keeper does not check the validity of title and description, no error + {&v1beta1.TextProposal{Title: "", Description: "description"}, govAcct, "", nil}, + {&v1beta1.TextProposal{Title: strings.Repeat("1234567890", 100), Description: "description"}, govAcct, "", nil}, + {&v1beta1.TextProposal{Title: "title", Description: ""}, govAcct, "", nil}, + {&v1beta1.TextProposal{Title: "title", Description: strings.Repeat("1234567890", 1000)}, govAcct, "", nil}, + // error when metadata is too long (>10000) + {&tp, govAcct, strings.Repeat("a", 100001), types.ErrMetadataTooLong}, + // error when signer is not gov acct + {&tp, randomAddr.String(), "", types.ErrInvalidSigner}, + // error only when invalid route + {&invalidProposalRoute{}, govAcct, "", types.ErrNoProposalHandlerExists}, + } + + for i, tc := range testCases { + prop, err := v1.NewLegacyContent(tc.content, tc.authority) + suite.Require().NoError(err) + _, err = suite.govKeeper.SubmitProposal(suite.ctx, []sdk.Msg{prop}, tc.metadata, "title", "", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().True(errors.Is(tc.expectedErr, err), "tc #%d; got: %v, expected: %v", i, err, tc.expectedErr) + } +} + +func (suite *KeeperTestSuite) TestGetProposalsFiltered() { + proposalID := uint64(1) + status := []v1.ProposalStatus{v1.StatusDepositPeriod, v1.StatusVotingPeriod} + + addr1 := sdk.AccAddress("foo_________________") + + for _, s := range status { + for i := 0; i < 50; i++ { + p, err := v1.NewProposal(TestProposal, proposalID, time.Now(), time.Now(), "", "title", "summary", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + suite.Require().NoError(err) + + p.Status = s + + if i%2 == 0 { + d := v1.NewDeposit(proposalID, addr1, nil) + v := v1.NewVote(proposalID, addr1, v1.NewNonSplitVoteOption(v1.OptionYes), "") + suite.govKeeper.SetDeposit(suite.ctx, d) + suite.govKeeper.SetVote(suite.ctx, v) + } + + suite.govKeeper.SetProposal(suite.ctx, p) + proposalID++ + } + } + + testCases := []struct { + params v1.QueryProposalsParams + expectedNumResults int + }{ + {v1.NewQueryProposalsParams(1, 50, v1.StatusNil, nil, nil), 50}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusDepositPeriod, nil, nil), 50}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusVotingPeriod, nil, nil), 50}, + {v1.NewQueryProposalsParams(1, 25, v1.StatusNil, nil, nil), 25}, + {v1.NewQueryProposalsParams(2, 25, v1.StatusNil, nil, nil), 25}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusRejected, nil, nil), 0}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusNil, addr1, nil), 50}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusNil, nil, addr1), 50}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusNil, addr1, addr1), 50}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusDepositPeriod, addr1, addr1), 25}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusDepositPeriod, nil, nil), 50}, + {v1.NewQueryProposalsParams(1, 50, v1.StatusVotingPeriod, nil, nil), 50}, + } + + for i, tc := range testCases { + suite.Run(fmt.Sprintf("Test Case %d", i), func() { + proposals := suite.govKeeper.GetProposalsFiltered(suite.ctx, tc.params) + suite.Require().Len(proposals, tc.expectedNumResults) + + for _, p := range proposals { + if v1.ValidProposalStatus(tc.params.ProposalStatus) { + suite.Require().Equal(tc.params.ProposalStatus, p.Status) + } + } + }) + } +} + +func TestMigrateProposalMessages(t *testing.T) { + content := v1beta1.NewTextProposal("Test", "description") + contentMsg, err := v1.NewLegacyContent(content, sdk.AccAddress("test1").String()) + require.NoError(t, err) + content, err = v1.LegacyContentFromMessage(contentMsg) + require.NoError(t, err) + require.Equal(t, "Test", content.GetTitle()) + require.Equal(t, "description", content.GetDescription()) +} diff --git a/x/gov/keeper/tally.go b/x/gov/keeper/tally.go new file mode 100644 index 00000000..863c64f5 --- /dev/null +++ b/x/gov/keeper/tally.go @@ -0,0 +1,127 @@ +package keeper + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// TODO: Break into several smaller functions for clarity + +// Tally iterates over the votes and updates the tally of a proposal based on the voting power of the +// voters +func (keeper Keeper) Tally(ctx sdk.Context, proposal v1.Proposal) (passes bool, burnDeposits bool, tallyResults v1.TallyResult) { + results := make(map[v1.VoteOption]sdk.Dec) + results[v1.OptionYes] = math.LegacyZeroDec() + results[v1.OptionAbstain] = math.LegacyZeroDec() + results[v1.OptionNo] = math.LegacyZeroDec() + results[v1.OptionNoWithVeto] = math.LegacyZeroDec() + + totalVotingPower := math.LegacyZeroDec() + currValidators := make(map[string]v1.ValidatorGovInfo) + + // fetch all the bonded validators, insert them into currValidators + keeper.sk.IterateBondedValidatorsByPower(ctx, func(index int64, validator stakingtypes.ValidatorI) (stop bool) { + currValidators[validator.GetOperator().String()] = v1.NewValidatorGovInfo( + validator.GetOperator(), + validator.GetBondedTokens(), + validator.GetDelegatorShares(), + math.LegacyZeroDec(), + v1.WeightedVoteOptions{}, + ) + + return false + }) + + keeper.IterateVotes(ctx, proposal.Id, func(vote v1.Vote) bool { + // if validator, just record it in the map + voter := sdk.MustAccAddressFromBech32(vote.Voter) + + valAddrStr := sdk.ValAddress(voter.Bytes()).String() + if val, ok := currValidators[valAddrStr]; ok { + val.Vote = vote.Options + currValidators[valAddrStr] = val + } + + // iterate over all delegations from voter, deduct from any delegated-to validators + keeper.sk.IterateDelegations(ctx, voter, func(index int64, delegation stakingtypes.DelegationI) (stop bool) { + valAddrStr := delegation.GetValidatorAddr().String() + + if val, ok := currValidators[valAddrStr]; ok { + // There is no need to handle the special case that validator address equal to voter address. + // Because voter's voting power will tally again even if there will be deduction of voter's voting power from validator. + val.DelegatorDeductions = val.DelegatorDeductions.Add(delegation.GetShares()) + currValidators[valAddrStr] = val + + // delegation shares * bonded / total shares + votingPower := delegation.GetShares().MulInt(val.BondedTokens).Quo(val.DelegatorShares) + + for _, option := range vote.Options { + weight, _ := sdk.NewDecFromStr(option.Weight) + subPower := votingPower.Mul(weight) + results[option.Option] = results[option.Option].Add(subPower) + } + totalVotingPower = totalVotingPower.Add(votingPower) + } + + return false + }) + + keeper.deleteVote(ctx, vote.ProposalId, voter) + return false + }) + + // iterate over the validators again to tally their voting power + for _, val := range currValidators { + if len(val.Vote) == 0 { + continue + } + + sharesAfterDeductions := val.DelegatorShares.Sub(val.DelegatorDeductions) + votingPower := sharesAfterDeductions.MulInt(val.BondedTokens).Quo(val.DelegatorShares) + + for _, option := range val.Vote { + weight, _ := sdk.NewDecFromStr(option.Weight) + subPower := votingPower.Mul(weight) + results[option.Option] = results[option.Option].Add(subPower) + } + totalVotingPower = totalVotingPower.Add(votingPower) + } + + params := keeper.GetParams(ctx) + tallyResults = v1.NewTallyResultFromMap(results) + + // TODO: Upgrade the spec to cover all of these cases & remove pseudocode. + // If there is no staked coins, the proposal fails + if keeper.sk.TotalBondedTokens(ctx).IsZero() { + return false, false, tallyResults + } + + // If there is not enough quorum of votes, the proposal fails + percentVoting := totalVotingPower.Quo(sdk.NewDecFromInt(keeper.sk.TotalBondedTokens(ctx))) + quorum, _ := sdk.NewDecFromStr(params.Quorum) + if percentVoting.LT(quorum) { + return false, params.BurnVoteQuorum, tallyResults + } + + // If no one votes (everyone abstains), proposal fails + if totalVotingPower.Sub(results[v1.OptionAbstain]).Equal(math.LegacyZeroDec()) { + return false, false, tallyResults + } + + // If more than 1/3 of voters veto, proposal fails + vetoThreshold, _ := sdk.NewDecFromStr(params.VetoThreshold) + if results[v1.OptionNoWithVeto].Quo(totalVotingPower).GT(vetoThreshold) { + return false, params.BurnVoteVeto, tallyResults + } + + // If more than 1/2 of non-abstaining voters vote Yes, proposal passes + threshold, _ := sdk.NewDecFromStr(params.Threshold) + if results[v1.OptionYes].Quo(totalVotingPower.Sub(results[v1.OptionAbstain])).GT(threshold) { + return true, false, tallyResults + } + + // If more than 1/2 of non-abstaining voters vote No, proposal fails + return false, false, tallyResults +} diff --git a/x/gov/keeper/vote.go b/x/gov/keeper/vote.go new file mode 100644 index 00000000..2f24c37d --- /dev/null +++ b/x/gov/keeper/vote.go @@ -0,0 +1,125 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// AddVote adds a vote on a specific proposal +func (keeper Keeper) AddVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress, options v1.WeightedVoteOptions, metadata string) error { + // Check if proposal is in voting period. + store := ctx.KVStore(keeper.storeKey) + if !store.Has(types.VotingPeriodProposalKey(proposalID)) { + return sdkerrors.Wrapf(types.ErrInactiveProposal, "%d", proposalID) + } + + err := keeper.assertMetadataLength(metadata) + if err != nil { + return err + } + + for _, option := range options { + if !v1.ValidWeightedVoteOption(*option) { + return sdkerrors.Wrap(types.ErrInvalidVote, option.String()) + } + } + + vote := v1.NewVote(proposalID, voterAddr, options, metadata) + keeper.SetVote(ctx, vote) + + // called after a vote on a proposal is cast + keeper.Hooks().AfterProposalVote(ctx, proposalID, voterAddr) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeProposalVote, + sdk.NewAttribute(types.AttributeKeyVoter, voterAddr.String()), + sdk.NewAttribute(types.AttributeKeyOption, options.String()), + sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", proposalID)), + ), + ) + + return nil +} + +// GetAllVotes returns all the votes from the store +func (keeper Keeper) GetAllVotes(ctx sdk.Context) (votes v1.Votes) { + keeper.IterateAllVotes(ctx, func(vote v1.Vote) bool { + votes = append(votes, &vote) + return false + }) + return +} + +// GetVotes returns all the votes from a proposal +func (keeper Keeper) GetVotes(ctx sdk.Context, proposalID uint64) (votes v1.Votes) { + keeper.IterateVotes(ctx, proposalID, func(vote v1.Vote) bool { + votes = append(votes, &vote) + return false + }) + return +} + +// GetVote gets the vote from an address on a specific proposal +func (keeper Keeper) GetVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) (vote v1.Vote, found bool) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.VoteKey(proposalID, voterAddr)) + if bz == nil { + return vote, false + } + + keeper.cdc.MustUnmarshal(bz, &vote) + + return vote, true +} + +// SetVote sets a Vote to the gov store +func (keeper Keeper) SetVote(ctx sdk.Context, vote v1.Vote) { + store := ctx.KVStore(keeper.storeKey) + bz := keeper.cdc.MustMarshal(&vote) + addr := sdk.MustAccAddressFromBech32(vote.Voter) + + store.Set(types.VoteKey(vote.ProposalId, addr), bz) +} + +// IterateAllVotes iterates over all the stored votes and performs a callback function +func (keeper Keeper) IterateAllVotes(ctx sdk.Context, cb func(vote v1.Vote) (stop bool)) { + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.VotesKeyPrefix) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var vote v1.Vote + keeper.cdc.MustUnmarshal(iterator.Value(), &vote) + + if cb(vote) { + break + } + } +} + +// IterateVotes iterates over all the proposals votes and performs a callback function +func (keeper Keeper) IterateVotes(ctx sdk.Context, proposalID uint64, cb func(vote v1.Vote) (stop bool)) { + store := ctx.KVStore(keeper.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.VotesKey(proposalID)) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + var vote v1.Vote + keeper.cdc.MustUnmarshal(iterator.Value(), &vote) + + if cb(vote) { + break + } + } +} + +// deleteVote deletes a vote from a given proposalID and voter from the store +func (keeper Keeper) deleteVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { + store := ctx.KVStore(keeper.storeKey) + store.Delete(types.VoteKey(proposalID, voterAddr)) +} diff --git a/x/gov/keeper/vote_test.go b/x/gov/keeper/vote_test.go new file mode 100644 index 00000000..048b75c3 --- /dev/null +++ b/x/gov/keeper/vote_test.go @@ -0,0 +1,88 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +func TestVotes(t *testing.T) { + govKeeper, _, bankKeeper, stakingKeeper, _, ctx := setupGovKeeper(t) + addrs := simtestutil.AddTestAddrsIncremental(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(10000000)) + + tp := TestProposal + proposal, err := govKeeper.SubmitProposal(ctx, tp, "", "title", "description", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + proposalID := proposal.Id + metadata := "metadata" + + var invalidOption v1.VoteOption = 0x10 + + require.Error(t, govKeeper.AddVote(ctx, proposalID, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), metadata), "proposal not on voting period") + require.Error(t, govKeeper.AddVote(ctx, 10, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), ""), "invalid proposal ID") + + proposal.Status = v1.StatusVotingPeriod + govKeeper.SetProposal(ctx, proposal) + + require.Error(t, govKeeper.AddVote(ctx, proposalID, addrs[0], v1.NewNonSplitVoteOption(invalidOption), ""), "invalid option") + + // Test first vote + require.NoError(t, govKeeper.AddVote(ctx, proposalID, addrs[0], v1.NewNonSplitVoteOption(v1.OptionAbstain), metadata)) + vote, found := govKeeper.GetVote(ctx, proposalID, addrs[0]) + require.True(t, found) + require.Equal(t, addrs[0].String(), vote.Voter) + require.Equal(t, proposalID, vote.ProposalId) + require.True(t, len(vote.Options) == 1) + require.Equal(t, v1.OptionAbstain, vote.Options[0].Option) + + // Test change of vote + require.NoError(t, govKeeper.AddVote(ctx, proposalID, addrs[0], v1.NewNonSplitVoteOption(v1.OptionYes), "")) + vote, found = govKeeper.GetVote(ctx, proposalID, addrs[0]) + require.True(t, found) + require.Equal(t, addrs[0].String(), vote.Voter) + require.Equal(t, proposalID, vote.ProposalId) + require.True(t, len(vote.Options) == 1) + require.Equal(t, v1.OptionYes, vote.Options[0].Option) + + // Test second vote + require.NoError(t, govKeeper.AddVote(ctx, proposalID, addrs[1], v1.WeightedVoteOptions{ + v1.NewWeightedVoteOption(v1.OptionYes, sdk.NewDecWithPrec(60, 2)), + v1.NewWeightedVoteOption(v1.OptionNo, sdk.NewDecWithPrec(30, 2)), + v1.NewWeightedVoteOption(v1.OptionAbstain, sdk.NewDecWithPrec(5, 2)), + v1.NewWeightedVoteOption(v1.OptionNoWithVeto, sdk.NewDecWithPrec(5, 2)), + }, "")) + vote, found = govKeeper.GetVote(ctx, proposalID, addrs[1]) + require.True(t, found) + require.Equal(t, addrs[1].String(), vote.Voter) + require.Equal(t, proposalID, vote.ProposalId) + require.True(t, len(vote.Options) == 4) + require.Equal(t, v1.OptionYes, vote.Options[0].Option) + require.Equal(t, v1.OptionNo, vote.Options[1].Option) + require.Equal(t, v1.OptionAbstain, vote.Options[2].Option) + require.Equal(t, v1.OptionNoWithVeto, vote.Options[3].Option) + require.Equal(t, vote.Options[0].Weight, sdk.NewDecWithPrec(60, 2).String()) + require.Equal(t, vote.Options[1].Weight, sdk.NewDecWithPrec(30, 2).String()) + require.Equal(t, vote.Options[2].Weight, sdk.NewDecWithPrec(5, 2).String()) + require.Equal(t, vote.Options[3].Weight, sdk.NewDecWithPrec(5, 2).String()) + + // Test vote iterator + // NOTE order of deposits is determined by the addresses + votes := govKeeper.GetAllVotes(ctx) + require.Len(t, votes, 2) + require.Equal(t, votes, govKeeper.GetVotes(ctx, proposalID)) + require.Equal(t, addrs[0].String(), votes[0].Voter) + require.Equal(t, proposalID, votes[0].ProposalId) + require.True(t, len(votes[0].Options) == 1) + require.Equal(t, v1.OptionYes, votes[0].Options[0].Option) + require.Equal(t, addrs[1].String(), votes[1].Voter) + require.Equal(t, proposalID, votes[1].ProposalId) + require.True(t, len(votes[1].Options) == 4) + require.Equal(t, votes[1].Options[0].Weight, sdk.NewDecWithPrec(60, 2).String()) + require.Equal(t, votes[1].Options[1].Weight, sdk.NewDecWithPrec(30, 2).String()) + require.Equal(t, votes[1].Options[2].Weight, sdk.NewDecWithPrec(5, 2).String()) + require.Equal(t, votes[1].Options[3].Weight, sdk.NewDecWithPrec(5, 2).String()) +} diff --git a/x/gov/module.go b/x/gov/module.go new file mode 100644 index 00000000..67821de5 --- /dev/null +++ b/x/gov/module.go @@ -0,0 +1,346 @@ +package gov + +// DONTCOVER + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + + abci "github.com/cometbft/cometbft/abci/types" + + modulev1 "cosmossdk.io/api/cosmos/gov/module/v1" + "cosmossdk.io/core/appmodule" + "cosmossdk.io/depinject" + + "github.com/atomone-hub/atomone/x/gov/keeper" + "github.com/atomone-hub/atomone/x/gov/simulation" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + store "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" +) + +const ConsensusVersion = 4 + +var ( + _ module.EndBlockAppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} + _ module.AppModuleSimulation = AppModule{} +) + +// AppModuleBasic defines the basic application module used by the gov module. +type AppModuleBasic struct { + cdc codec.Codec + legacyProposalHandlers []govclient.ProposalHandler // legacy proposal handlers which live in governance cli and rest +} + +// NewAppModuleBasic creates a new AppModuleBasic object +func NewAppModuleBasic(legacyProposalHandlers []govclient.ProposalHandler) AppModuleBasic { + return AppModuleBasic{ + legacyProposalHandlers: legacyProposalHandlers, + } +} + +// Name returns the gov module's name. +func (AppModuleBasic) Name() string { + return govtypes.ModuleName +} + +// RegisterLegacyAminoCodec registers the gov module's types for the given codec. +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + v1beta1.RegisterLegacyAminoCodec(cdc) + v1.RegisterLegacyAminoCodec(cdc) +} + +// DefaultGenesis returns default genesis state as raw bytes for the gov +// module. +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(v1.DefaultGenesisState()) +} + +// ValidateGenesis performs genesis state validation for the gov module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { + var data v1.GenesisState + if err := cdc.UnmarshalJSON(bz, &data); err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", govtypes.ModuleName, err) + } + + return v1.ValidateGenesis(&data) +} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the gov module. +func (a AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *gwruntime.ServeMux) { + if err := v1.RegisterQueryHandlerClient(context.Background(), mux, v1.NewQueryClient(clientCtx)); err != nil { + panic(err) + } + if err := v1beta1.RegisterQueryHandlerClient(context.Background(), mux, v1beta1.NewQueryClient(clientCtx)); err != nil { + panic(err) + } +} + +// GetTxCmd returns the root tx command for the gov module. +func (a AppModuleBasic) GetTxCmd() *cobra.Command { + legacyProposalCLIHandlers := getProposalCLIHandlers(a.legacyProposalHandlers) + + return cli.NewTxCmd(legacyProposalCLIHandlers) +} + +func getProposalCLIHandlers(handlers []govclient.ProposalHandler) []*cobra.Command { + proposalCLIHandlers := make([]*cobra.Command, 0, len(handlers)) + for _, proposalHandler := range handlers { + proposalCLIHandlers = append(proposalCLIHandlers, proposalHandler.CLIHandler()) + } + return proposalCLIHandlers +} + +// GetQueryCmd returns the root query command for the gov module. +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return cli.GetQueryCmd() +} + +// RegisterInterfaces implements InterfaceModule.RegisterInterfaces +func (a AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) { + v1.RegisterInterfaces(registry) + v1beta1.RegisterInterfaces(registry) +} + +// AppModule implements an application module for the gov module. +type AppModule struct { + AppModuleBasic + + keeper *keeper.Keeper + accountKeeper govtypes.AccountKeeper + bankKeeper govtypes.BankKeeper + + // legacySubspace is used solely for migration of x/params managed parameters + legacySubspace govtypes.ParamSubspace +} + +// NewAppModule creates a new AppModule object +func NewAppModule( + cdc codec.Codec, keeper *keeper.Keeper, + ak govtypes.AccountKeeper, bk govtypes.BankKeeper, ss govtypes.ParamSubspace, +) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{cdc: cdc}, + keeper: keeper, + accountKeeper: ak, + bankKeeper: bk, + legacySubspace: ss, + } +} + +var _ appmodule.AppModule = AppModule{} + +// IsOnePerModuleType implements the depinject.OnePerModuleType interface. +func (am AppModule) IsOnePerModuleType() {} + +// IsAppModule implements the appmodule.AppModule interface. +func (am AppModule) IsAppModule() {} + +func init() { + appmodule.Register( + &modulev1.Module{}, + appmodule.Provide(ProvideModule, ProvideKeyTable), + appmodule.Invoke(InvokeAddRoutes, InvokeSetHooks)) +} + +type GovInputs struct { + depinject.In + + Config *modulev1.Module + Cdc codec.Codec + Key *store.KVStoreKey + ModuleKey depinject.OwnModuleKey + MsgServiceRouter *baseapp.MsgServiceRouter + + AccountKeeper govtypes.AccountKeeper + BankKeeper govtypes.BankKeeper + StakingKeeper govtypes.StakingKeeper + + // LegacySubspace is used solely for migration of x/params managed parameters + LegacySubspace govtypes.ParamSubspace `optional:"true"` +} + +type GovOutputs struct { + depinject.Out + + Module appmodule.AppModule + Keeper *keeper.Keeper + HandlerRoute v1beta1.HandlerRoute +} + +func ProvideModule(in GovInputs) GovOutputs { + kConfig := govtypes.DefaultConfig() + if in.Config.MaxMetadataLen != 0 { + kConfig.MaxMetadataLen = in.Config.MaxMetadataLen + } + + // default to governance authority if not provided + authority := authtypes.NewModuleAddress(govtypes.ModuleName) + if in.Config.Authority != "" { + authority = authtypes.NewModuleAddressOrBech32Address(in.Config.Authority) + } + + k := keeper.NewKeeper( + in.Cdc, + in.Key, + in.AccountKeeper, + in.BankKeeper, + in.StakingKeeper, + in.MsgServiceRouter, + kConfig, + authority.String(), + ) + m := NewAppModule(in.Cdc, k, in.AccountKeeper, in.BankKeeper, in.LegacySubspace) + hr := v1beta1.HandlerRoute{Handler: v1beta1.ProposalHandler, RouteKey: govtypes.RouterKey} + + return GovOutputs{Module: m, Keeper: k, HandlerRoute: hr} +} + +func ProvideKeyTable() paramtypes.KeyTable { + return v1.ParamKeyTable() //nolint:staticcheck +} + +func InvokeAddRoutes(keeper *keeper.Keeper, routes []v1beta1.HandlerRoute) { + if keeper == nil || routes == nil { + return + } + + // Default route order is a lexical sort by RouteKey. + // Explicit ordering can be added to the module config if required. + slices.SortFunc(routes, func(x, y v1beta1.HandlerRoute) bool { + return x.RouteKey < y.RouteKey + }) + + router := v1beta1.NewRouter() + for _, r := range routes { + router.AddRoute(r.RouteKey, r.Handler) + } + keeper.SetLegacyRouter(router) +} + +func InvokeSetHooks(keeper *keeper.Keeper, govHooks map[string]govtypes.GovHooksWrapper) error { + if keeper == nil || govHooks == nil { + return nil + } + + // Default ordering is lexical by module name. + // Explicit ordering can be added to the module config if required. + modNames := maps.Keys(govHooks) + order := modNames + sort.Strings(order) + + var multiHooks govtypes.MultiGovHooks + for _, modName := range order { + hook, ok := govHooks[modName] + if !ok { + return fmt.Errorf("can't find staking hooks for module %s", modName) + } + multiHooks = append(multiHooks, hook) + } + + keeper.SetHooks(multiHooks) + return nil +} + +// Name returns the gov module's name. +func (AppModule) Name() string { + return govtypes.ModuleName +} + +// RegisterInvariants registers module invariants +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + keeper.RegisterInvariants(ir, am.keeper, am.bankKeeper) +} + +// RegisterServices registers module services. +func (am AppModule) RegisterServices(cfg module.Configurator) { + msgServer := keeper.NewMsgServerImpl(am.keeper) + v1beta1.RegisterMsgServer(cfg.MsgServer(), keeper.NewLegacyMsgServerImpl(am.accountKeeper.GetModuleAddress(govtypes.ModuleName).String(), msgServer)) + v1.RegisterMsgServer(cfg.MsgServer(), msgServer) + + legacyQueryServer := keeper.NewLegacyQueryServer(am.keeper) + v1beta1.RegisterQueryServer(cfg.QueryServer(), legacyQueryServer) + v1.RegisterQueryServer(cfg.QueryServer(), am.keeper) + + m := keeper.NewMigrator(am.keeper, am.legacySubspace) + _ = m +} + +// InitGenesis performs genesis initialization for the gov module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState v1.GenesisState + cdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.accountKeeper, am.bankKeeper, am.keeper, &genesisState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the gov +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return cdc.MustMarshalJSON(gs) +} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion } + +// EndBlock returns the end blocker for the gov module. It returns no validator +// updates. +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + EndBlocker(ctx, am.keeper) + return []abci.ValidatorUpdate{} +} + +// AppModuleSimulation functions + +// GenerateGenesisState creates a randomized GenState of the gov module. +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simulation.RandomizedGenState(simState) +} + +// ProposalContents returns all the gov content functions used to +// simulate governance proposals. +func (AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { //nolint:staticcheck + return simulation.ProposalContents() +} + +// ProposalMsgs returns all the gov msgs used to simulate governance proposals. +func (AppModule) ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg { + return simulation.ProposalMsgs() +} + +// RegisterStoreDecoder registers a decoder for gov module's types +func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { + sdr[govtypes.StoreKey] = simulation.NewDecodeStore(am.cdc) +} + +// WeightedOperations returns the all the gov module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, + am.accountKeeper, am.bankKeeper, am.keeper, + simState.ProposalMsgs, simState.LegacyProposalContents, + ) +} diff --git a/x/gov/simulation/decoder.go b/x/gov/simulation/decoder.go new file mode 100644 index 00000000..25af6b25 --- /dev/null +++ b/x/gov/simulation/decoder.go @@ -0,0 +1,58 @@ +package simulation + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/kv" + "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" +) + +// NewDecodeStore returns a decoder function closure that unmarshals the KVPair's +// Value to the corresponding gov type. +func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { + return func(kvA, kvB kv.Pair) string { + switch { + case bytes.Equal(kvA.Key[:1], types.ProposalsKeyPrefix): + var proposalA v1beta1.Proposal + err := cdc.Unmarshal(kvA.Value, &proposalA) + if err != nil { + panic(err) + } + var proposalB v1beta1.Proposal + err = cdc.Unmarshal(kvB.Value, &proposalB) + if err != nil { + panic(err) + } + return fmt.Sprintf("%v\n%v", proposalA, proposalB) + + case bytes.Equal(kvA.Key[:1], types.ActiveProposalQueuePrefix), + bytes.Equal(kvA.Key[:1], types.InactiveProposalQueuePrefix), + bytes.Equal(kvA.Key[:1], types.ProposalIDKey): + proposalIDA := binary.LittleEndian.Uint64(kvA.Value) + proposalIDB := binary.LittleEndian.Uint64(kvB.Value) + return fmt.Sprintf("proposalIDA: %d\nProposalIDB: %d", proposalIDA, proposalIDB) + + case bytes.Equal(kvA.Key[:1], types.DepositsKeyPrefix): + var depositA, depositB v1beta1.Deposit + cdc.MustUnmarshal(kvA.Value, &depositA) + cdc.MustUnmarshal(kvB.Value, &depositB) + return fmt.Sprintf("%v\n%v", depositA, depositB) + + case bytes.Equal(kvA.Key[:1], types.VotesKeyPrefix): + var voteA, voteB v1beta1.Vote + cdc.MustUnmarshal(kvA.Value, &voteA) + cdc.MustUnmarshal(kvB.Value, &voteB) + return fmt.Sprintf("%v\n%v", voteA, voteB) + + case bytes.Equal(kvA.Key[:1], types.VotingPeriodProposalKeyPrefix): + return fmt.Sprintf("%v\n%v", kvA.Value, kvB.Value) + + default: + panic(fmt.Sprintf("invalid governance key prefix %X", kvA.Key[:1])) + } + } +} diff --git a/x/gov/simulation/decoder_test.go b/x/gov/simulation/decoder_test.go new file mode 100644 index 00000000..be84b904 --- /dev/null +++ b/x/gov/simulation/decoder_test.go @@ -0,0 +1,99 @@ +package simulation_test + +import ( + "encoding/binary" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/kv" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + + "github.com/atomone-hub/atomone/x/gov" + "github.com/atomone-hub/atomone/x/gov/simulation" +) + +var ( + delPk1 = ed25519.GenPrivKey().PubKey() + delAddr1 = sdk.AccAddress(delPk1.Address()) +) + +func TestDecodeStore(t *testing.T) { + cdc := moduletestutil.MakeTestEncodingConfig(gov.AppModuleBasic{}).Codec + dec := simulation.NewDecodeStore(cdc) + + endTime := time.Now().UTC() + content, ok := v1beta1.ContentFromProposalType("test", "test", v1beta1.ProposalTypeText) + require.True(t, ok) + proposalA, err := v1beta1.NewProposal(content, 1, endTime, endTime.Add(24*time.Hour)) + require.NoError(t, err) + proposalB, err := v1beta1.NewProposal(content, 2, endTime, endTime.Add(24*time.Hour)) + require.NoError(t, err) + + proposalIDBz := make([]byte, 8) + binary.LittleEndian.PutUint64(proposalIDBz, 1) + deposit := v1beta1.NewDeposit(1, delAddr1, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.OneInt()))) + vote := v1beta1.NewVote(1, delAddr1, v1beta1.NewNonSplitVoteOption(v1beta1.OptionYes)) + + proposalBzA, err := cdc.Marshal(&proposalA) + require.NoError(t, err) + proposalBzB, err := cdc.Marshal(&proposalB) + require.NoError(t, err) + + tests := []struct { + name string + kvA, kvB kv.Pair + expectedLog string + wantPanic bool + }{ + { + "proposals", + kv.Pair{Key: types.ProposalKey(1), Value: proposalBzA}, + kv.Pair{Key: types.ProposalKey(2), Value: proposalBzB}, + fmt.Sprintf("%v\n%v", proposalA, proposalB), false, + }, + { + "proposal IDs", + kv.Pair{Key: types.InactiveProposalQueueKey(1, endTime), Value: proposalIDBz}, + kv.Pair{Key: types.InactiveProposalQueueKey(1, endTime), Value: proposalIDBz}, + "proposalIDA: 1\nProposalIDB: 1", false, + }, + { + "deposits", + kv.Pair{Key: types.DepositKey(1, delAddr1), Value: cdc.MustMarshal(&deposit)}, + kv.Pair{Key: types.DepositKey(1, delAddr1), Value: cdc.MustMarshal(&deposit)}, + fmt.Sprintf("%v\n%v", deposit, deposit), false, + }, + { + "votes", + kv.Pair{Key: types.VoteKey(1, delAddr1), Value: cdc.MustMarshal(&vote)}, + kv.Pair{Key: types.VoteKey(1, delAddr1), Value: cdc.MustMarshal(&vote)}, + fmt.Sprintf("%v\n%v", vote, vote), false, + }, + { + "other", + kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}}, + kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}}, + "", true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if tt.wantPanic { + require.Panics(t, func() { dec(tt.kvA, tt.kvB) }, tt.name) + } else { + require.Equal(t, tt.expectedLog, dec(tt.kvA, tt.kvB), tt.name) + } + }) + } +} diff --git a/x/gov/simulation/genesis.go b/x/gov/simulation/genesis.go new file mode 100644 index 00000000..b6c4e1e0 --- /dev/null +++ b/x/gov/simulation/genesis.go @@ -0,0 +1,123 @@ +package simulation + +// DONTCOVER + +import ( + "encoding/json" + "fmt" + "math/rand" + "time" + + "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// Simulation parameter constants +const ( + DepositParamsMinDeposit = "deposit_params_min_deposit" + DepositParamsDepositPeriod = "deposit_params_deposit_period" + DepositMinInitialRatio = "deposit_params_min_initial_ratio" + VotingParamsVotingPeriod = "voting_params_voting_period" + TallyParamsQuorum = "tally_params_quorum" + TallyParamsThreshold = "tally_params_threshold" + TallyParamsVeto = "tally_params_veto" +) + +// GenDepositParamsDepositPeriod returns randomized DepositParamsDepositPeriod +func GenDepositParamsDepositPeriod(r *rand.Rand) time.Duration { + return time.Duration(simulation.RandIntBetween(r, 1, 2*60*60*24*2)) * time.Second +} + +// GenDepositParamsMinDeposit returns randomized DepositParamsMinDeposit +func GenDepositParamsMinDeposit(r *rand.Rand) sdk.Coins { + return sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, int64(simulation.RandIntBetween(r, 1, 1e3)))) +} + +// GenDepositMinInitialRatio returns randomized DepositMinInitialRatio +func GenDepositMinInitialDepositRatio(r *rand.Rand) sdk.Dec { + return sdk.NewDec(int64(simulation.RandIntBetween(r, 0, 99))).Quo(sdk.NewDec(100)) +} + +// GenVotingParamsVotingPeriod returns randomized VotingParamsVotingPeriod +func GenVotingParamsVotingPeriod(r *rand.Rand) time.Duration { + return time.Duration(simulation.RandIntBetween(r, 1, 2*60*60*24*2)) * time.Second +} + +// GenTallyParamsQuorum returns randomized TallyParamsQuorum +func GenTallyParamsQuorum(r *rand.Rand) math.LegacyDec { + return sdk.NewDecWithPrec(int64(simulation.RandIntBetween(r, 334, 500)), 3) +} + +// GenTallyParamsThreshold returns randomized TallyParamsThreshold +func GenTallyParamsThreshold(r *rand.Rand) math.LegacyDec { + return sdk.NewDecWithPrec(int64(simulation.RandIntBetween(r, 450, 550)), 3) +} + +// GenTallyParamsVeto returns randomized TallyParamsVeto +func GenTallyParamsVeto(r *rand.Rand) math.LegacyDec { + return sdk.NewDecWithPrec(int64(simulation.RandIntBetween(r, 250, 334)), 3) +} + +// RandomizedGenState generates a random GenesisState for gov +func RandomizedGenState(simState *module.SimulationState) { + startingProposalID := uint64(simState.Rand.Intn(100)) + + var minDeposit sdk.Coins + simState.AppParams.GetOrGenerate( + simState.Cdc, DepositParamsMinDeposit, &minDeposit, simState.Rand, + func(r *rand.Rand) { minDeposit = GenDepositParamsMinDeposit(r) }, + ) + + var depositPeriod time.Duration + simState.AppParams.GetOrGenerate( + simState.Cdc, DepositParamsDepositPeriod, &depositPeriod, simState.Rand, + func(r *rand.Rand) { depositPeriod = GenDepositParamsDepositPeriod(r) }, + ) + + var minInitialDepositRatio sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, DepositMinInitialRatio, &minInitialDepositRatio, simState.Rand, + func(r *rand.Rand) { minInitialDepositRatio = GenDepositMinInitialDepositRatio(r) }, + ) + + var votingPeriod time.Duration + simState.AppParams.GetOrGenerate( + simState.Cdc, VotingParamsVotingPeriod, &votingPeriod, simState.Rand, + func(r *rand.Rand) { votingPeriod = GenVotingParamsVotingPeriod(r) }, + ) + + var quorum sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, TallyParamsQuorum, &quorum, simState.Rand, + func(r *rand.Rand) { quorum = GenTallyParamsQuorum(r) }, + ) + + var threshold sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, TallyParamsThreshold, &threshold, simState.Rand, + func(r *rand.Rand) { threshold = GenTallyParamsThreshold(r) }, + ) + + var veto sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, TallyParamsVeto, &veto, simState.Rand, + func(r *rand.Rand) { veto = GenTallyParamsVeto(r) }, + ) + + govGenesis := v1.NewGenesisState( + startingProposalID, + v1.NewParams(minDeposit, depositPeriod, votingPeriod, quorum.String(), threshold.String(), veto.String(), minInitialDepositRatio.String(), simState.Rand.Intn(2) == 0, simState.Rand.Intn(2) == 0, simState.Rand.Intn(2) == 0), + ) + + bz, err := json.MarshalIndent(&govGenesis, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("Selected randomly generated governance parameters:\n%s\n", bz) + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(govGenesis) +} diff --git a/x/gov/simulation/genesis_test.go b/x/gov/simulation/genesis_test.go new file mode 100644 index 00000000..d9b034a5 --- /dev/null +++ b/x/gov/simulation/genesis_test.go @@ -0,0 +1,89 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/atomone-hub/atomone/x/gov/simulation" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" +) + +// TestRandomizedGenState tests the normal scenario of applying RandomizedGenState. +// Abnormal scenarios are not tested here. +func TestRandomizedGenState(t *testing.T) { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + + s := rand.NewSource(1) + r := rand.New(s) + + simState := module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + NumBonded: 3, + Accounts: simtypes.RandomAccounts(r, 3), + InitialStake: sdkmath.NewInt(1000), + GenState: make(map[string]json.RawMessage), + } + + simulation.RandomizedGenState(&simState) + + var govGenesis v1.GenesisState + simState.Cdc.MustUnmarshalJSON(simState.GenState[types.ModuleName], &govGenesis) + + const ( + tallyQuorum = "0.400000000000000000" + tallyThreshold = "0.539000000000000000" + tallyVetoThreshold = "0.314000000000000000" + minInitialDepositDec = "0.590000000000000000" + ) + + require.Equal(t, "905stake", govGenesis.Params.MinDeposit[0].String()) + require.Equal(t, "77h26m10s", govGenesis.Params.MaxDepositPeriod.String()) + require.Equal(t, float64(275567), govGenesis.Params.VotingPeriod.Seconds()) + require.Equal(t, tallyQuorum, govGenesis.Params.Quorum) + require.Equal(t, tallyThreshold, govGenesis.Params.Threshold) + require.Equal(t, tallyVetoThreshold, govGenesis.Params.VetoThreshold) + require.Equal(t, uint64(0x28), govGenesis.StartingProposalId) + require.Equal(t, []*v1.Deposit{}, govGenesis.Deposits) + require.Equal(t, []*v1.Vote{}, govGenesis.Votes) + require.Equal(t, []*v1.Proposal{}, govGenesis.Proposals) +} + +// TestRandomizedGenState tests abnormal scenarios of applying RandomizedGenState. +func TestRandomizedGenState1(t *testing.T) { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + + s := rand.NewSource(1) + r := rand.New(s) + // all these tests will panic + tests := []struct { + simState module.SimulationState + panicMsg string + }{ + { // panic => reason: incomplete initialization of the simState + module.SimulationState{}, "invalid memory address or nil pointer dereference"}, + { // panic => reason: incomplete initialization of the simState + module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + }, "assignment to entry in nil map"}, + } + + for _, tt := range tests { + require.Panicsf(t, func() { simulation.RandomizedGenState(&tt.simState) }, tt.panicMsg) + } +} diff --git a/x/gov/simulation/operations.go b/x/gov/simulation/operations.go new file mode 100644 index 00000000..9b9cf43b --- /dev/null +++ b/x/gov/simulation/operations.go @@ -0,0 +1,552 @@ +package simulation + +import ( + "math" + "math/rand" + "time" + + "github.com/atomone-hub/atomone/x/gov/keeper" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +var initialProposalID = uint64(100000000000000) + +// Governance message types and routes +var ( + TypeMsgDeposit = sdk.MsgTypeURL(&v1.MsgDeposit{}) + TypeMsgVote = sdk.MsgTypeURL(&v1.MsgVote{}) + TypeMsgVoteWeighted = sdk.MsgTypeURL(&v1.MsgVoteWeighted{}) + TypeMsgSubmitProposal = sdk.MsgTypeURL(&v1.MsgSubmitProposal{}) +) + +// Simulation operation weights constants +// +//nolint:gosec // these are not hard-coded credentials. +const ( + OpWeightMsgDeposit = "op_weight_msg_deposit" + OpWeightMsgVote = "op_weight_msg_vote" + OpWeightMsgVoteWeighted = "op_weight_msg_weighted_vote" + + DefaultWeightMsgDeposit = 100 + DefaultWeightMsgVote = 67 + DefaultWeightMsgVoteWeighted = 33 + DefaultWeightTextProposal = 5 +) + +// WeightedOperations returns all the operations from the module with their respective weights +func WeightedOperations(appParams simtypes.AppParams, cdc codec.JSONCodec, ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, wMsgs []simtypes.WeightedProposalMsg, wContents []simtypes.WeightedProposalContent) simulation.WeightedOperations { //nolint:staticcheck + var ( + weightMsgDeposit int + weightMsgVote int + weightMsgVoteWeighted int + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgDeposit, &weightMsgDeposit, nil, + func(_ *rand.Rand) { + weightMsgDeposit = DefaultWeightMsgDeposit + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgVote, &weightMsgVote, nil, + func(_ *rand.Rand) { + weightMsgVote = DefaultWeightMsgVote + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgVoteWeighted, &weightMsgVoteWeighted, nil, + func(_ *rand.Rand) { + weightMsgVoteWeighted = DefaultWeightMsgVoteWeighted + }, + ) + + // generate the weighted operations for the proposal contents + var wProposalOps simulation.WeightedOperations + for _, wMsg := range wMsgs { + wMsg := wMsg // pin variable + var weight int + appParams.GetOrGenerate(cdc, wMsg.AppParamsKey(), &weight, nil, + func(_ *rand.Rand) { weight = wMsg.DefaultWeight() }, + ) + + wProposalOps = append( + wProposalOps, + simulation.NewWeightedOperation( + weight, + SimulateMsgSubmitProposal(ak, bk, k, wMsg.MsgSimulatorFn()), + ), + ) + } + + // generate the weighted operations for the proposal contents + var wLegacyProposalOps simulation.WeightedOperations + for _, wContent := range wContents { + wContent := wContent // pin variable + var weight int + appParams.GetOrGenerate(cdc, wContent.AppParamsKey(), &weight, nil, + func(_ *rand.Rand) { weight = wContent.DefaultWeight() }, + ) + + wLegacyProposalOps = append( + wLegacyProposalOps, + simulation.NewWeightedOperation( + weight, + SimulateMsgSubmitLegacyProposal(ak, bk, k, wContent.ContentSimulatorFn()), + ), + ) + } + + wGovOps := simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgDeposit, + SimulateMsgDeposit(ak, bk, k), + ), + simulation.NewWeightedOperation( + weightMsgVote, + SimulateMsgVote(ak, bk, k), + ), + simulation.NewWeightedOperation( + weightMsgVoteWeighted, + SimulateMsgVoteWeighted(ak, bk, k), + ), + } + + return append(wGovOps, append(wProposalOps, wLegacyProposalOps...)...) +} + +// SimulateMsgSubmitProposal simulates creating a msg Submit Proposal +// voting on the proposal, and subsequently slashing the proposal. It is implemented using +// future operations. +func SimulateMsgSubmitProposal(ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, msgSim simtypes.MsgSimulatorFn) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msgs := []sdk.Msg{} + proposalMsg := msgSim(r, ctx, accs) + if proposalMsg != nil { + msgs = append(msgs, proposalMsg) + } + + return simulateMsgSubmitProposal(ak, bk, k, msgs)(r, app, ctx, accs, chainID) + } +} + +// SimulateMsgSubmitLegacyProposal simulates creating a msg Submit Proposal +// voting on the proposal, and subsequently slashing the proposal. It is implemented using +// future operations. +func SimulateMsgSubmitLegacyProposal(ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, contentSim simtypes.ContentSimulatorFn) simtypes.Operation { //nolint:staticcheck + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + // 1) submit proposal now + content := contentSim(r, ctx, accs) + if content == nil { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "content is nil"), nil, nil + } + + govacc := k.GetGovernanceAccount(ctx) + contentMsg, err := v1.NewLegacyContent(content, govacc.GetAddress().String()) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "error converting legacy content into proposal message"), nil, err + } + + return simulateMsgSubmitProposal(ak, bk, k, []sdk.Msg{contentMsg})(r, app, ctx, accs, chainID) + } +} + +func simulateMsgSubmitProposal(ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, proposalMsgs []sdk.Msg) simtypes.Operation { + // The states are: + // column 1: All validators vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, values chosen by @valardragon semi-arbitrarily, + // feel free to change. + numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + deposit, skip, err := randomDeposit(r, ctx, ak, bk, k, simAccount.Address, true) + switch { + case skip: + return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "skip deposit"), nil, nil + case err != nil: + return simtypes.NoOpMsg(types.ModuleName, TypeMsgSubmitProposal, "unable to generate deposit"), nil, err + } + + msg, err := v1.NewMsgSubmitProposal( + proposalMsgs, + deposit, + simAccount.Address.String(), + simtypes.RandStringOfLength(r, 100), + simtypes.RandStringOfLength(r, 100), + simtypes.RandStringOfLength(r, 100), + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate a submit proposal msg"), nil, err + } + + account := ak.GetAccount(ctx, simAccount.Address) + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{msg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(msg, true, "", nil) + + // get the submitted proposal ID + proposalID, err := k.GetProposalID(ctx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate proposalID"), nil, err + } + + // 2) Schedule operations for votes + // 2.1) first pick a number of people to vote. + curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(accs)) * statePercentageArray[curNumVotesState])) + + // 2.2) select who votes and when + whoVotes := r.Perm(len(accs)) + + // didntVote := whoVotes[numVotes:] + whoVotes = whoVotes[:numVotes] + votingPeriod := k.GetParams(ctx).VotingPeriod + + fops := make([]simtypes.FutureOperation, numVotes+1) + for i := 0; i < numVotes; i++ { + whenVote := ctx.BlockHeader().Time.Add(time.Duration(r.Int63n(int64(votingPeriod.Seconds()))) * time.Second) + fops[i] = simtypes.FutureOperation{ + BlockTime: whenVote, + Op: operationSimulateMsgVote(ak, bk, k, accs[whoVotes[i]], int64(proposalID)), + } + } + + return opMsg, fops, nil + } +} + +// SimulateMsgDeposit generates a MsgDeposit with random values. +func SimulateMsgDeposit(ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + proposalID, ok := randomProposalID(r, k, ctx, v1.StatusDepositPeriod) + if !ok { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to generate proposalID"), nil, nil + } + + deposit, skip, err := randomDeposit(r, ctx, ak, bk, k, simAccount.Address, false) + switch { + case skip: + return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "skip deposit"), nil, nil + case err != nil: + return simtypes.NoOpMsg(types.ModuleName, TypeMsgDeposit, "unable to generate deposit"), nil, err + } + + msg := v1.NewMsgDeposit(simAccount.Address, proposalID, deposit) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + var fees sdk.Coins + coins, hasNeg := spendable.SafeSub(deposit...) + if !hasNeg { + fees, err = simtypes.RandomFees(r, ctx, coins) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate fees"), nil, err + } + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + ModuleName: types.ModuleName, + } + + return simulation.GenAndDeliverTx(txCtx, fees) + } +} + +// SimulateMsgVote generates a MsgVote with random values. +func SimulateMsgVote(ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper) simtypes.Operation { + return operationSimulateMsgVote(ak, bk, k, simtypes.Account{}, -1) +} + +func operationSimulateMsgVote(ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, simAccount simtypes.Account, proposalIDInt int64) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + if simAccount.Equals(simtypes.Account{}) { + simAccount, _ = simtypes.RandomAcc(r, accs) + } + + var proposalID uint64 + + switch { + case proposalIDInt < 0: + var ok bool + proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod) + if !ok { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgVote, "unable to generate proposalID"), nil, nil + } + default: + proposalID = uint64(proposalIDInt) + } + + option := randomVotingOption(r) + msg := v1.NewMsgVote(simAccount.Address, proposalID, option, "") + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateMsgVoteWeighted generates a MsgVoteWeighted with random values. +func SimulateMsgVoteWeighted(ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper) simtypes.Operation { + return operationSimulateMsgVoteWeighted(ak, bk, k, simtypes.Account{}, -1) +} + +func operationSimulateMsgVoteWeighted(ak types.AccountKeeper, bk types.BankKeeper, k *keeper.Keeper, simAccount simtypes.Account, proposalIDInt int64) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + if simAccount.Equals(simtypes.Account{}) { + simAccount, _ = simtypes.RandomAcc(r, accs) + } + + var proposalID uint64 + + switch { + case proposalIDInt < 0: + var ok bool + proposalID, ok = randomProposalID(r, k, ctx, v1.StatusVotingPeriod) + if !ok { + return simtypes.NoOpMsg(types.ModuleName, TypeMsgVoteWeighted, "unable to generate proposalID"), nil, nil + } + default: + proposalID = uint64(proposalIDInt) + } + + options := randomWeightedVotingOptions(r) + msg := v1.NewMsgVoteWeighted(simAccount.Address, proposalID, options, "") + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +// Pick a random deposit with a random denomination with a +// deposit amount between (0, min(balance, minDepositAmount)) +// This is to simulate multiple users depositing to get the +// proposal above the minimum deposit amount +func randomDeposit( + r *rand.Rand, + ctx sdk.Context, + ak types.AccountKeeper, + bk types.BankKeeper, + k *keeper.Keeper, + addr sdk.AccAddress, + useMinAmount bool, +) (deposit sdk.Coins, skip bool, err error) { + account := ak.GetAccount(ctx, addr) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + if spendable.Empty() { + return nil, true, nil // skip + } + + params := k.GetParams(ctx) + minDeposit := params.MinDeposit + denomIndex := r.Intn(len(minDeposit)) + denom := minDeposit[denomIndex].Denom + + spendableBalance := spendable.AmountOf(denom) + if spendableBalance.IsZero() { + return nil, true, nil + } + + minDepositAmount := minDeposit[denomIndex].Amount + + minAmount := sdk.ZeroInt() + if useMinAmount { + minDepositPercent, err := sdk.NewDecFromStr(params.MinInitialDepositRatio) + if err != nil { + return nil, false, err + } + + minAmount = sdk.NewDecFromInt(minDepositAmount).Mul(minDepositPercent).TruncateInt() + } + + amount, err := simtypes.RandPositiveInt(r, minDepositAmount.Sub(minAmount)) + if err != nil { + return nil, false, err + } + amount = amount.Add(minAmount) + + if amount.GT(spendableBalance) { + return nil, true, nil + } + + return sdk.Coins{sdk.NewCoin(denom, amount)}, false, nil +} + +// Pick a random proposal ID between the initial proposal ID +// (defined in gov GenesisState) and the latest proposal ID +// that matches a given Status. +// It does not provide a default ID. +func randomProposalID(r *rand.Rand, k *keeper.Keeper, ctx sdk.Context, status v1.ProposalStatus) (proposalID uint64, found bool) { + proposalID, _ = k.GetProposalID(ctx) + + switch { + case proposalID > initialProposalID: + // select a random ID between [initialProposalID, proposalID] + proposalID = uint64(simtypes.RandIntBetween(r, int(initialProposalID), int(proposalID))) + + default: + // This is called on the first call to this funcion + // in order to update the global variable + initialProposalID = proposalID + } + + proposal, ok := k.GetProposal(ctx, proposalID) + if !ok || proposal.Status != status { + return proposalID, false + } + + return proposalID, true +} + +// Pick a random voting option +func randomVotingOption(r *rand.Rand) v1.VoteOption { + switch r.Intn(4) { + case 0: + return v1.OptionYes + case 1: + return v1.OptionAbstain + case 2: + return v1.OptionNo + case 3: + return v1.OptionNoWithVeto + default: + panic("invalid vote option") + } +} + +// Pick a random weighted voting options +func randomWeightedVotingOptions(r *rand.Rand) v1.WeightedVoteOptions { + w1 := r.Intn(100 + 1) + w2 := r.Intn(100 - w1 + 1) + w3 := r.Intn(100 - w1 - w2 + 1) + w4 := 100 - w1 - w2 - w3 + weightedVoteOptions := v1.WeightedVoteOptions{} + if w1 > 0 { + weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ + Option: v1.OptionYes, + Weight: sdk.NewDecWithPrec(int64(w1), 2).String(), + }) + } + if w2 > 0 { + weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ + Option: v1.OptionAbstain, + Weight: sdk.NewDecWithPrec(int64(w2), 2).String(), + }) + } + if w3 > 0 { + weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ + Option: v1.OptionNo, + Weight: sdk.NewDecWithPrec(int64(w3), 2).String(), + }) + } + if w4 > 0 { + weightedVoteOptions = append(weightedVoteOptions, &v1.WeightedVoteOption{ + Option: v1.OptionNoWithVeto, + Weight: sdk.NewDecWithPrec(int64(w4), 2).String(), + }) + } + return weightedVoteOptions +} diff --git a/x/gov/simulation/operations_test.go b/x/gov/simulation/operations_test.go new file mode 100644 index 00000000..87421715 --- /dev/null +++ b/x/gov/simulation/operations_test.go @@ -0,0 +1,378 @@ +package simulation_test + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + abci "github.com/cometbft/cometbft/abci/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/atomone-hub/atomone/x/gov/keeper" + "github.com/atomone-hub/atomone/x/gov/simulation" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/testutil/configurator" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + _ "github.com/cosmos/cosmos-sdk/x/auth" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + _ "github.com/cosmos/cosmos-sdk/x/auth/tx/config" + _ "github.com/cosmos/cosmos-sdk/x/bank" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/bank/testutil" + _ "github.com/cosmos/cosmos-sdk/x/consensus" + govcodec "github.com/cosmos/cosmos-sdk/x/gov/codec" + "github.com/cosmos/cosmos-sdk/x/gov/types" + v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + _ "github.com/cosmos/cosmos-sdk/x/params" + _ "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" +) + +var ( + _ simtypes.WeightedProposalMsg = MockWeightedProposals{} + _ simtypes.WeightedProposalContent = MockWeightedProposals{} //nolint:staticcheck +) + +type MockWeightedProposals struct { + n int +} + +func (m MockWeightedProposals) AppParamsKey() string { + return fmt.Sprintf("AppParamsKey-%d", m.n) +} + +func (m MockWeightedProposals) DefaultWeight() int { + return m.n +} + +func (m MockWeightedProposals) MsgSimulatorFn() simtypes.MsgSimulatorFn { + return func(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { + return nil + } +} + +func (m MockWeightedProposals) ContentSimulatorFn() simtypes.ContentSimulatorFn { //nolint:staticcheck + return func(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) simtypes.Content { //nolint:staticcheck + return v1beta1.NewTextProposal( + fmt.Sprintf("title-%d: %s", m.n, simtypes.RandStringOfLength(r, 100)), + fmt.Sprintf("description-%d: %s", m.n, simtypes.RandStringOfLength(r, 4000)), + ) + } +} + +func mockWeightedProposalMsg(n int) []simtypes.WeightedProposalMsg { + wpc := make([]simtypes.WeightedProposalMsg, n) + for i := 0; i < n; i++ { + wpc[i] = MockWeightedProposals{i} + } + return wpc +} + +func mockWeightedLegacyProposalContent(n int) []simtypes.WeightedProposalContent { //nolint:staticcheck + wpc := make([]simtypes.WeightedProposalContent, n) //nolint:staticcheck + for i := 0; i < n; i++ { + wpc[i] = MockWeightedProposals{i} + } + return wpc +} + +// TestWeightedOperations tests the weights of the operations. +func TestWeightedOperations(t *testing.T) { + suite, ctx := createTestSuite(t, false) + app := suite.App + ctx.WithChainID("test-chain") + appParams := make(simtypes.AppParams) + + weightesOps := simulation.WeightedOperations(appParams, govcodec.ModuleCdc, suite.AccountKeeper, + suite.BankKeeper, suite.GovKeeper, mockWeightedProposalMsg(3), mockWeightedLegacyProposalContent(1), + ) + + // setup 3 accounts + s := rand.NewSource(1) + r := rand.New(s) + accs := getTestingAccounts(t, r, suite.AccountKeeper, suite.BankKeeper, suite.StakingKeeper, ctx, 3) + + expected := []struct { + weight int + opMsgRoute string + opMsgName string + }{ + {simulation.DefaultWeightMsgDeposit, types.ModuleName, simulation.TypeMsgDeposit}, + {simulation.DefaultWeightMsgVote, types.ModuleName, simulation.TypeMsgVote}, + {simulation.DefaultWeightMsgVoteWeighted, types.ModuleName, simulation.TypeMsgVoteWeighted}, + {0, types.ModuleName, simulation.TypeMsgSubmitProposal}, + {1, types.ModuleName, simulation.TypeMsgSubmitProposal}, + {2, types.ModuleName, simulation.TypeMsgSubmitProposal}, + {0, types.ModuleName, simulation.TypeMsgSubmitProposal}, + } + + require.Equal(t, len(weightesOps), len(expected), "number of operations should be the same") + for i, w := range weightesOps { + operationMsg, _, err := w.Op()(r, app.BaseApp, ctx, accs, ctx.ChainID()) + require.NoError(t, err) + + // the following checks are very much dependent from the ordering of the output given + // by WeightedOperations. if the ordering in WeightedOperations changes some tests + // will fail + require.Equal(t, expected[i].weight, w.Weight(), "weight should be the same") + require.Equal(t, expected[i].opMsgRoute, operationMsg.Route, "route should be the same") + require.Equal(t, expected[i].opMsgName, operationMsg.Name, "operation Msg name should be the same") + } +} + +// TestSimulateMsgSubmitProposal tests the normal scenario of a valid message of type TypeMsgSubmitProposal. +// Abnormal scenarios, where errors occur, are not tested here. +func TestSimulateMsgSubmitProposal(t *testing.T) { + suite, ctx := createTestSuite(t, false) + app := suite.App + + // setup 3 accounts + s := rand.NewSource(1) + r := rand.New(s) + accounts := getTestingAccounts(t, r, suite.AccountKeeper, suite.BankKeeper, suite.StakingKeeper, ctx, 3) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + + // execute operation + op := simulation.SimulateMsgSubmitProposal(suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper, MockWeightedProposals{3}.MsgSimulatorFn()) + operationMsg, _, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg v1.MsgSubmitProposal + err = govcodec.ModuleCdc.UnmarshalJSON(operationMsg.Msg, &msg) + require.NoError(t, err) + + require.True(t, operationMsg.OK) + require.Equal(t, "cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r", msg.Proposer) + require.NotEqual(t, len(msg.InitialDeposit), 0) + require.Equal(t, "560969stake", msg.InitialDeposit[0].String()) + require.Equal(t, simulation.TypeMsgSubmitProposal, sdk.MsgTypeURL(&msg)) +} + +// TestSimulateMsgSubmitProposal tests the normal scenario of a valid message of type TypeMsgSubmitProposal. +// Abnormal scenarios, where errors occur, are not tested here. +func TestSimulateMsgSubmitLegacyProposal(t *testing.T) { + suite, ctx := createTestSuite(t, false) + app := suite.App + + // setup 3 accounts + s := rand.NewSource(1) + r := rand.New(s) + accounts := getTestingAccounts(t, r, suite.AccountKeeper, suite.BankKeeper, suite.StakingKeeper, ctx, 3) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + + // execute operation + op := simulation.SimulateMsgSubmitLegacyProposal(suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper, MockWeightedProposals{3}.ContentSimulatorFn()) + operationMsg, _, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg v1.MsgSubmitProposal + err = govcodec.ModuleCdc.UnmarshalJSON(operationMsg.Msg, &msg) + require.NoError(t, err) + + require.True(t, operationMsg.OK) + require.Equal(t, "cosmos1p8wcgrjr4pjju90xg6u9cgq55dxwq8j7u4x9a0", msg.Proposer) + require.NotEqual(t, len(msg.InitialDeposit), 0) + require.Equal(t, "2686011stake", msg.InitialDeposit[0].String()) + require.Equal(t, "title-3: ZBSpYuLyYggwexjxusrBqDOTtGTOWeLrQKjLxzIivHSlcxgdXhhuTSkuxKGLwQvuyNhYFmBZHeAerqyNEUzXPFGkqEGqiQWIXnku", msg.Messages[0].GetCachedValue().(*v1.MsgExecLegacyContent).Content.GetCachedValue().(v1beta1.Content).GetTitle()) + require.Equal(t, "description-3: NJWzHdBNpAXKJPHWQdrGYcAHSctgVlqwqHoLfHsXUdStwfefwzqLuKEhmMyYLdbZrcPgYqjNHxPexsruwEGStAneKbWkQDDIlCWBLSiAASNhZqNFlPtfqPJoxKsgMdzjWqLWdqKQuJqWPMvwPQWZUtVMOTMYKJbfdlZsjdsomuScvDmbDkgRualsxDvRJuCAmPOXitIbcyWsKGSdrEunFAOdmXnsuyFVgJqEjbklvmwrUlsxjRSfKZxGcpayDdgoFcnVSutxjRgOSFzPwidAjubMncNweqpbxhXGchpZUxuFDOtpnhNUycJICRYqsPhPSCjPTWZFLkstHWJxvdPEAyEIxXgLwbNOjrgzmaujiBABBIXvcXpLrbcEWNNQsbjvgJFgJkflpRohHUutvnaUqoopuKjTDaemDeSdqbnOzcfJpcTuAQtZoiLZOoAIlboFDAeGmSNwkvObPRvRWQgWkGkxwtPauYgdkmypLjbqhlHJIQTntgWjXwZdOyYEdQRRLfMSdnxqppqUofqLbLQDUjwKVKfZJUJQPsWIPwIVaSTrmKskoAhvmZyJgeRpkaTfGgrJzAigcxtfshmiDCFkuiluqtMOkidknnTBtumyJYlIsWLnCQclqdVmikUoMOPdPWwYbJxXyqUVicNxFxyqJTenNblyyKSdlCbiXxUiYUiMwXZASYfvMDPFgxniSjWaZTjHkqlJvtBsXqwPpyVxnJVGFWhfSxgOcduoxkiopJvFjMmFabrGYeVtTXLhxVUEiGwYUvndjFGzDVntUvibiyZhfMQdMhgsiuysLMiePBNXifRLMsSmXPkwlPloUbJveCvUlaalhZHuvdkCnkSHbMbmOnrfEGPwQiACiPlnihiaOdbjPqPiTXaHDoJXjSlZmltGqNHHNrcKdlFSCdmVOuvDcBLdSklyGJmcLTbSFtALdGlPkqqecJrpLCXNPWefoTJNgEJlyMEPneVaxxduAAEqQpHWZodWyRkDAxzyMnFMcjSVqeRXLqsNyNtQBbuRvunZflWSbbvXXdkyLikYqutQhLPONXbvhcQZJPSWnOulqQaXmbfFxAkqfYeseSHOQidHwbcsOaMnSrrmGjjRmEMQNuknupMxJiIeVjmgZvbmjPIQTEhQFULQLBMPrxcFPvBinaOPYWGvYGRKxLZdwamfRQQFngcdSlvwjfaPbURasIsGJVHtcEAxnIIrhSriiXLOlbEBLXFElXJFGxHJczRBIxAuPKtBisjKBwfzZFagdNmjdwIRvwzLkFKWRTDPxJCmpzHUcrPiiXXHnOIlqNVoGSXZewdnCRhuxeYGPVTfrNTQNOxZmxInOazUYNTNDgzsxlgiVEHPKMfbesvPHUqpNkUqbzeuzfdrsuLDpKHMUbBMKczKKWOdYoIXoPYtEjfOnlQLoGnbQUCuERdEFaptwnsHzTJDsuZkKtzMpFaZobynZdzNydEeJJHDYaQcwUxcqvwfWwNUsCiLvkZQiSfzAHftYgAmVsXgtmcYgTqJIawstRYJrZdSxlfRiqTufgEQVambeZZmaAyRQbcmdjVUZZCgqDrSeltJGXPMgZnGDZqISrGDOClxXCxMjmKqEPwKHoOfOeyGmqWqihqjINXLqnyTesZePQRqaWDQNqpLgNrAUKulklmckTijUltQKuWQDwpLmDyxLppPVMwsmBIpOwQttYFMjgJQZLYFPmxWFLIeZihkRNnkzoypBICIxgEuYsVWGIGRbbxqVasYnstWomJnHwmtOhAFSpttRYYzBmyEtZXiCthvKvWszTXDbiJbGXMcrYpKAgvUVFtdKUfvdMfhAryctklUCEdjetjuGNfJjajZtvzdYaqInKtFPPLYmRaXPdQzxdSQfmZDEVHlHGEGNSPRFJuIfKLLfUmnHxHnRjmzQPNlqrXgifUdzAGKVabYqvcDeYoTYgPsBUqehrBhmQUgTvDnsdpuhUoxskDdppTsYMcnDIPSwKIqhXDCIxOuXrywahvVavvHkPuaenjLmEbMgrkrQLHEAwrhHkPRNvonNQKqprqOFVZKAtpRSpvQUxMoXCMZLSSbnLEFsjVfANdQNQVwTmGxqVjVqRuxREAhuaDrFgEZpYKhwWPEKBevBfsOIcaZKyykQafzmGPLRAKDtTcJxJVgiiuUkmyMYuDUNEUhBEdoBLJnamtLmMJQgmLiUELIhLpiEvpOXOvXCPUeldLFqkKOwfacqIaRcnnZvERKRMCKUkMABbDHytQqQblrvoxOZkwzosQfDKGtIdfcXRJNqlBNwOCWoQBcEWyqrMlYZIAXYJmLfnjoJepgSFvrgajaBAIksoyeHqgqbGvpAstMIGmIhRYGGNPRIfOQKsGoKgxtsidhTaAePRCBFqZgPDWCIkqOJezGVkjfYUCZTlInbxBXwUAVRsxHTQtJFnnpmMvXDYCVlEmnZBKhmmxQOIQzxFWpJQkQoSAYzTEiDWEOsVLNrbfzeHFRyeYATakQQWmFDLPbVMCJcWjFGJjfqCoVzlbNNEsqxdSmNPjTjHYOkuEMFLkXYGaoJlraLqayMeCsTjWNRDPBywBJLAPVkGQqTwApVVwYAetlwSbzsdHWsTwSIcctkyKDuRWYDQikRqsKTMJchrliONJeaZIzwPQrNbTwxsGdwuduvibtYndRwpdsvyCktRHFalvUuEKMqXbItfGcNGWsGzubdPMYayOUOINjpcFBeESdwpdlTYmrPsLsVDhpTzoMegKrytNVZkfJRPuDCUXxSlSthOohmsuxmIZUedzxKmowKOdXTMcEtdpHaPWgIsIjrViKrQOCONlSuazmLuCUjLltOGXeNgJKedTVrrVCpWYWHyVrdXpKgNaMJVjbXxnVMSChdWKuZdqpisvrkBJPoURDYxWOtpjzZoOpWzyUuYNhCzRoHsMjmmWDcXzQiHIyjwdhPNwiPqFxeUfMVFQGImhykFgMIlQEoZCaRoqSBXTSWAeDumdbsOGtATwEdZlLfoBKiTvodQBGOEcuATWXfiinSjPmJKcWgQrTVYVrwlyMWhxqNbCMpIQNoSMGTiWfPTCezUjYcdWppnsYJihLQCqbNLRGgqrwHuIvsazapTpoPZIyZyeeSueJuTIhpHMEJfJpScshJubJGfkusuVBgfTWQoywSSliQQSfbvaHKiLnyjdSbpMkdBgXepoSsHnCQaYuHQqZsoEOmJCiuQUpJkmfyfbIShzlZpHFmLCsbknEAkKXKfRTRnuwdBeuOGgFbJLbDksHVapaRayWzwoYBEpmrlAxrUxYMUekKbpjPNfjUCjhbdMAnJmYQVZBQZkFVweHDAlaqJjRqoQPoOMLhyvYCzqEuQsAFoxWrzRnTVjStPadhsESlERnKhpEPsfDxNvxqcOyIulaCkmPdambLHvGhTZzysvqFauEgkFRItPfvisehFmoBhQqmkfbHVsgfHXDPJVyhwPllQpuYLRYvGodxKjkarnSNgsXoKEMlaSKxKdcVgvOkuLcfLFfdtXGTclqfPOfeoVLbqcjcXCUEBgAGplrkgsmIEhWRZLlGPGCwKWRaCKMkBHTAcypUrYjWwCLtOPVygMwMANGoQwFnCqFrUGMCRZUGJKTZIGPyldsifauoMnJPLTcDHmilcmahlqOELaAUYDBuzsVywnDQfwRLGIWozYaOAilMBcObErwgTDNGWnwQMUgFFSKtPDMEoEQCTKVREqrXZSGLqwTMcxHfWotDllNkIJPMbXzjDVjPOOjCFuIvTyhXKLyhUScOXvYthRXpPfKwMhptXaxIxgqBoUqzrWbaoLTVpQoottZyPFfNOoMioXHRuFwMRYUiKvcWPkrayyTLOCFJlAyslDameIuqVAuxErqFPEWIScKpBORIuZqoXlZuTvAjEdlEWDODFRregDTqGNoFBIHxvimmIZwLfFyKUfEWAnNBdtdzDmTPXtpHRGdIbuucfTjOygZsTxPjfweXhSUkMhPjMaxKlMIJMOXcnQfyzeOcbWwNbeH", msg.Messages[0].GetCachedValue().(*v1.MsgExecLegacyContent).Content.GetCachedValue().(v1beta1.Content).GetDescription()) + require.Equal(t, "gov", msg.Route()) + require.Equal(t, simulation.TypeMsgSubmitProposal, msg.Type()) +} + +// TestSimulateMsgDeposit tests the normal scenario of a valid message of type TypeMsgDeposit. +// Abnormal scenarios, where errors occur, are not tested here. +func TestSimulateMsgDeposit(t *testing.T) { + suite, ctx := createTestSuite(t, false) + app := suite.App + blockTime := time.Now().UTC() + ctx = ctx.WithBlockTime(blockTime) + + // setup 3 accounts + s := rand.NewSource(1) + r := rand.New(s) + accounts := getTestingAccounts(t, r, suite.AccountKeeper, suite.BankKeeper, suite.StakingKeeper, ctx, 3) + + // setup a proposal + content := v1beta1.NewTextProposal("Test", "description") + contentMsg, err := v1.NewLegacyContent(content, suite.GovKeeper.GetGovernanceAccount(ctx).GetAddress().String()) + require.NoError(t, err) + + submitTime := ctx.BlockHeader().Time + depositPeriod := suite.GovKeeper.GetParams(ctx).MaxDepositPeriod + + proposal, err := v1.NewProposal([]sdk.Msg{contentMsg}, 1, submitTime, submitTime.Add(*depositPeriod), "", "text proposal", "description", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + + suite.GovKeeper.SetProposal(ctx, proposal) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash, Time: blockTime}}) + + // execute operation + op := simulation.SimulateMsgDeposit(suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper) + operationMsg, _, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg v1.MsgDeposit + err = govcodec.ModuleCdc.UnmarshalJSON(operationMsg.Msg, &msg) + require.NoError(t, err) + + require.True(t, operationMsg.OK) + require.Equal(t, uint64(1), msg.ProposalId) + require.Equal(t, "cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r", msg.Depositor) + require.NotEqual(t, len(msg.Amount), 0) + require.Equal(t, "560969stake", msg.Amount[0].String()) + require.Equal(t, "gov", msg.Route()) + require.Equal(t, simulation.TypeMsgDeposit, msg.Type()) +} + +// TestSimulateMsgVote tests the normal scenario of a valid message of type TypeMsgVote. +// Abnormal scenarios, where errors occur, are not tested here. +func TestSimulateMsgVote(t *testing.T) { + suite, ctx := createTestSuite(t, false) + app := suite.App + blockTime := time.Now().UTC() + ctx = ctx.WithBlockTime(blockTime) + + // setup 3 accounts + s := rand.NewSource(1) + r := rand.New(s) + accounts := getTestingAccounts(t, r, suite.AccountKeeper, suite.BankKeeper, suite.StakingKeeper, ctx, 3) + + // setup a proposal + govAcc := suite.GovKeeper.GetGovernanceAccount(ctx).GetAddress().String() + contentMsg, err := v1.NewLegacyContent(v1beta1.NewTextProposal("Test", "description"), govAcc) + require.NoError(t, err) + + submitTime := ctx.BlockHeader().Time + depositPeriod := suite.GovKeeper.GetParams(ctx).MaxDepositPeriod + + proposal, err := v1.NewProposal([]sdk.Msg{contentMsg}, 1, submitTime, submitTime.Add(*depositPeriod), "", "text proposal", "description", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + + suite.GovKeeper.ActivateVotingPeriod(ctx, proposal) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash, Time: blockTime}}) + + // execute operation + op := simulation.SimulateMsgVote(suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper) + operationMsg, _, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg v1.MsgVote + govcodec.ModuleCdc.UnmarshalJSON(operationMsg.Msg, &msg) + + require.True(t, operationMsg.OK) + require.Equal(t, uint64(1), msg.ProposalId) + require.Equal(t, "cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r", msg.Voter) + require.Equal(t, v1.OptionYes, msg.Option) + require.Equal(t, "gov", msg.Route()) + require.Equal(t, simulation.TypeMsgVote, msg.Type()) +} + +// TestSimulateMsgVoteWeighted tests the normal scenario of a valid message of type TypeMsgVoteWeighted. +// Abnormal scenarios, where errors occur, are not tested here. +func TestSimulateMsgVoteWeighted(t *testing.T) { + suite, ctx := createTestSuite(t, false) + app := suite.App + blockTime := time.Now().UTC() + ctx = ctx.WithBlockTime(blockTime) + + // setup 3 accounts + s := rand.NewSource(1) + r := rand.New(s) + accounts := getTestingAccounts(t, r, suite.AccountKeeper, suite.BankKeeper, suite.StakingKeeper, ctx, 3) + + // setup a proposal + govAcc := suite.GovKeeper.GetGovernanceAccount(ctx).GetAddress().String() + contentMsg, err := v1.NewLegacyContent(v1beta1.NewTextProposal("Test", "description"), govAcc) + require.NoError(t, err) + submitTime := ctx.BlockHeader().Time + depositPeriod := suite.GovKeeper.GetParams(ctx).MaxDepositPeriod + + proposal, err := v1.NewProposal([]sdk.Msg{contentMsg}, 1, submitTime, submitTime.Add(*depositPeriod), "", "text proposal", "test", sdk.AccAddress("cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r")) + require.NoError(t, err) + + suite.GovKeeper.ActivateVotingPeriod(ctx, proposal) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash, Time: blockTime}}) + + // execute operation + op := simulation.SimulateMsgVoteWeighted(suite.AccountKeeper, suite.BankKeeper, suite.GovKeeper) + operationMsg, _, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg v1.MsgVoteWeighted + govcodec.ModuleCdc.UnmarshalJSON(operationMsg.Msg, &msg) + + require.True(t, operationMsg.OK) + require.Equal(t, uint64(1), msg.ProposalId) + require.Equal(t, "cosmos1ghekyjucln7y67ntx7cf27m9dpuxxemn4c8g4r", msg.Voter) + require.True(t, len(msg.Options) >= 1) + require.Equal(t, "gov", msg.Route()) + require.Equal(t, simulation.TypeMsgVoteWeighted, msg.Type()) +} + +type suite struct { + cdc codec.Codec + AccountKeeper authkeeper.AccountKeeper + BankKeeper bankkeeper.Keeper + GovKeeper *keeper.Keeper + StakingKeeper *stakingkeeper.Keeper + App *runtime.App +} + +// returns context and an app with updated mint keeper +func createTestSuite(t *testing.T, isCheckTx bool) (suite, sdk.Context) { + res := suite{} + + app, err := simtestutil.Setup(configurator.NewAppConfig( + configurator.AuthModule(), + configurator.TxModule(), + configurator.ParamsModule(), + configurator.BankModule(), + configurator.StakingModule(), + configurator.ConsensusModule(), + configurator.GovModule(), + ), &res.AccountKeeper, &res.BankKeeper, &res.GovKeeper, &res.StakingKeeper, &res.cdc) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{}) + + res.App = app + return res, ctx +} + +func getTestingAccounts( + t *testing.T, r *rand.Rand, + accountKeeper authkeeper.AccountKeeper, bankKeeper bankkeeper.Keeper, stakingKeeper *stakingkeeper.Keeper, + ctx sdk.Context, n int, +) []simtypes.Account { + accounts := simtypes.RandomAccounts(r, n) + + initAmt := stakingKeeper.TokensFromConsensusPower(ctx, 200) + initCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, initAmt)) + + // add coins to the accounts + for _, account := range accounts { + acc := accountKeeper.NewAccountWithAddress(ctx, account.Address) + accountKeeper.SetAccount(ctx, acc) + require.NoError(t, testutil.FundAccount(bankKeeper, ctx, account.Address, initCoins)) + } + + return accounts +} diff --git a/x/gov/simulation/proposals.go b/x/gov/simulation/proposals.go new file mode 100644 index 00000000..4afda5b0 --- /dev/null +++ b/x/gov/simulation/proposals.go @@ -0,0 +1,53 @@ +package simulation + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// OpWeightSubmitTextProposal app params key for text proposal +const OpWeightSubmitTextProposal = "op_weight_submit_text_proposal" + +// ProposalMsgs defines the module weighted proposals' contents +func ProposalMsgs() []simtypes.WeightedProposalMsg { + return []simtypes.WeightedProposalMsg{ + simulation.NewWeightedProposalMsg( + OpWeightSubmitTextProposal, + DefaultWeightTextProposal, + SimulateTextProposal, + ), + } +} + +// SimulateTextProposal returns a random text proposal content. +// A text proposal is a proposal that contains no msgs. +func SimulateTextProposal(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) sdk.Msg { + return nil +} + +// ProposalContents defines the module weighted proposals' contents +// +//nolint:staticcheck +func ProposalContents() []simtypes.WeightedProposalContent { + return []simtypes.WeightedProposalContent{ + simulation.NewWeightedProposalContent( + OpWeightMsgDeposit, + DefaultWeightTextProposal, + SimulateLegacyTextProposalContent, + ), + } +} + +// SimulateTextProposalContent returns a random text proposal content. +// +//nolint:staticcheck +func SimulateLegacyTextProposalContent(r *rand.Rand, _ sdk.Context, _ []simtypes.Account) simtypes.Content { + return v1beta1.NewTextProposal( + simtypes.RandStringOfLength(r, 140), + simtypes.RandStringOfLength(r, 5000), + ) +} diff --git a/x/gov/simulation/proposals_test.go b/x/gov/simulation/proposals_test.go new file mode 100644 index 00000000..dcb5e0d5 --- /dev/null +++ b/x/gov/simulation/proposals_test.go @@ -0,0 +1,63 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + "gotest.tools/v3/assert" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/atomone-hub/atomone/x/gov/simulation" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" +) + +func TestProposalMsgs(t *testing.T) { + // initialize parameters + s := rand.NewSource(1) + r := rand.New(s) + + ctx := sdk.NewContext(nil, tmproto.Header{}, true, nil) + accounts := simtypes.RandomAccounts(r, 3) + + // execute ProposalMsgs function + weightedProposalMsgs := simulation.ProposalMsgs() + assert.Assert(t, len(weightedProposalMsgs) == 1) + + w0 := weightedProposalMsgs[0] + + // tests w0 interface: + assert.Equal(t, simulation.OpWeightSubmitTextProposal, w0.AppParamsKey()) + assert.Equal(t, simulation.DefaultWeightTextProposal, w0.DefaultWeight()) + + msg := w0.MsgSimulatorFn()(r, ctx, accounts) + assert.Assert(t, msg == nil) +} + +func TestProposalContents(t *testing.T) { + // initialize parameters + s := rand.NewSource(1) + r := rand.New(s) + + ctx := sdk.NewContext(nil, tmproto.Header{}, true, nil) + accounts := simtypes.RandomAccounts(r, 3) + + // execute ProposalContents function + weightedProposalContent := simulation.ProposalContents() + assert.Assert(t, len(weightedProposalContent) == 1) + + w0 := weightedProposalContent[0] + + // tests w0 interface: + assert.Equal(t, simulation.OpWeightMsgDeposit, w0.AppParamsKey()) + assert.Equal(t, simulation.DefaultWeightTextProposal, w0.DefaultWeight()) + + content := w0.ContentSimulatorFn()(r, ctx, accounts) + + assert.Equal(t, "NxImpptHBIFDQfnxaTiOBJUgNzvqHbVcVJYlIFWFlzFqqRTTyFzDUMntPzyRamUFqeJAEaSHIuUHZoTWDjWXsYxYvwXwXZEsjRQKgKMselyUqWXMbHzRNDHnMzhWSirUgVggjiBxtWDfhzPDgrorEoNmDEiDdBldYegphCBTYWrmFFXNjxhtygsGBFHTejaKjMsqNdikEzDalEyWRHfJhKqifCKsedVuuJbQMbmRVuIPDluAWGpngjgBjOxuRFwSadayHNIhVVmNWBbfaTOldclxTTLUMvaBnLfwjHTtsKetEIvgrxLijhKJNablmvqpWIWsmhWQAYNLycREypoASHnyKWrxpoNLBJuyCGysZJgXbQAAmSIbGxMFXuwMVGZgBiZWfPWorAfjBeekCFvljHAtVZaTOsRxbPIioNxLTnWUTzGTvaNhplQQPmMADRRDuUIsiBpnGqPheKmLnopieVseFdTSAvOCacxaqFWFuXzsrVZzlGfeRpClwKuGEBujaPrzSLjVIOMvLlWxuznEOXlxbZroBRVEvEfBBAHOECribZNrYiFnzQqQmBnLksmFNAadusWAGltuqYNntgOlgOGwSdDjWdLboWyAWIcCfmpGJTfbljKPriLehwObuszICkaXNUkmeddeeRulbZBXJVLgteiKIfofGdNBregwUPlINQECatDSNXSIuefyMxxoKfcmjHEwbVtFiXtEnLJkLHUghmzFiymrgBChucZgOQUpGGVQEpRtIQjIBxYhtZPgUORdxXNWUMErWrUeriqYJPcgIDgLMWAyuuQnsHncCtjvHmvFbzYErxeunQllYDUVlXaRBveRUKeXwEGJFTSAqZtaBSDGDtzlADCnGjuTmYMJlapRsWfugmjwKEuoXJVpZvlcHeFvVvRRktRVGwzLfKezPEMABZtbLExQIjynSoahmkmoTHefdzFoBHMcQHFkKVHhpNtudPqJrYuQswzFuFHbSmpNltFnYJpvMrAYHFrNouZaanEUGHvbHIUUFTCtZrcpRHwgjblxlDNJWzHdBNpAXKJPHWQdrGYcAHSctgVlqwqHoLfHsXUdStwfefwzqLuKEhmMyYLdbZrcPgYqjNHxPexsruwEGStAneKbWkQDDIlCWBLSiAASNhZqNFlPtfqPJoxKsgMdzjWqLWdqKQuJqWPMvwPQWZUtVMOTMYKJbfdlZsjdsomuScvDmbDkgRualsxDvRJuCAmPOXitIbcyWsKGSdrEunFAOdmXnsuyFVgJqEjbklvmwrUlsxjRSfKZxGcpayDdgoFcnVSutxjRgOSFzPwidAjubMncNweqpbxhXGchpZUxuFDOtpnhNUycJICRYqsPhPSCjPTWZFLkstHWJxvdPEAyEIxXgLwbNOjrgzmaujiBABBIXvcXpLrbcEWNNQsbjvgJFgJkflpRohHUutvnaUqoopuKjTDaemDeSdqbnOzcfJpcTuAQtZoiLZOoAIlboFDAeGmSNwkvObPRvRWQgWkGkxwtPauYgdkmypLjbqhlHJIQTntgWjXwZdOyYEdQRRLfMSdnxqppqUofqLbLQDUjwKVKfZJUJQPsWIPwIVaSTrmKskoAhvmZyJgeRpkaTfGgrJzAigcxtfshmiDCFkuiluqtMOkidknnTBtumyJYlIsWLnCQclqdVmikUoMOPdPWwYbJxXyqUVicNxFxyqJTenNblyyKSdlCbiXxUiYUiMwXZASYfvMDPFgxniSjWaZTjHkqlJvtBsXqwPpyVxnJVGFWhfSxgOcduoxkiopJvFjMmFabrGYeVtTXLhxVUEiGwYUvndjFGzDVntUvibiyZhfMQdMhgsiuysLMiePBNXifRLMsSmXPkwlPloUbJveCvUlaalhZHuvdkCnkSHbMbmOnrfEGPwQiACiPlnihiaOdbjPqPiTXaHDoJXjSlZmltGqNHHNrcKdlFSCdmVOuvDcBLdSklyGJmcLTbSFtALdGlPkqqecJrpLCXNPWefoTJNgEJlyMEPneVaxxduAAEqQpHWZodWyRkDAxzyMnFMcjSVqeRXLqsNyNtQBbuRvunZflWSbbvXXdkyLikYqutQhLPONXbvhcQZJPSWnOulqQaXmbfFxAkqfYeseSHOQidHwbcsOaMnSrrmGjjRmEMQNuknupMxJiIeVjmgZvbmjPIQTEhQFULQLBMPrxcFPvBinaOPYWGvYGRKxLZdwamfRQQFngcdSlvwjfaPbURasIsGJVHtcEAxnIIrhSriiXLOlbEBLXFElXJFGxHJczRBIxAuPKtBisjKBwfzZFagdNmjdwIRvwzLkFKWRTDPxJCmpzHUcrPiiXXHnOIlqNVoGSXZewdnCRhuxeYGPVTfrNTQNOxZmxInOazUYNTNDgzsxlgiVEHPKMfbesvPHUqpNkUqbzeuzfdrsuLDpKHMUbBMKczKKWOdYoIXoPYtEjfOnlQLoGnbQUCuERdEFaptwnsHzTJDsuZkKtzMpFaZobynZdzNydEeJJHDYaQcwUxcqvwfWwNUsCiLvkZQiSfzAHftYgAmVsXgtmcYgTqJIawstRYJrZdSxlfRiqTufgEQVambeZZmaAyRQbcmdjVUZZCgqDrSeltJGXPMgZnGDZqISrGDOClxXCxMjmKqEPwKHoOfOeyGmqWqihqjINXLqnyTesZePQRqaWDQNqpLgNrAUKulklmckTijUltQKuWQDwpLmDyxLppPVMwsmBIpOwQttYFMjgJQZLYFPmxWFLIeZihkRNnkzoypBICIxgEuYsVWGIGRbbxqVasYnstWomJnHwmtOhAFSpttRYYzBmyEtZXiCthvKvWszTXDbiJbGXMcrYpKAgvUVFtdKUfvdMfhAryctklUCEdjetjuGNfJjajZtvzdYaqInKtFPPLYmRaXPdQzxdSQfmZDEVHlHGEGNSPRFJuIfKLLfUmnHxHnRjmzQPNlqrXgifUdzAGKVabYqvcDeYoTYgPsBUqehrBhmQUgTvDnsdpuhUoxskDdppTsYMcnDIPSwKIqhXDCIxOuXrywahvVavvHkPuaenjLmEbMgrkrQLHEAwrhHkPRNvonNQKqprqOFVZKAtpRSpvQUxMoXCMZLSSbnLEFsjVfANdQNQVwTmGxqVjVqRuxREAhuaDrFgEZpYKhwWPEKBevBfsOIcaZKyykQafzmGPLRAKDtTcJxJVgiiuUkmyMYuDUNEUhBEdoBLJnamtLmMJQgmLiUELIhLpiEvpOXOvXCPUeldLFqkKOwfacqIaRcnnZvERKRMCKUkMABbDHytQqQblrvoxOZkwzosQfDKGtIdfcXRJNqlBNwOCWoQBcEWyqrMlYZIAXYJmLfnjoJepgSFvrgajaBAIksoyeHqgqbGvpAstMIGmIhRYGGNPRIfOQKsGoKgxtsidhTaAePRCBFqZgPDWCIkqOJezGVkjfYUCZTlInbxBXwUAVRsxHTQtJFnnpmMvXDYCVlEmnZBKhmmxQOIQzxFWpJQkQoSAYzTEiDWEOsVLNrbfzeHFRyeYATakQQWmFDLPbVMCJcWjFGJjfqCoVzlbNNEsqxdSmNPjTjHYOkuEMFLkXYGaoJlraLqayMeCsTjWNRDPBywBJLAPVkGQqTwApVVwYAetlwSbzsdHWsTwSIcctkyKDuRWYDQikRqsKTMJchrliONJeaZIzwPQrNbTwxsGdwuduvibtYndRwpdsvyCktRHFalvUuEKMqXbItfGcNGWsGzubdPMYayOUOINjpcFBeESdwpdlTYmrPsLsVDhpTzoMegKrytNVZkfJRPuDCUXxSlSthOohmsuxmIZUedzxKmowKOdXTMcEtdpHaPWgIsIjrViKrQOCONlSuazmLuCUjLltOGXeNgJKedTVrrVCpWYWHyVrdXpKgNaMJVjbXxnVMSChdWKuZdqpisvrkBJPoURDYxWOtpjzZoOpWzyUuYNhCzRoHsMjmmWDcXzQiHIyjwdhPNwiPqFxeUfMVFQGImhykFgMIlQEoZCaRoqSBXTSWAeDumdbsOGtATwEdZlLfoBKiTvodQBGOEcuATWXfiinSjPmJKcWgQrTVYVrwlyMWhxqNbCMpIQNoSMGTiWfPTCezUjYcdWppnsYJihLQCqbNLRGgqrwHuIvsazapTpoPZIyZyeeSueJuTIhpHMEJfJpScshJubJGfkusuVBgfTWQoywSSliQQSfbvaHKiLnyjdSbpMkdBgXepoSsHnCQaYuHQqZsoEOmJCiuQUpJkmfyfbIShzlZpHFmLCsbknEAkKXKfRTRnuwdBeuOGgFbJLbDksHVapaRayWzwoYBEpmrlAxrUxYMUekKbpjPNfjUCjhbdMAnJmYQVZBQZkFVweHDAlaqJjRqoQPoOMLhyvYCzqEuQsAFoxWrzRnTVjStPadhsESlERnKhpEPsfDxNvxqcOyIulaCkmPdambLHvGhTZzysvqFauEgkFRItPfvisehFmoBhQqmkfbHVsgfHXDPJVyhwPllQpuYLRYvGodxKjkarnSNgsXoKEMlaSKxKdcVgvOkuLcfLFfdtXGTclqfPOfeoVLbqcjcXCUEBgAGplrkgsmIEhWRZLlGPGCwKWRaCKMkBHTAcypUrYjWwCLtOPVygMwMANGoQwFnCqFrUGMCRZUGJKTZIGPyldsifauoMnJPLTcDHmilcmahlqOELaAUYDBuzsVywnDQfwRLGIWozYaOAilMBcObErwgTDNGWnwQMUgFFSKtPDMEoEQCTKVREqrXZSGLqwTMcxHfWotDllNkIJPMbXzjDVjPOOjCFuIvTyhXKLyhUScOXvYthRXpPfKwMhptXaxIxgqBoUqzrWbaoLTVpQoottZyPFfNOoMioXHRuFwMRYUiKvcWPkrayyTLOCFJlAyslDameIuqVAuxErqFPEWIScKpBORIuZqoXlZuTvAjEdlEWDODFRregDTqGNoFBIHxvimmIZwLfFyKUfEWAnNBdtdzDmTPXtpHRGdIbuucfTjOygZsTxPjf", content.GetDescription()) + assert.Equal(t, "XhSUkMhPjMaxKlMIJMOXcnQfyzeOcbWwNbeHVIkPZBSpYuLyYggwexjxusrBqDOTtGTOWeLrQKjLxzIivHSlcxgdXhhuTSkuxKGLwQvuyNhYFmBZHeAerqyNEUzXPFGkqEGqiQWIXnku", content.GetTitle()) + assert.Equal(t, "gov", content.ProposalRoute()) + assert.Equal(t, "Text", content.ProposalType()) +} diff --git a/x/gov/testutil/expected_keepers.go b/x/gov/testutil/expected_keepers.go new file mode 100644 index 00000000..669dbc01 --- /dev/null +++ b/x/gov/testutil/expected_keepers.go @@ -0,0 +1,35 @@ +// This file only used to generate mocks + +package testutil + +import ( + math "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +// AccountKeeper extends gov's actual expected AccountKeeper with additional +// methods used in tests. +type AccountKeeper interface { + types.AccountKeeper + + IterateAccounts(ctx sdk.Context, cb func(account authtypes.AccountI) (stop bool)) +} + +// BankKeeper extends gov's actual expected BankKeeper with additional +// methods used in tests. +type BankKeeper interface { + bankkeeper.Keeper +} + +// StakingKeeper extends gov's actual expected StakingKeeper with additional +// methods used in tests. +type StakingKeeper interface { + types.StakingKeeper + + BondDenom(ctx sdk.Context) string + TokensFromConsensusPower(ctx sdk.Context, power int64) math.Int +} diff --git a/x/gov/testutil/expected_keepers_mocks.go b/x/gov/testutil/expected_keepers_mocks.go new file mode 100644 index 00000000..3492e16a --- /dev/null +++ b/x/gov/testutil/expected_keepers_mocks.go @@ -0,0 +1,1037 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: x/gov/testutil/expected_keepers.go + +// Package testutil is a generated GoMock package. +package testutil + +import ( + context "context" + reflect "reflect" + + math "cosmossdk.io/math" + types "github.com/cosmos/cosmos-sdk/types" + query "github.com/cosmos/cosmos-sdk/types/query" + types0 "github.com/cosmos/cosmos-sdk/x/auth/types" + keeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + types1 "github.com/cosmos/cosmos-sdk/x/bank/types" + types2 "github.com/cosmos/cosmos-sdk/x/staking/types" + gomock "github.com/golang/mock/gomock" +) + +// MockAccountKeeper is a mock of AccountKeeper interface. +type MockAccountKeeper struct { + ctrl *gomock.Controller + recorder *MockAccountKeeperMockRecorder +} + +// MockAccountKeeperMockRecorder is the mock recorder for MockAccountKeeper. +type MockAccountKeeperMockRecorder struct { + mock *MockAccountKeeper +} + +// NewMockAccountKeeper creates a new mock instance. +func NewMockAccountKeeper(ctrl *gomock.Controller) *MockAccountKeeper { + mock := &MockAccountKeeper{ctrl: ctrl} + mock.recorder = &MockAccountKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccountKeeper) EXPECT() *MockAccountKeeperMockRecorder { + return m.recorder +} + +// GetAccount mocks base method. +func (m *MockAccountKeeper) GetAccount(ctx types.Context, addr types.AccAddress) types0.AccountI { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccount", ctx, addr) + ret0, _ := ret[0].(types0.AccountI) + return ret0 +} + +// GetAccount indicates an expected call of GetAccount. +func (mr *MockAccountKeeperMockRecorder) GetAccount(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockAccountKeeper)(nil).GetAccount), ctx, addr) +} + +// GetModuleAccount mocks base method. +func (m *MockAccountKeeper) GetModuleAccount(ctx types.Context, name string) types0.ModuleAccountI { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetModuleAccount", ctx, name) + ret0, _ := ret[0].(types0.ModuleAccountI) + return ret0 +} + +// GetModuleAccount indicates an expected call of GetModuleAccount. +func (mr *MockAccountKeeperMockRecorder) GetModuleAccount(ctx, name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleAccount", reflect.TypeOf((*MockAccountKeeper)(nil).GetModuleAccount), ctx, name) +} + +// GetModuleAddress mocks base method. +func (m *MockAccountKeeper) GetModuleAddress(name string) types.AccAddress { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetModuleAddress", name) + ret0, _ := ret[0].(types.AccAddress) + return ret0 +} + +// GetModuleAddress indicates an expected call of GetModuleAddress. +func (mr *MockAccountKeeperMockRecorder) GetModuleAddress(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModuleAddress", reflect.TypeOf((*MockAccountKeeper)(nil).GetModuleAddress), name) +} + +// IterateAccounts mocks base method. +func (m *MockAccountKeeper) IterateAccounts(ctx types.Context, cb func(types0.AccountI) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateAccounts", ctx, cb) +} + +// IterateAccounts indicates an expected call of IterateAccounts. +func (mr *MockAccountKeeperMockRecorder) IterateAccounts(ctx, cb interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateAccounts", reflect.TypeOf((*MockAccountKeeper)(nil).IterateAccounts), ctx, cb) +} + +// SetModuleAccount mocks base method. +func (m *MockAccountKeeper) SetModuleAccount(arg0 types.Context, arg1 types0.ModuleAccountI) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetModuleAccount", arg0, arg1) +} + +// SetModuleAccount indicates an expected call of SetModuleAccount. +func (mr *MockAccountKeeperMockRecorder) SetModuleAccount(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetModuleAccount", reflect.TypeOf((*MockAccountKeeper)(nil).SetModuleAccount), arg0, arg1) +} + +// MockBankKeeper is a mock of BankKeeper interface. +type MockBankKeeper struct { + ctrl *gomock.Controller + recorder *MockBankKeeperMockRecorder +} + +// MockBankKeeperMockRecorder is the mock recorder for MockBankKeeper. +type MockBankKeeperMockRecorder struct { + mock *MockBankKeeper +} + +// NewMockBankKeeper creates a new mock instance. +func NewMockBankKeeper(ctrl *gomock.Controller) *MockBankKeeper { + mock := &MockBankKeeper{ctrl: ctrl} + mock.recorder = &MockBankKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBankKeeper) EXPECT() *MockBankKeeperMockRecorder { + return m.recorder +} + +// AllBalances mocks base method. +func (m *MockBankKeeper) AllBalances(arg0 context.Context, arg1 *types1.QueryAllBalancesRequest) (*types1.QueryAllBalancesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AllBalances", arg0, arg1) + ret0, _ := ret[0].(*types1.QueryAllBalancesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AllBalances indicates an expected call of AllBalances. +func (mr *MockBankKeeperMockRecorder) AllBalances(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllBalances", reflect.TypeOf((*MockBankKeeper)(nil).AllBalances), arg0, arg1) +} + +// Balance mocks base method. +func (m *MockBankKeeper) Balance(arg0 context.Context, arg1 *types1.QueryBalanceRequest) (*types1.QueryBalanceResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Balance", arg0, arg1) + ret0, _ := ret[0].(*types1.QueryBalanceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Balance indicates an expected call of Balance. +func (mr *MockBankKeeperMockRecorder) Balance(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Balance", reflect.TypeOf((*MockBankKeeper)(nil).Balance), arg0, arg1) +} + +// BlockedAddr mocks base method. +func (m *MockBankKeeper) BlockedAddr(addr types.AccAddress) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockedAddr", addr) + ret0, _ := ret[0].(bool) + return ret0 +} + +// BlockedAddr indicates an expected call of BlockedAddr. +func (mr *MockBankKeeperMockRecorder) BlockedAddr(addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockedAddr", reflect.TypeOf((*MockBankKeeper)(nil).BlockedAddr), addr) +} + +// BurnCoins mocks base method. +func (m *MockBankKeeper) BurnCoins(ctx types.Context, moduleName string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BurnCoins", ctx, moduleName, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// BurnCoins indicates an expected call of BurnCoins. +func (mr *MockBankKeeperMockRecorder) BurnCoins(ctx, moduleName, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BurnCoins", reflect.TypeOf((*MockBankKeeper)(nil).BurnCoins), ctx, moduleName, amt) +} + +// DelegateCoins mocks base method. +func (m *MockBankKeeper) DelegateCoins(ctx types.Context, delegatorAddr, moduleAccAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DelegateCoins", ctx, delegatorAddr, moduleAccAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// DelegateCoins indicates an expected call of DelegateCoins. +func (mr *MockBankKeeperMockRecorder) DelegateCoins(ctx, delegatorAddr, moduleAccAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelegateCoins", reflect.TypeOf((*MockBankKeeper)(nil).DelegateCoins), ctx, delegatorAddr, moduleAccAddr, amt) +} + +// DelegateCoinsFromAccountToModule mocks base method. +func (m *MockBankKeeper) DelegateCoinsFromAccountToModule(ctx types.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DelegateCoinsFromAccountToModule", ctx, senderAddr, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// DelegateCoinsFromAccountToModule indicates an expected call of DelegateCoinsFromAccountToModule. +func (mr *MockBankKeeperMockRecorder) DelegateCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelegateCoinsFromAccountToModule", reflect.TypeOf((*MockBankKeeper)(nil).DelegateCoinsFromAccountToModule), ctx, senderAddr, recipientModule, amt) +} + +// DeleteSendEnabled mocks base method. +func (m *MockBankKeeper) DeleteSendEnabled(ctx types.Context, denoms ...string) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range denoms { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "DeleteSendEnabled", varargs...) +} + +// DeleteSendEnabled indicates an expected call of DeleteSendEnabled. +func (mr *MockBankKeeperMockRecorder) DeleteSendEnabled(ctx interface{}, denoms ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, denoms...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSendEnabled", reflect.TypeOf((*MockBankKeeper)(nil).DeleteSendEnabled), varargs...) +} + +// DenomMetadata mocks base method. +func (m *MockBankKeeper) DenomMetadata(arg0 context.Context, arg1 *types1.QueryDenomMetadataRequest) (*types1.QueryDenomMetadataResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DenomMetadata", arg0, arg1) + ret0, _ := ret[0].(*types1.QueryDenomMetadataResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DenomMetadata indicates an expected call of DenomMetadata. +func (mr *MockBankKeeperMockRecorder) DenomMetadata(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DenomMetadata", reflect.TypeOf((*MockBankKeeper)(nil).DenomMetadata), arg0, arg1) +} + +// DenomOwners mocks base method. +func (m *MockBankKeeper) DenomOwners(arg0 context.Context, arg1 *types1.QueryDenomOwnersRequest) (*types1.QueryDenomOwnersResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DenomOwners", arg0, arg1) + ret0, _ := ret[0].(*types1.QueryDenomOwnersResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DenomOwners indicates an expected call of DenomOwners. +func (mr *MockBankKeeperMockRecorder) DenomOwners(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DenomOwners", reflect.TypeOf((*MockBankKeeper)(nil).DenomOwners), arg0, arg1) +} + +// DenomsMetadata mocks base method. +func (m *MockBankKeeper) DenomsMetadata(arg0 context.Context, arg1 *types1.QueryDenomsMetadataRequest) (*types1.QueryDenomsMetadataResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DenomsMetadata", arg0, arg1) + ret0, _ := ret[0].(*types1.QueryDenomsMetadataResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DenomsMetadata indicates an expected call of DenomsMetadata. +func (mr *MockBankKeeperMockRecorder) DenomsMetadata(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DenomsMetadata", reflect.TypeOf((*MockBankKeeper)(nil).DenomsMetadata), arg0, arg1) +} + +// ExportGenesis mocks base method. +func (m *MockBankKeeper) ExportGenesis(arg0 types.Context) *types1.GenesisState { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportGenesis", arg0) + ret0, _ := ret[0].(*types1.GenesisState) + return ret0 +} + +// ExportGenesis indicates an expected call of ExportGenesis. +func (mr *MockBankKeeperMockRecorder) ExportGenesis(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportGenesis", reflect.TypeOf((*MockBankKeeper)(nil).ExportGenesis), arg0) +} + +// GetAccountsBalances mocks base method. +func (m *MockBankKeeper) GetAccountsBalances(ctx types.Context) []types1.Balance { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAccountsBalances", ctx) + ret0, _ := ret[0].([]types1.Balance) + return ret0 +} + +// GetAccountsBalances indicates an expected call of GetAccountsBalances. +func (mr *MockBankKeeperMockRecorder) GetAccountsBalances(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountsBalances", reflect.TypeOf((*MockBankKeeper)(nil).GetAccountsBalances), ctx) +} + +// GetAllBalances mocks base method. +func (m *MockBankKeeper) GetAllBalances(ctx types.Context, addr types.AccAddress) types.Coins { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllBalances", ctx, addr) + ret0, _ := ret[0].(types.Coins) + return ret0 +} + +// GetAllBalances indicates an expected call of GetAllBalances. +func (mr *MockBankKeeperMockRecorder) GetAllBalances(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBalances", reflect.TypeOf((*MockBankKeeper)(nil).GetAllBalances), ctx, addr) +} + +// GetAllDenomMetaData mocks base method. +func (m *MockBankKeeper) GetAllDenomMetaData(ctx types.Context) []types1.Metadata { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllDenomMetaData", ctx) + ret0, _ := ret[0].([]types1.Metadata) + return ret0 +} + +// GetAllDenomMetaData indicates an expected call of GetAllDenomMetaData. +func (mr *MockBankKeeperMockRecorder) GetAllDenomMetaData(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllDenomMetaData", reflect.TypeOf((*MockBankKeeper)(nil).GetAllDenomMetaData), ctx) +} + +// GetAllSendEnabledEntries mocks base method. +func (m *MockBankKeeper) GetAllSendEnabledEntries(ctx types.Context) []types1.SendEnabled { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAllSendEnabledEntries", ctx) + ret0, _ := ret[0].([]types1.SendEnabled) + return ret0 +} + +// GetAllSendEnabledEntries indicates an expected call of GetAllSendEnabledEntries. +func (mr *MockBankKeeperMockRecorder) GetAllSendEnabledEntries(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllSendEnabledEntries", reflect.TypeOf((*MockBankKeeper)(nil).GetAllSendEnabledEntries), ctx) +} + +// GetAuthority mocks base method. +func (m *MockBankKeeper) GetAuthority() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthority") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetAuthority indicates an expected call of GetAuthority. +func (mr *MockBankKeeperMockRecorder) GetAuthority() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthority", reflect.TypeOf((*MockBankKeeper)(nil).GetAuthority)) +} + +// GetBalance mocks base method. +func (m *MockBankKeeper) GetBalance(ctx types.Context, addr types.AccAddress, denom string) types.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalance", ctx, addr, denom) + ret0, _ := ret[0].(types.Coin) + return ret0 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockBankKeeperMockRecorder) GetBalance(ctx, addr, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockBankKeeper)(nil).GetBalance), ctx, addr, denom) +} + +// GetBlockedAddresses mocks base method. +func (m *MockBankKeeper) GetBlockedAddresses() map[string]bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlockedAddresses") + ret0, _ := ret[0].(map[string]bool) + return ret0 +} + +// GetBlockedAddresses indicates an expected call of GetBlockedAddresses. +func (mr *MockBankKeeperMockRecorder) GetBlockedAddresses() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockedAddresses", reflect.TypeOf((*MockBankKeeper)(nil).GetBlockedAddresses)) +} + +// GetDenomMetaData mocks base method. +func (m *MockBankKeeper) GetDenomMetaData(ctx types.Context, denom string) (types1.Metadata, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDenomMetaData", ctx, denom) + ret0, _ := ret[0].(types1.Metadata) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetDenomMetaData indicates an expected call of GetDenomMetaData. +func (mr *MockBankKeeperMockRecorder) GetDenomMetaData(ctx, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDenomMetaData", reflect.TypeOf((*MockBankKeeper)(nil).GetDenomMetaData), ctx, denom) +} + +// GetPaginatedTotalSupply mocks base method. +func (m *MockBankKeeper) GetPaginatedTotalSupply(ctx types.Context, pagination *query.PageRequest) (types.Coins, *query.PageResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPaginatedTotalSupply", ctx, pagination) + ret0, _ := ret[0].(types.Coins) + ret1, _ := ret[1].(*query.PageResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetPaginatedTotalSupply indicates an expected call of GetPaginatedTotalSupply. +func (mr *MockBankKeeperMockRecorder) GetPaginatedTotalSupply(ctx, pagination interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPaginatedTotalSupply", reflect.TypeOf((*MockBankKeeper)(nil).GetPaginatedTotalSupply), ctx, pagination) +} + +// GetParams mocks base method. +func (m *MockBankKeeper) GetParams(ctx types.Context) types1.Params { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetParams", ctx) + ret0, _ := ret[0].(types1.Params) + return ret0 +} + +// GetParams indicates an expected call of GetParams. +func (mr *MockBankKeeperMockRecorder) GetParams(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParams", reflect.TypeOf((*MockBankKeeper)(nil).GetParams), ctx) +} + +// GetSendEnabledEntry mocks base method. +func (m *MockBankKeeper) GetSendEnabledEntry(ctx types.Context, denom string) (types1.SendEnabled, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSendEnabledEntry", ctx, denom) + ret0, _ := ret[0].(types1.SendEnabled) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetSendEnabledEntry indicates an expected call of GetSendEnabledEntry. +func (mr *MockBankKeeperMockRecorder) GetSendEnabledEntry(ctx, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSendEnabledEntry", reflect.TypeOf((*MockBankKeeper)(nil).GetSendEnabledEntry), ctx, denom) +} + +// GetSupply mocks base method. +func (m *MockBankKeeper) GetSupply(ctx types.Context, denom string) types.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSupply", ctx, denom) + ret0, _ := ret[0].(types.Coin) + return ret0 +} + +// GetSupply indicates an expected call of GetSupply. +func (mr *MockBankKeeperMockRecorder) GetSupply(ctx, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSupply", reflect.TypeOf((*MockBankKeeper)(nil).GetSupply), ctx, denom) +} + +// HasBalance mocks base method. +func (m *MockBankKeeper) HasBalance(ctx types.Context, addr types.AccAddress, amt types.Coin) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasBalance", ctx, addr, amt) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasBalance indicates an expected call of HasBalance. +func (mr *MockBankKeeperMockRecorder) HasBalance(ctx, addr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasBalance", reflect.TypeOf((*MockBankKeeper)(nil).HasBalance), ctx, addr, amt) +} + +// HasDenomMetaData mocks base method. +func (m *MockBankKeeper) HasDenomMetaData(ctx types.Context, denom string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasDenomMetaData", ctx, denom) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasDenomMetaData indicates an expected call of HasDenomMetaData. +func (mr *MockBankKeeperMockRecorder) HasDenomMetaData(ctx, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasDenomMetaData", reflect.TypeOf((*MockBankKeeper)(nil).HasDenomMetaData), ctx, denom) +} + +// HasSupply mocks base method. +func (m *MockBankKeeper) HasSupply(ctx types.Context, denom string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasSupply", ctx, denom) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasSupply indicates an expected call of HasSupply. +func (mr *MockBankKeeperMockRecorder) HasSupply(ctx, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasSupply", reflect.TypeOf((*MockBankKeeper)(nil).HasSupply), ctx, denom) +} + +// InitGenesis mocks base method. +func (m *MockBankKeeper) InitGenesis(arg0 types.Context, arg1 *types1.GenesisState) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "InitGenesis", arg0, arg1) +} + +// InitGenesis indicates an expected call of InitGenesis. +func (mr *MockBankKeeperMockRecorder) InitGenesis(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitGenesis", reflect.TypeOf((*MockBankKeeper)(nil).InitGenesis), arg0, arg1) +} + +// InputOutputCoins mocks base method. +func (m *MockBankKeeper) InputOutputCoins(ctx types.Context, inputs []types1.Input, outputs []types1.Output) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InputOutputCoins", ctx, inputs, outputs) + ret0, _ := ret[0].(error) + return ret0 +} + +// InputOutputCoins indicates an expected call of InputOutputCoins. +func (mr *MockBankKeeperMockRecorder) InputOutputCoins(ctx, inputs, outputs interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InputOutputCoins", reflect.TypeOf((*MockBankKeeper)(nil).InputOutputCoins), ctx, inputs, outputs) +} + +// IsSendEnabledCoin mocks base method. +func (m *MockBankKeeper) IsSendEnabledCoin(ctx types.Context, coin types.Coin) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsSendEnabledCoin", ctx, coin) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsSendEnabledCoin indicates an expected call of IsSendEnabledCoin. +func (mr *MockBankKeeperMockRecorder) IsSendEnabledCoin(ctx, coin interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSendEnabledCoin", reflect.TypeOf((*MockBankKeeper)(nil).IsSendEnabledCoin), ctx, coin) +} + +// IsSendEnabledCoins mocks base method. +func (m *MockBankKeeper) IsSendEnabledCoins(ctx types.Context, coins ...types.Coin) error { + m.ctrl.T.Helper() + varargs := []interface{}{ctx} + for _, a := range coins { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "IsSendEnabledCoins", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// IsSendEnabledCoins indicates an expected call of IsSendEnabledCoins. +func (mr *MockBankKeeperMockRecorder) IsSendEnabledCoins(ctx interface{}, coins ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx}, coins...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSendEnabledCoins", reflect.TypeOf((*MockBankKeeper)(nil).IsSendEnabledCoins), varargs...) +} + +// IsSendEnabledDenom mocks base method. +func (m *MockBankKeeper) IsSendEnabledDenom(ctx types.Context, denom string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsSendEnabledDenom", ctx, denom) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsSendEnabledDenom indicates an expected call of IsSendEnabledDenom. +func (mr *MockBankKeeperMockRecorder) IsSendEnabledDenom(ctx, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSendEnabledDenom", reflect.TypeOf((*MockBankKeeper)(nil).IsSendEnabledDenom), ctx, denom) +} + +// IterateAccountBalances mocks base method. +func (m *MockBankKeeper) IterateAccountBalances(ctx types.Context, addr types.AccAddress, cb func(types.Coin) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateAccountBalances", ctx, addr, cb) +} + +// IterateAccountBalances indicates an expected call of IterateAccountBalances. +func (mr *MockBankKeeperMockRecorder) IterateAccountBalances(ctx, addr, cb interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateAccountBalances", reflect.TypeOf((*MockBankKeeper)(nil).IterateAccountBalances), ctx, addr, cb) +} + +// IterateAllBalances mocks base method. +func (m *MockBankKeeper) IterateAllBalances(ctx types.Context, cb func(types.AccAddress, types.Coin) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateAllBalances", ctx, cb) +} + +// IterateAllBalances indicates an expected call of IterateAllBalances. +func (mr *MockBankKeeperMockRecorder) IterateAllBalances(ctx, cb interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateAllBalances", reflect.TypeOf((*MockBankKeeper)(nil).IterateAllBalances), ctx, cb) +} + +// IterateAllDenomMetaData mocks base method. +func (m *MockBankKeeper) IterateAllDenomMetaData(ctx types.Context, cb func(types1.Metadata) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateAllDenomMetaData", ctx, cb) +} + +// IterateAllDenomMetaData indicates an expected call of IterateAllDenomMetaData. +func (mr *MockBankKeeperMockRecorder) IterateAllDenomMetaData(ctx, cb interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateAllDenomMetaData", reflect.TypeOf((*MockBankKeeper)(nil).IterateAllDenomMetaData), ctx, cb) +} + +// IterateSendEnabledEntries mocks base method. +func (m *MockBankKeeper) IterateSendEnabledEntries(ctx types.Context, cb func(string, bool) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateSendEnabledEntries", ctx, cb) +} + +// IterateSendEnabledEntries indicates an expected call of IterateSendEnabledEntries. +func (mr *MockBankKeeperMockRecorder) IterateSendEnabledEntries(ctx, cb interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateSendEnabledEntries", reflect.TypeOf((*MockBankKeeper)(nil).IterateSendEnabledEntries), ctx, cb) +} + +// IterateTotalSupply mocks base method. +func (m *MockBankKeeper) IterateTotalSupply(ctx types.Context, cb func(types.Coin) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateTotalSupply", ctx, cb) +} + +// IterateTotalSupply indicates an expected call of IterateTotalSupply. +func (mr *MockBankKeeperMockRecorder) IterateTotalSupply(ctx, cb interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateTotalSupply", reflect.TypeOf((*MockBankKeeper)(nil).IterateTotalSupply), ctx, cb) +} + +// LockedCoins mocks base method. +func (m *MockBankKeeper) LockedCoins(ctx types.Context, addr types.AccAddress) types.Coins { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LockedCoins", ctx, addr) + ret0, _ := ret[0].(types.Coins) + return ret0 +} + +// LockedCoins indicates an expected call of LockedCoins. +func (mr *MockBankKeeperMockRecorder) LockedCoins(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LockedCoins", reflect.TypeOf((*MockBankKeeper)(nil).LockedCoins), ctx, addr) +} + +// MintCoins mocks base method. +func (m *MockBankKeeper) MintCoins(ctx types.Context, moduleName string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MintCoins", ctx, moduleName, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// MintCoins indicates an expected call of MintCoins. +func (mr *MockBankKeeperMockRecorder) MintCoins(ctx, moduleName, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MintCoins", reflect.TypeOf((*MockBankKeeper)(nil).MintCoins), ctx, moduleName, amt) +} + +// Params mocks base method. +func (m *MockBankKeeper) Params(arg0 context.Context, arg1 *types1.QueryParamsRequest) (*types1.QueryParamsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Params", arg0, arg1) + ret0, _ := ret[0].(*types1.QueryParamsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Params indicates an expected call of Params. +func (mr *MockBankKeeperMockRecorder) Params(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Params", reflect.TypeOf((*MockBankKeeper)(nil).Params), arg0, arg1) +} + +// SendCoins mocks base method. +func (m *MockBankKeeper) SendCoins(ctx types.Context, fromAddr, toAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoins", ctx, fromAddr, toAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoins indicates an expected call of SendCoins. +func (mr *MockBankKeeperMockRecorder) SendCoins(ctx, fromAddr, toAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoins", reflect.TypeOf((*MockBankKeeper)(nil).SendCoins), ctx, fromAddr, toAddr, amt) +} + +// SendCoinsFromAccountToModule mocks base method. +func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx types.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromAccountToModule", ctx, senderAddr, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromAccountToModule indicates an expected call of SendCoinsFromAccountToModule. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromAccountToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromAccountToModule), ctx, senderAddr, recipientModule, amt) +} + +// SendCoinsFromModuleToAccount mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToAccount(ctx types.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToAccount", ctx, senderModule, recipientAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToAccount indicates an expected call of SendCoinsFromModuleToAccount. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToAccount", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToAccount), ctx, senderModule, recipientAddr, amt) +} + +// SendCoinsFromModuleToModule mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx types.Context, senderModule, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToModule", ctx, senderModule, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToModule indicates an expected call of SendCoinsFromModuleToModule. +func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToModule(ctx, senderModule, recipientModule, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoinsFromModuleToModule", reflect.TypeOf((*MockBankKeeper)(nil).SendCoinsFromModuleToModule), ctx, senderModule, recipientModule, amt) +} + +// SendEnabled mocks base method. +func (m *MockBankKeeper) SendEnabled(arg0 context.Context, arg1 *types1.QuerySendEnabledRequest) (*types1.QuerySendEnabledResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendEnabled", arg0, arg1) + ret0, _ := ret[0].(*types1.QuerySendEnabledResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SendEnabled indicates an expected call of SendEnabled. +func (mr *MockBankKeeperMockRecorder) SendEnabled(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendEnabled", reflect.TypeOf((*MockBankKeeper)(nil).SendEnabled), arg0, arg1) +} + +// SetAllSendEnabled mocks base method. +func (m *MockBankKeeper) SetAllSendEnabled(ctx types.Context, sendEnableds []*types1.SendEnabled) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetAllSendEnabled", ctx, sendEnableds) +} + +// SetAllSendEnabled indicates an expected call of SetAllSendEnabled. +func (mr *MockBankKeeperMockRecorder) SetAllSendEnabled(ctx, sendEnableds interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAllSendEnabled", reflect.TypeOf((*MockBankKeeper)(nil).SetAllSendEnabled), ctx, sendEnableds) +} + +// SetDenomMetaData mocks base method. +func (m *MockBankKeeper) SetDenomMetaData(ctx types.Context, denomMetaData types1.Metadata) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetDenomMetaData", ctx, denomMetaData) +} + +// SetDenomMetaData indicates an expected call of SetDenomMetaData. +func (mr *MockBankKeeperMockRecorder) SetDenomMetaData(ctx, denomMetaData interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDenomMetaData", reflect.TypeOf((*MockBankKeeper)(nil).SetDenomMetaData), ctx, denomMetaData) +} + +// SetParams mocks base method. +func (m *MockBankKeeper) SetParams(ctx types.Context, params types1.Params) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetParams", ctx, params) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetParams indicates an expected call of SetParams. +func (mr *MockBankKeeperMockRecorder) SetParams(ctx, params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetParams", reflect.TypeOf((*MockBankKeeper)(nil).SetParams), ctx, params) +} + +// SetSendEnabled mocks base method. +func (m *MockBankKeeper) SetSendEnabled(ctx types.Context, denom string, value bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetSendEnabled", ctx, denom, value) +} + +// SetSendEnabled indicates an expected call of SetSendEnabled. +func (mr *MockBankKeeperMockRecorder) SetSendEnabled(ctx, denom, value interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSendEnabled", reflect.TypeOf((*MockBankKeeper)(nil).SetSendEnabled), ctx, denom, value) +} + +// SpendableBalanceByDenom mocks base method. +func (m *MockBankKeeper) SpendableBalanceByDenom(arg0 context.Context, arg1 *types1.QuerySpendableBalanceByDenomRequest) (*types1.QuerySpendableBalanceByDenomResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SpendableBalanceByDenom", arg0, arg1) + ret0, _ := ret[0].(*types1.QuerySpendableBalanceByDenomResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SpendableBalanceByDenom indicates an expected call of SpendableBalanceByDenom. +func (mr *MockBankKeeperMockRecorder) SpendableBalanceByDenom(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpendableBalanceByDenom", reflect.TypeOf((*MockBankKeeper)(nil).SpendableBalanceByDenom), arg0, arg1) +} + +// SpendableBalances mocks base method. +func (m *MockBankKeeper) SpendableBalances(arg0 context.Context, arg1 *types1.QuerySpendableBalancesRequest) (*types1.QuerySpendableBalancesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SpendableBalances", arg0, arg1) + ret0, _ := ret[0].(*types1.QuerySpendableBalancesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SpendableBalances indicates an expected call of SpendableBalances. +func (mr *MockBankKeeperMockRecorder) SpendableBalances(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpendableBalances", reflect.TypeOf((*MockBankKeeper)(nil).SpendableBalances), arg0, arg1) +} + +// SpendableCoin mocks base method. +func (m *MockBankKeeper) SpendableCoin(ctx types.Context, addr types.AccAddress, denom string) types.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SpendableCoin", ctx, addr, denom) + ret0, _ := ret[0].(types.Coin) + return ret0 +} + +// SpendableCoin indicates an expected call of SpendableCoin. +func (mr *MockBankKeeperMockRecorder) SpendableCoin(ctx, addr, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpendableCoin", reflect.TypeOf((*MockBankKeeper)(nil).SpendableCoin), ctx, addr, denom) +} + +// SpendableCoins mocks base method. +func (m *MockBankKeeper) SpendableCoins(ctx types.Context, addr types.AccAddress) types.Coins { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SpendableCoins", ctx, addr) + ret0, _ := ret[0].(types.Coins) + return ret0 +} + +// SpendableCoins indicates an expected call of SpendableCoins. +func (mr *MockBankKeeperMockRecorder) SpendableCoins(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpendableCoins", reflect.TypeOf((*MockBankKeeper)(nil).SpendableCoins), ctx, addr) +} + +// SupplyOf mocks base method. +func (m *MockBankKeeper) SupplyOf(arg0 context.Context, arg1 *types1.QuerySupplyOfRequest) (*types1.QuerySupplyOfResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SupplyOf", arg0, arg1) + ret0, _ := ret[0].(*types1.QuerySupplyOfResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SupplyOf indicates an expected call of SupplyOf. +func (mr *MockBankKeeperMockRecorder) SupplyOf(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupplyOf", reflect.TypeOf((*MockBankKeeper)(nil).SupplyOf), arg0, arg1) +} + +// TotalSupply mocks base method. +func (m *MockBankKeeper) TotalSupply(arg0 context.Context, arg1 *types1.QueryTotalSupplyRequest) (*types1.QueryTotalSupplyResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TotalSupply", arg0, arg1) + ret0, _ := ret[0].(*types1.QueryTotalSupplyResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TotalSupply indicates an expected call of TotalSupply. +func (mr *MockBankKeeperMockRecorder) TotalSupply(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalSupply", reflect.TypeOf((*MockBankKeeper)(nil).TotalSupply), arg0, arg1) +} + +// UndelegateCoins mocks base method. +func (m *MockBankKeeper) UndelegateCoins(ctx types.Context, moduleAccAddr, delegatorAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UndelegateCoins", ctx, moduleAccAddr, delegatorAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// UndelegateCoins indicates an expected call of UndelegateCoins. +func (mr *MockBankKeeperMockRecorder) UndelegateCoins(ctx, moduleAccAddr, delegatorAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndelegateCoins", reflect.TypeOf((*MockBankKeeper)(nil).UndelegateCoins), ctx, moduleAccAddr, delegatorAddr, amt) +} + +// UndelegateCoinsFromModuleToAccount mocks base method. +func (m *MockBankKeeper) UndelegateCoinsFromModuleToAccount(ctx types.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UndelegateCoinsFromModuleToAccount", ctx, senderModule, recipientAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// UndelegateCoinsFromModuleToAccount indicates an expected call of UndelegateCoinsFromModuleToAccount. +func (mr *MockBankKeeperMockRecorder) UndelegateCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UndelegateCoinsFromModuleToAccount", reflect.TypeOf((*MockBankKeeper)(nil).UndelegateCoinsFromModuleToAccount), ctx, senderModule, recipientAddr, amt) +} + +// ValidateBalance mocks base method. +func (m *MockBankKeeper) ValidateBalance(ctx types.Context, addr types.AccAddress) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateBalance", ctx, addr) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateBalance indicates an expected call of ValidateBalance. +func (mr *MockBankKeeperMockRecorder) ValidateBalance(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateBalance", reflect.TypeOf((*MockBankKeeper)(nil).ValidateBalance), ctx, addr) +} + +// WithMintCoinsRestriction mocks base method. +func (m *MockBankKeeper) WithMintCoinsRestriction(arg0 keeper.MintingRestrictionFn) keeper.BaseKeeper { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithMintCoinsRestriction", arg0) + ret0, _ := ret[0].(keeper.BaseKeeper) + return ret0 +} + +// WithMintCoinsRestriction indicates an expected call of WithMintCoinsRestriction. +func (mr *MockBankKeeperMockRecorder) WithMintCoinsRestriction(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithMintCoinsRestriction", reflect.TypeOf((*MockBankKeeper)(nil).WithMintCoinsRestriction), arg0) +} + +// MockStakingKeeper is a mock of StakingKeeper interface. +type MockStakingKeeper struct { + ctrl *gomock.Controller + recorder *MockStakingKeeperMockRecorder +} + +// MockStakingKeeperMockRecorder is the mock recorder for MockStakingKeeper. +type MockStakingKeeperMockRecorder struct { + mock *MockStakingKeeper +} + +// NewMockStakingKeeper creates a new mock instance. +func NewMockStakingKeeper(ctrl *gomock.Controller) *MockStakingKeeper { + mock := &MockStakingKeeper{ctrl: ctrl} + mock.recorder = &MockStakingKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStakingKeeper) EXPECT() *MockStakingKeeperMockRecorder { + return m.recorder +} + +// BondDenom mocks base method. +func (m *MockStakingKeeper) BondDenom(ctx types.Context) string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BondDenom", ctx) + ret0, _ := ret[0].(string) + return ret0 +} + +// BondDenom indicates an expected call of BondDenom. +func (mr *MockStakingKeeperMockRecorder) BondDenom(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BondDenom", reflect.TypeOf((*MockStakingKeeper)(nil).BondDenom), ctx) +} + +// IterateBondedValidatorsByPower mocks base method. +func (m *MockStakingKeeper) IterateBondedValidatorsByPower(arg0 types.Context, arg1 func(int64, types2.ValidatorI) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateBondedValidatorsByPower", arg0, arg1) +} + +// IterateBondedValidatorsByPower indicates an expected call of IterateBondedValidatorsByPower. +func (mr *MockStakingKeeperMockRecorder) IterateBondedValidatorsByPower(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateBondedValidatorsByPower", reflect.TypeOf((*MockStakingKeeper)(nil).IterateBondedValidatorsByPower), arg0, arg1) +} + +// IterateDelegations mocks base method. +func (m *MockStakingKeeper) IterateDelegations(ctx types.Context, delegator types.AccAddress, fn func(int64, types2.DelegationI) bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "IterateDelegations", ctx, delegator, fn) +} + +// IterateDelegations indicates an expected call of IterateDelegations. +func (mr *MockStakingKeeperMockRecorder) IterateDelegations(ctx, delegator, fn interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateDelegations", reflect.TypeOf((*MockStakingKeeper)(nil).IterateDelegations), ctx, delegator, fn) +} + +// TokensFromConsensusPower mocks base method. +func (m *MockStakingKeeper) TokensFromConsensusPower(ctx types.Context, power int64) math.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TokensFromConsensusPower", ctx, power) + ret0, _ := ret[0].(math.Int) + return ret0 +} + +// TokensFromConsensusPower indicates an expected call of TokensFromConsensusPower. +func (mr *MockStakingKeeperMockRecorder) TokensFromConsensusPower(ctx, power interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TokensFromConsensusPower", reflect.TypeOf((*MockStakingKeeper)(nil).TokensFromConsensusPower), ctx, power) +} + +// TotalBondedTokens mocks base method. +func (m *MockStakingKeeper) TotalBondedTokens(arg0 types.Context) math.Int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TotalBondedTokens", arg0) + ret0, _ := ret[0].(math.Int) + return ret0 +} + +// TotalBondedTokens indicates an expected call of TotalBondedTokens. +func (mr *MockStakingKeeperMockRecorder) TotalBondedTokens(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalBondedTokens", reflect.TypeOf((*MockStakingKeeper)(nil).TotalBondedTokens), arg0) +}