├── .github └── workflows │ ├── e2e.yml │ └── go.yml ├── .gitignore ├── .gitpod.yml ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── app ├── ante.go ├── app.go ├── encoding.go ├── export.go ├── genesis.go ├── testing │ └── mock.go ├── upgrades │ ├── types.go │ └── v2 │ │ ├── README.md │ │ ├── constants.go │ │ └── upgrades.go └── wasm │ ├── msg.go │ ├── msg_plugin.go │ ├── query.go │ ├── query_plugin.go │ └── wasm.go ├── cmd └── marsd │ ├── address.go │ ├── appcreator.go │ ├── config.go │ ├── genaccounts.go │ ├── genwasm.go │ ├── main.go │ └── root.go ├── docs ├── config.json ├── docs.go ├── handler.go ├── index.tpl └── swagger.yml ├── go.mod ├── go.sum ├── proto ├── buf.gen.go.yaml ├── buf.gen.swagger.yaml ├── buf.lock ├── buf.yaml └── mars │ ├── envoy │ └── v1beta1 │ │ ├── genesis.proto │ │ ├── query.proto │ │ └── tx.proto │ ├── incentives │ └── v1beta1 │ │ ├── genesis.proto │ │ ├── query.proto │ │ ├── store.proto │ │ └── tx.proto │ └── safety │ └── v1beta1 │ ├── genesis.proto │ ├── query.proto │ └── tx.proto ├── scripts ├── protoc-swagger-gen.sh └── protocgen.sh ├── tests └── e2e │ ├── MANUAL.md │ ├── README.md │ ├── configs.tar.gz │ ├── envoy_test.ts │ ├── send_funds.json │ └── send_messages.json ├── tools └── tools.go ├── utils ├── cmp.go ├── math.go └── strconv.go └── x ├── envoy ├── README.md ├── client │ └── cli │ │ ├── query.go │ │ └── tx.go ├── ibc_module.go ├── keeper │ ├── genesis.go │ ├── genesis_test.go │ ├── keeper.go │ ├── keeper_test.go │ ├── msg_server.go │ ├── msg_server_test.go │ ├── query_server.go │ └── query_server_test.go ├── module.go ├── spec │ ├── README.md │ └── envoy.tla └── types │ ├── codec.go │ ├── errors.go │ ├── genesis.go │ ├── genesis.pb.go │ ├── keys.go │ ├── query.pb.go │ ├── query.pb.gw.go │ ├── tx.go │ ├── tx.pb.go │ └── tx_test.go ├── gov ├── README.md ├── abci.go ├── keeper │ ├── keeper.go │ ├── keeper_test.go │ ├── msg_server.go │ ├── msg_server_test.go │ ├── query_server.go │ ├── query_server_test.go │ ├── tally.go │ ├── tally_test.go │ ├── vesting.go │ └── vesting_test.go ├── module.go ├── testdata │ ├── data.go │ └── vesting.wasm └── types │ ├── errors.go │ ├── metadata.go │ └── vesting.go ├── incentives ├── README.md ├── abci.go ├── client │ └── cli │ │ └── query.go ├── keeper │ ├── genesis.go │ ├── genesis_test.go │ ├── invariants.go │ ├── invariants_test.go │ ├── keeper.go │ ├── keeper_test.go │ ├── msg_server.go │ ├── msg_server_test.go │ ├── query_server.go │ ├── query_server_test.go │ ├── reward.go │ ├── reward_test.go │ ├── schedule.go │ └── schedule_test.go ├── module.go ├── module_test.go └── types │ ├── codec.go │ ├── errors.go │ ├── events.go │ ├── expected_keepers.go │ ├── genesis.go │ ├── genesis.pb.go │ ├── genesis_test.go │ ├── keys.go │ ├── query.pb.go │ ├── query.pb.gw.go │ ├── store.go │ ├── store.pb.go │ ├── store_test.go │ ├── tx.go │ ├── tx.pb.go │ └── tx_test.go └── safety ├── README.md ├── client └── cli │ └── query.go ├── keeper ├── genesis.go ├── keeper.go ├── msg_server.go └── query_server.go ├── module.go ├── module_test.go └── types ├── codec.go ├── errors.go ├── expected_keepers.go ├── genesis.go ├── genesis.pb.go ├── keys.go ├── query.pb.go ├── query.pb.gw.go ├── tx.go ├── tx.pb.go └── tx_test.go /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | envoy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v3 11 | 12 | - name: Download and setup wasmd source 13 | env: 14 | REPO: CosmWasm/wasmd 15 | TAG: 0.30.0 16 | run: | 17 | curl -OL https://github.com/${REPO}/archive/refs/tags/v${TAG}.zip 18 | unzip v${TAG}.zip 19 | mv wasmd-${TAG} wasmd 20 | 21 | - name: Setup Go-lang 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: '1.20' 25 | 26 | - name: Setup Rust toolchain 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: stable 30 | 31 | - name: Setup Deno 32 | uses: denoland/setup-deno@v1 33 | with: 34 | deno-version: v1.x 35 | 36 | - name: Cache Go artifacts 37 | uses: actions/cache@v3 38 | with: 39 | path: | 40 | ~/go 41 | ~/.cache/go-build 42 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} 43 | restore-keys: ${{ runner.os }}-go-build- 44 | 45 | - name: Cache Rust artifacts 46 | uses: actions/cache@v3 47 | with: 48 | path: | 49 | ~/.cargo 50 | key: ${{ runner.os }}-cargo 51 | 52 | - name: Cache Deno artifacts 53 | uses: actions/cache@v3 54 | with: 55 | path: | 56 | ~/.cache/deno 57 | key: ${{ runner.os }}-deno 58 | 59 | - name: Install marsd 60 | run: make install 61 | 62 | - name: Install wasmd 63 | working-directory: wasmd 64 | run: make install 65 | 66 | - name: Install hermes 67 | uses: actions-rs/cargo@v1 68 | with: 69 | command: install 70 | args: ibc-relayer-cli --bin hermes --locked 71 | 72 | - name: Unzip testdata 73 | run: tar -xzf tests/e2e/configs.tar.gz -C ~ 74 | 75 | - name: E2E test for Envoy module 76 | working-directory: tests/e2e 77 | run: deno test -A envoy_test.ts 78 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v3 12 | 13 | - name: Install Go 14 | uses: actions/setup-go@v4 15 | with: 16 | go-version: '1.20' 17 | 18 | - name: Run tests 19 | run: make test 20 | 21 | lint: 22 | name: Lint 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout sources 26 | uses: actions/checkout@v3 27 | 28 | - name: Install Go 29 | uses: actions/setup-go@v4 30 | with: 31 | go-version: '1.20' 32 | 33 | - name: Run linter 34 | run: make lint 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode/ 3 | 4 | # build 5 | build/ 6 | 7 | # testing 8 | testdata/ 9 | 10 | # macOS 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: ghcr.io/notional-labs/cosmos 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | timeout: 5m # timeout for analysis, e.g. 30s, 5m, default is 1m 4 | 5 | linters: 6 | disable-all: true 7 | enable: 8 | - depguard 9 | - dogsled 10 | - errcheck 11 | - exportloopref 12 | - goconst 13 | - gocritic 14 | - gofumpt 15 | - gosec 16 | - gosimple 17 | - govet 18 | - ineffassign 19 | - misspell 20 | - nakedret 21 | - nolintlint 22 | - prealloc 23 | - staticcheck 24 | - stylecheck 25 | - revive 26 | - typecheck 27 | - unconvert 28 | - unused 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG GO_VERSION="1.20" 4 | ARG RUNNER_IMAGE="gcr.io/distroless/static-debian11" 5 | 6 | # -------------------------------------------------------- 7 | # Builder 8 | # -------------------------------------------------------- 9 | 10 | FROM golang:${GO_VERSION}-alpine as builder 11 | 12 | ARG GIT_VERSION 13 | ARG GIT_COMMIT 14 | 15 | RUN apk add --no-cache \ 16 | ca-certificates \ 17 | build-base \ 18 | linux-headers 19 | 20 | # Download go dependencies 21 | WORKDIR /app 22 | COPY go.mod go.sum ./ 23 | RUN --mount=type=cache,target=/root/.cache/go-build \ 24 | --mount=type=cache,target=/root/go/pkg/mod \ 25 | go mod download 26 | 27 | # Cosmwasm - Download correct libwasmvm version 28 | RUN WASMVM_VERSION=$(go list -m github.com/CosmWasm/wasmvm | cut -d ' ' -f 2) && \ 29 | wget https://github.com/CosmWasm/wasmvm/releases/download/$WASMVM_VERSION/libwasmvm_muslc.$(uname -m).a \ 30 | -O /lib/libwasmvm_muslc.a && \ 31 | # verify checksum 32 | wget https://github.com/CosmWasm/wasmvm/releases/download/$WASMVM_VERSION/checksums.txt -O /tmp/checksums.txt && \ 33 | sha256sum /lib/libwasmvm_muslc.a | grep $(cat /tmp/checksums.txt | grep $(uname -m) | cut -d ' ' -f 1) 34 | 35 | # Copy the remaining files 36 | COPY . . 37 | 38 | # Build osmosisd binary 39 | RUN --mount=type=cache,target=/root/.cache/go-build \ 40 | --mount=type=cache,target=/root/go/pkg/mod \ 41 | GOWORK=off go build \ 42 | -mod=readonly \ 43 | -tags "netgo,ledger,muslc" \ 44 | -ldflags \ 45 | "-X github.com/cosmos/cosmos-sdk/version.Name="mars" \ 46 | -X github.com/cosmos/cosmos-sdk/version.AppName="marsd" \ 47 | -X github.com/cosmos/cosmos-sdk/version.Version=${GIT_VERSION} \ 48 | -X github.com/cosmos/cosmos-sdk/version.Commit=${GIT_COMMIT} \ 49 | -X github.com/cosmos/cosmos-sdk/version.BuildTags='netgo,ledger,muslc' \ 50 | -w -s -linkmode=external -extldflags '-Wl,-z,muldefs -static'" \ 51 | -trimpath \ 52 | -o /app/build/marsd \ 53 | /app/cmd/marsd 54 | 55 | # -------------------------------------------------------- 56 | # Runner 57 | # -------------------------------------------------------- 58 | 59 | FROM ${RUNNER_IMAGE} 60 | 61 | COPY --from=builder /app/build/marsd /bin/marsd 62 | 63 | ENV HOME /mars 64 | WORKDIR $HOME 65 | 66 | EXPOSE 26656 67 | EXPOSE 26657 68 | EXPOSE 1317 69 | 70 | ENTRYPOINT ["marsd"] 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mars Hub 2 | 3 | Mars Hub app-chain, built on top of [Tendermint][1], [Cosmos SDK][2], [IBC][3], and [CosmWasm][4]. 4 | 5 | ## Bug bounty 6 | 7 | A bug bounty is currently open for Mars Hub and peripheral contracts. See details [here](https://immunefi.com/bounty/mars/). 8 | 9 | ## Audits 10 | 11 | See reports [here](https://github.com/mars-protocol/mars-audits/tree/main/hub). 12 | 13 | ## Installation 14 | 15 | Install the latest version of [Go programming language][5] and configure related environment variables. See [here][6] for a tutorial. 16 | 17 | Clone this repository, checkout to the latest tag, the compile the code: 18 | 19 | ```bash 20 | git clone https://github.com/mars-protocol/hub.git 21 | cd hub 22 | git checkout 23 | make install 24 | ``` 25 | 26 | A `marsd` executable will be created in the `$GOBIN` directory. 27 | 28 | ## License 29 | 30 | Contents of this repository are open source under [GNU General Public License v3](./LICENSE) or later. 31 | 32 | [1]: https://github.com/tendermint/tendermint 33 | [2]: https://github.com/cosmos/cosmos-sdk 34 | [3]: https://github.com/cosmos/ibc-go 35 | [4]: https://github.com/CosmWasm/wasmd 36 | [5]: https://go.dev/dl/ 37 | [6]: https://github.com/larry0x/workshops/tree/main/how-to-run-a-validator 38 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | If there is any security issue with this repository, please contact the following email address(es): 2 | 3 | - dane@delphilabs.io 4 | - larry@delphilabs.io 5 | -------------------------------------------------------------------------------- /app/ante.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | storetypes "github.com/cosmos/cosmos-sdk/store/types" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 7 | "github.com/cosmos/cosmos-sdk/x/auth/ante" 8 | 9 | ibcante "github.com/cosmos/ibc-go/v6/modules/core/ante" 10 | ibckeeper "github.com/cosmos/ibc-go/v6/modules/core/keeper" 11 | 12 | wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" 13 | wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" 14 | ) 15 | 16 | // HandlerOptions extends the SDK's `AnteHandler` options by requiring 17 | // additional keepers. 18 | type HandlerOptions struct { 19 | ante.HandlerOptions 20 | 21 | IBCKeeper *ibckeeper.Keeper 22 | WasmConfig wasmtypes.WasmConfig 23 | TxCounterStoreKey storetypes.StoreKey 24 | } 25 | 26 | // NewAnteHandler returns an AnteHandler that checks and increments sequence 27 | // numbers, checks signatures & account numbers, and deducts fees from the first 28 | // signer. 29 | func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { 30 | if options.AccountKeeper == nil { 31 | return nil, sdkerrors.ErrLogic.Wrap("account keeper is required for ante builder") 32 | } 33 | if options.BankKeeper == nil { 34 | return nil, sdkerrors.ErrLogic.Wrap("bank keeper is required for ante builder") 35 | } 36 | if options.SignModeHandler == nil { 37 | return nil, sdkerrors.ErrLogic.Wrap("sign mode handler is required for ante builder") 38 | } 39 | 40 | sigGasConsumer := options.SigGasConsumer 41 | if sigGasConsumer == nil { 42 | sigGasConsumer = ante.DefaultSigVerificationGasConsumer 43 | } 44 | 45 | anteDecorators := []sdk.AnteDecorator{ 46 | // core 47 | ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first 48 | ante.NewValidateBasicDecorator(), 49 | ante.NewTxTimeoutHeightDecorator(), 50 | ante.NewValidateMemoDecorator(options.AccountKeeper), 51 | ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), 52 | ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), 53 | // SetPubKeyDecorator must be called before all signature verification decorators 54 | ante.NewSetPubKeyDecorator(options.AccountKeeper), 55 | ante.NewValidateSigCountDecorator(options.AccountKeeper), 56 | ante.NewSigGasConsumeDecorator(options.AccountKeeper, sigGasConsumer), 57 | ante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), 58 | ante.NewIncrementSequenceDecorator(options.AccountKeeper), 59 | 60 | // ibc 61 | ibcante.NewRedundantRelayDecorator(options.IBCKeeper), 62 | 63 | // wasm 64 | wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), 65 | wasmkeeper.NewCountTXDecorator(options.TxCounterStoreKey), 66 | } 67 | 68 | return sdk.ChainAnteDecorators(anteDecorators...), nil 69 | } 70 | -------------------------------------------------------------------------------- /app/encoding.go: -------------------------------------------------------------------------------- 1 | // https://github.com/ignite-hq/cli/blob/v0.21.2/ignite/pkg/cosmoscmd/encoding.go 2 | 3 | package app 4 | 5 | import ( 6 | "github.com/cosmos/cosmos-sdk/client" 7 | "github.com/cosmos/cosmos-sdk/codec" 8 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 9 | "github.com/cosmos/cosmos-sdk/std" 10 | "github.com/cosmos/cosmos-sdk/x/auth/tx" 11 | ) 12 | 13 | // EncodingConfig specifies the concrete encoding types to use for a given app. 14 | // This is provided for compatibility between protobuf and amino implementations. 15 | type EncodingConfig struct { 16 | InterfaceRegistry codectypes.InterfaceRegistry 17 | Codec codec.Codec 18 | TxConfig client.TxConfig 19 | Amino *codec.LegacyAmino 20 | } 21 | 22 | // MakeEncodingConfig creates an EncodingConfig instance 23 | func NewEncodingConfig() EncodingConfig { 24 | amino := codec.NewLegacyAmino() 25 | interfaceRegistry := codectypes.NewInterfaceRegistry() 26 | codec := codec.NewProtoCodec(interfaceRegistry) 27 | txCfg := tx.NewTxConfig(codec, tx.DefaultSignModes) 28 | 29 | encodingConfig := EncodingConfig{ 30 | InterfaceRegistry: interfaceRegistry, 31 | Codec: codec, 32 | TxConfig: txCfg, 33 | Amino: amino, 34 | } 35 | 36 | return encodingConfig 37 | } 38 | 39 | // MakeEncodingConfig creates an EncodingConfig instance; registers types with 40 | // codec and interface registry. 41 | func MakeEncodingConfig() EncodingConfig { 42 | encodingConfig := NewEncodingConfig() 43 | 44 | std.RegisterLegacyAminoCodec(encodingConfig.Amino) 45 | std.RegisterInterfaces(encodingConfig.InterfaceRegistry) 46 | ModuleBasics.RegisterLegacyAminoCodec(encodingConfig.Amino) 47 | ModuleBasics.RegisterInterfaces(encodingConfig.InterfaceRegistry) 48 | 49 | return encodingConfig 50 | } 51 | -------------------------------------------------------------------------------- /app/genesis.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/cosmos/cosmos-sdk/codec" 7 | ) 8 | 9 | // The genesis state of the blockchain is represented here as a map of raw JSON 10 | // messages key'd by an identifier string. 11 | // 12 | // The identifier is used to determine which module genesis information belongs 13 | // to, so it may be appropriately routed during init chain. 14 | // 15 | // Within this application, default genesis information is retieved from the 16 | // `ModuleBasicManager` which populates JSON from each `BasicModule` object 17 | // provided to it during init. 18 | type GenesisState map[string]json.RawMessage 19 | 20 | // DefaultGenesisState generates the default state for the application. 21 | func DefaultGenesisState(cdc codec.JSONCodec) GenesisState { 22 | return ModuleBasics.DefaultGenesis(cdc) 23 | } 24 | -------------------------------------------------------------------------------- /app/upgrades/types.go: -------------------------------------------------------------------------------- 1 | package upgrades 2 | 3 | import ( 4 | store "github.com/cosmos/cosmos-sdk/store/types" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | "github.com/cosmos/cosmos-sdk/types/module" 7 | upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" 8 | ) 9 | 10 | // Upgrade defines a struct containing necessary fields that a 11 | // SoftwareUpgradeProposal must have written, in order for the state migration 12 | // to go smoothly. 13 | // 14 | // An upgrade must implement this struct, and then set it in the app.go. 15 | // The app.go will then define the handler. 16 | type Upgrade struct { 17 | // version name for the upgrade handler, e.g. `v7` 18 | UpgradeName string 19 | 20 | // CreateUpgradeHandler defines the function that creates an upgrade handler 21 | CreateUpgradeHandler func(*module.Manager, module.Configurator) upgradetypes.UpgradeHandler 22 | 23 | // used for any new modules introduced, new modules deleted, or store names renamed 24 | StoreUpgrades store.StoreUpgrades 25 | } 26 | 27 | // Fork defines a struct containing the requisite fields for a non-software 28 | // upgrade proposal Hard Fork at a given height to implement. 29 | // 30 | // There is one time code that can be added for the start of the Fork, in 31 | // `BeginForkLogic`. 32 | // 33 | // Any other change in the code should be height-gated, if the goal is to have 34 | // old and new binaries to be compatible prior to the upgrade height. 35 | type Fork struct { 36 | // version name for the upgrade handler, e.g. `v7` 37 | UpgradeName string 38 | 39 | // height the upgrade occurs at 40 | UpgradeHeight int64 41 | 42 | // function that runs some custom state transition code at the beginning of a fork 43 | BeginForkLogic func(ctx sdk.Context) 44 | } 45 | -------------------------------------------------------------------------------- /app/upgrades/v2/constants.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import ( 4 | store "github.com/cosmos/cosmos-sdk/store/types" 5 | 6 | icacontrollertypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/controller/types" 7 | icahosttypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/host/types" 8 | 9 | "github.com/mars-protocol/hub/v2/app/upgrades" 10 | ) 11 | 12 | var Upgrade = upgrades.Upgrade{ 13 | UpgradeName: "v2", 14 | CreateUpgradeHandler: CreateUpgradeHandler, 15 | StoreUpgrades: store.StoreUpgrades{ 16 | Added: []string{ 17 | icacontrollertypes.StoreKey, 18 | icahosttypes.StoreKey, 19 | // envoy module does not store anything in the chain state, so doesn't 20 | // need a store upgrade for it 21 | }, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /app/upgrades/v2/upgrades.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | import ( 4 | "fmt" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | "github.com/cosmos/cosmos-sdk/types/module" 8 | upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" 9 | 10 | ica "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts" 11 | icacontrollertypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/controller/types" 12 | icahosttypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/host/types" 13 | icatypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/types" 14 | 15 | "github.com/mars-protocol/hub/v2/x/envoy" 16 | envoytypes "github.com/mars-protocol/hub/v2/x/envoy/types" 17 | ) 18 | 19 | // CreateUpgradeHandler creates the upgrade handler for the v2 upgrade. 20 | // 21 | // In this upgrade, we add two new modules, ICA and envoy, without making any 22 | // change to the existing modules. 23 | func CreateUpgradeHandler(mm *module.Manager, configurator module.Configurator) upgradetypes.UpgradeHandler { 24 | return func(ctx sdk.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { 25 | ctx.Logger().Info("🚀 executing Mars Hub v2 upgrade 🚀") 26 | 27 | ctx.Logger().Info("initializing interchain account module") 28 | initICAModule(ctx, mm, vm) 29 | 30 | ctx.Logger().Info("initializing envoy module") 31 | initEnvoyModule(ctx, mm, vm) 32 | 33 | return mm.RunMigrations(ctx, configurator, vm) 34 | } 35 | } 36 | 37 | func initICAModule(ctx sdk.Context, mm *module.Manager, vm module.VersionMap) { 38 | vm[icatypes.ModuleName] = mm.Modules[icatypes.ModuleName].ConsensusVersion() 39 | 40 | controllerParams := icacontrollertypes.Params{ 41 | ControllerEnabled: true, 42 | } 43 | 44 | hostParams := icahosttypes.Params{ 45 | HostEnabled: true, 46 | AllowMessages: []string{ 47 | "/cosmos.authz.v1beta1.MsgExec", 48 | "/cosmos.authz.v1beta1.MsgGrant", 49 | "/cosmos.authz.v1beta1.MsgRevoke", 50 | "/cosmos.bank.v1beta1.MsgSend", 51 | "/cosmos.bank.v1beta1.MsgMultiSend", 52 | "/cosmos.distribution.v1beta1.MsgFundCommunityPoolResponse", 53 | "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", 54 | "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", 55 | "/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission", 56 | "/cosmos.feegrant.v1beta1.MsgGrantAllowance", 57 | "/cosmos.feegrant.v1beta1.MsgRevokeAllowance", 58 | "/cosmos.gov.v1.MsgSubmitProposal", 59 | "/cosmos.gov.v1.MsgDeposit", 60 | "/cosmos.gov.v1.MsgVote", 61 | "/cosmos.gov.v1.MsgVoteWeighted", 62 | "/cosmos.slashing.v1beta1.MsgUnjail", 63 | "/cosmos.staking.v1beta1.MsgCreateValidator", 64 | "/cosmos.staking.v1beta1.MsgEditValidator", 65 | "/cosmos.staking.v1beta1.MsgDelegate", 66 | "/cosmos.staking.v1beta1.MsgBeginRedelegate", 67 | "/cosmos.staking.v1beta1.MsgUndelegate", 68 | "/cosmos.staking.v1beta1.MsgCancelUnbondingDelegation", 69 | "/ibc.applications.transfer.v1.MsgTransfer", 70 | "/cosmwasm.wasm.v1.MsgStoreCode", 71 | "/cosmwasm.wasm.v1.MsgInstantiateContract", 72 | "/cosmwasm.wasm.v1.MsgInstantiateContract2", 73 | "/cosmwasm.wasm.v1.MsgExecuteContract", 74 | "/cosmwasm.wasm.v1.MsgMigrateContract", 75 | "/cosmwasm.wasm.v1.MsgUpdateAdmin", 76 | "/cosmwasm.wasm.v1.MsgClearAdmin", 77 | "/cosmwasm.wasm.v1.MsgUpdateInstantiateConfig", 78 | }, 79 | } 80 | 81 | icaModule := getAppModule[ica.AppModule](mm, icatypes.ModuleName, "ica.AppModule") 82 | icaModule.InitModule(ctx, controllerParams, hostParams) 83 | } 84 | 85 | func initEnvoyModule(ctx sdk.Context, mm *module.Manager, vm module.VersionMap) { 86 | vm[envoytypes.ModuleName] = mm.Modules[envoytypes.ModuleName].ConsensusVersion() 87 | 88 | envoyModule := getAppModule[envoy.AppModule](mm, envoytypes.ModuleName, "envoy.AppModule") 89 | envoyModule.InitModule(ctx) 90 | } 91 | 92 | func getAppModule[T module.AppModule](mm *module.Manager, moduleName, typeName string) T { 93 | module, correctTypeCast := mm.Modules[moduleName].(T) 94 | if !correctTypeCast { 95 | panic(fmt.Sprintf("mm.Modules[\"%s\"] is not of %s", moduleName, typeName)) 96 | } 97 | 98 | return module 99 | } 100 | -------------------------------------------------------------------------------- /app/wasm/msg.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import sdk "github.com/cosmos/cosmos-sdk/types" 4 | 5 | type MarsMsg struct { 6 | FundCommunityPool *FundCommunityPool `json:"fund_community_pool,omitempty"` 7 | } 8 | 9 | type FundCommunityPool struct { 10 | Amount sdk.Coins `json:"amount"` 11 | } 12 | -------------------------------------------------------------------------------- /app/wasm/msg_plugin.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "cosmossdk.io/errors" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | 10 | wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" 11 | wasmvmtypes "github.com/CosmWasm/wasmvm/types" 12 | 13 | distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" 14 | distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" 15 | ) 16 | 17 | func CustomMessageDecorator(distrKeeper *distrkeeper.Keeper) func(wasmkeeper.Messenger) wasmkeeper.Messenger { 18 | return func(old wasmkeeper.Messenger) wasmkeeper.Messenger { 19 | return &CustomMessenger{ 20 | wrapped: old, 21 | distrKeeper: distrKeeper, 22 | } 23 | } 24 | } 25 | 26 | type CustomMessenger struct { 27 | wrapped wasmkeeper.Messenger 28 | distrKeeper *distrkeeper.Keeper 29 | } 30 | 31 | // CustomKeeper must implement the `wasmkeeper.Messenger` interface 32 | var _ wasmkeeper.Messenger = (*CustomMessenger)(nil) 33 | 34 | func (m *CustomMessenger) DispatchMsg( 35 | ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg, 36 | ) ([]sdk.Event, [][]byte, error) { 37 | // if the msg is a custom msg, parse it into `MarsMsg` then dispatch to the appropriate Mars module 38 | // otherwise, simply dispatch it to the wrapped messenger 39 | if msg.Custom != nil { 40 | var marsMsg MarsMsg 41 | if err := json.Unmarshal(msg.Custom, &marsMsg); err != nil { 42 | return nil, nil, errors.Wrapf(err, "invalid custom msg: %s", msg.Custom) 43 | } 44 | 45 | if marsMsg.FundCommunityPool != nil { 46 | return fundCommunityPool(ctx, m.distrKeeper, contractAddr, marsMsg.FundCommunityPool) 47 | } 48 | } 49 | 50 | return m.wrapped.DispatchMsg(ctx, contractAddr, contractIBCPortID, msg) 51 | } 52 | 53 | func fundCommunityPool( 54 | ctx sdk.Context, k *distrkeeper.Keeper, contractAddr sdk.AccAddress, 55 | fundCommunityPool *FundCommunityPool, 56 | ) ([]sdk.Event, [][]byte, error) { 57 | msgServer := distrkeeper.NewMsgServerImpl(*k) 58 | 59 | msg := &distrtypes.MsgFundCommunityPool{ 60 | Amount: fundCommunityPool.Amount, 61 | Depositor: contractAddr.String(), 62 | } 63 | 64 | if _, err := msgServer.FundCommunityPool(sdk.WrapSDKContext(ctx), msg); err != nil { 65 | return nil, nil, err 66 | } 67 | 68 | return nil, nil, nil 69 | } 70 | -------------------------------------------------------------------------------- /app/wasm/query.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | type MarsQuery struct { 4 | // currently we don't have any custom query implemented 5 | } 6 | -------------------------------------------------------------------------------- /app/wasm/query_plugin.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "cosmossdk.io/errors" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | 10 | wasmvmtypes "github.com/CosmWasm/wasmvm/types" 11 | ) 12 | 13 | func CustomQuerier(*QueryPlugin) func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { 14 | return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { 15 | var marsQuery MarsQuery 16 | if err := json.Unmarshal(request, &marsQuery); err != nil { 17 | return nil, errors.Wrapf(err, "invalid custom query: %s", request) 18 | } 19 | 20 | // here, dispatch query request to the appropriate query function 21 | 22 | return nil, wasmvmtypes.UnsupportedRequest{Kind: "unknown custom query variant"} 23 | } 24 | } 25 | 26 | type QueryPlugin struct { 27 | // currently we don't have any custom query implemented 28 | } 29 | -------------------------------------------------------------------------------- /app/wasm/wasm.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" 5 | 6 | wasm "github.com/CosmWasm/wasmd/x/wasm" 7 | wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" 8 | ) 9 | 10 | func RegisterCustomPlugins(distrKeeper *distrkeeper.Keeper) []wasm.Option { 11 | messengerDecoratorOpt := wasmkeeper.WithMessageHandlerDecorator( 12 | CustomMessageDecorator(distrKeeper), 13 | ) 14 | 15 | queryPluginOpt := wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ 16 | Custom: CustomQuerier(&QueryPlugin{}), 17 | }) 18 | 19 | return []wasm.Option{messengerDecoratorOpt, queryPluginOpt} 20 | } 21 | -------------------------------------------------------------------------------- /cmd/marsd/address.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import sdk "github.com/cosmos/cosmos-sdk/types" 4 | 5 | // Define prefixes for addresses and public keys 6 | func setAddressPrefixes(accountAddressPrefix string) { 7 | accountPubKeyPrefix := accountAddressPrefix + "pub" 8 | validatorAddressPrefix := accountAddressPrefix + "valoper" 9 | validatorPubKeyPrefix := accountAddressPrefix + "valoperpub" 10 | consNodeAddressPrefix := accountAddressPrefix + "valcons" 11 | consNodePubKeyPrefix := accountAddressPrefix + "valconspub" 12 | 13 | config := sdk.GetConfig() 14 | config.SetBech32PrefixForAccount(accountAddressPrefix, accountPubKeyPrefix) 15 | config.SetBech32PrefixForValidator(validatorAddressPrefix, validatorPubKeyPrefix) 16 | config.SetBech32PrefixForConsensusNode(consNodeAddressPrefix, consNodePubKeyPrefix) 17 | config.Seal() 18 | } 19 | -------------------------------------------------------------------------------- /cmd/marsd/appcreator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "path/filepath" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/spf13/cast" 10 | 11 | "github.com/tendermint/tendermint/libs/log" 12 | dbm "github.com/tendermint/tm-db" 13 | 14 | "github.com/cosmos/cosmos-sdk/baseapp" 15 | "github.com/cosmos/cosmos-sdk/client/flags" 16 | "github.com/cosmos/cosmos-sdk/server" 17 | servertypes "github.com/cosmos/cosmos-sdk/server/types" 18 | "github.com/cosmos/cosmos-sdk/snapshots" 19 | snapshottypes "github.com/cosmos/cosmos-sdk/snapshots/types" 20 | "github.com/cosmos/cosmos-sdk/store" 21 | sdk "github.com/cosmos/cosmos-sdk/types" 22 | 23 | "github.com/CosmWasm/wasmd/x/wasm" 24 | wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" 25 | 26 | marsapp "github.com/mars-protocol/hub/v2/app" 27 | ) 28 | 29 | // appCreator is a wrapper for EncodingConfig. This allows us to reuse 30 | // encodingConfig received by NewRootCmd in both createApp and exportApp. 31 | type appCreator struct{ encodingConfig marsapp.EncodingConfig } 32 | 33 | func (ac appCreator) createApp( 34 | logger log.Logger, 35 | db dbm.DB, 36 | traceStore io.Writer, 37 | appOpts servertypes.AppOptions, 38 | ) servertypes.Application { 39 | var cache sdk.MultiStorePersistentCache 40 | 41 | if cast.ToBool(appOpts.Get(server.FlagInterBlockCache)) { 42 | cache = store.NewCommitKVStoreCacheManager() 43 | } 44 | 45 | skipUpgradeHeights := make(map[int64]bool) 46 | for _, h := range cast.ToIntSlice(appOpts.Get(server.FlagUnsafeSkipUpgrades)) { 47 | skipUpgradeHeights[int64(h)] = true 48 | } 49 | 50 | pruningOpts, err := server.GetPruningOptionsFromFlags(appOpts) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | snapshotDir := filepath.Join(cast.ToString(appOpts.Get(flags.FlagHome)), "data", "snapshots") 56 | snapshotDB, err := dbm.NewDB("metadata", server.GetAppDBBackend(appOpts), snapshotDir) 57 | if err != nil { 58 | panic(err) 59 | } 60 | snapshotStore, err := snapshots.NewStore(snapshotDB, snapshotDir) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | snapshotOptions := snapshottypes.NewSnapshotOptions( 66 | cast.ToUint64(appOpts.Get(server.FlagStateSyncSnapshotInterval)), 67 | cast.ToUint32(appOpts.Get(server.FlagStateSyncSnapshotKeepRecent)), 68 | ) 69 | 70 | var wasmOpts []wasm.Option 71 | if cast.ToBool(appOpts.Get("telemetry.enabled")) { 72 | wasmOpts = append(wasmOpts, wasmkeeper.WithVMCacheMetrics(prometheus.DefaultRegisterer)) 73 | } 74 | 75 | return marsapp.NewMarsApp( 76 | logger, db, traceStore, true, skipUpgradeHeights, 77 | cast.ToString(appOpts.Get(flags.FlagHome)), 78 | cast.ToUint(appOpts.Get(server.FlagInvCheckPeriod)), 79 | ac.encodingConfig, 80 | appOpts, 81 | wasmOpts, 82 | baseapp.SetPruning(pruningOpts), 83 | baseapp.SetMinGasPrices(cast.ToString(appOpts.Get(server.FlagMinGasPrices))), 84 | baseapp.SetHaltHeight(cast.ToUint64(appOpts.Get(server.FlagHaltHeight))), 85 | baseapp.SetHaltTime(cast.ToUint64(appOpts.Get(server.FlagHaltTime))), 86 | baseapp.SetMinRetainBlocks(cast.ToUint64(appOpts.Get(server.FlagMinRetainBlocks))), 87 | baseapp.SetInterBlockCache(cache), 88 | baseapp.SetTrace(cast.ToBool(appOpts.Get(server.FlagTrace))), 89 | baseapp.SetIndexEvents(cast.ToStringSlice(appOpts.Get(server.FlagIndexEvents))), 90 | baseapp.SetSnapshot(snapshotStore, snapshotOptions), 91 | ) 92 | } 93 | 94 | func (ac appCreator) exportApp( 95 | logger log.Logger, 96 | db dbm.DB, 97 | traceStore io.Writer, 98 | height int64, 99 | forZeroHeight bool, 100 | jailWhiteList []string, 101 | appOpts servertypes.AppOptions, 102 | ) (servertypes.ExportedApp, error) { 103 | homePath, ok := appOpts.Get(flags.FlagHome).(string) 104 | if !ok || homePath == "" { 105 | return servertypes.ExportedApp{}, errors.New("application home not set") 106 | } 107 | 108 | app := marsapp.NewMarsApp( 109 | logger, 110 | db, 111 | traceStore, 112 | height == -1, // -1 means no height is provided 113 | map[int64]bool{}, 114 | homePath, 115 | uint(1), 116 | ac.encodingConfig, 117 | appOpts, 118 | []wasm.Option{}, 119 | ) 120 | 121 | if height != -1 { 122 | if err := app.LoadHeight(height); err != nil { 123 | return servertypes.ExportedApp{}, err 124 | } 125 | } 126 | 127 | return app.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) 128 | } 129 | -------------------------------------------------------------------------------- /cmd/marsd/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import serverconfig "github.com/cosmos/cosmos-sdk/server/config" 4 | 5 | // initAppConfig generates contents for `app.toml`. Take the default template 6 | // and config, append custom parameters. 7 | 8 | func initAppConfig() (string, interface{}) { 9 | template := serverconfig.DefaultConfigTemplate 10 | cfg := serverconfig.DefaultConfig() 11 | 12 | // The SDK's default minimum gas price is set to "" (empty value) inside 13 | // app.toml. If left empty by validators, the node will halt on startup. 14 | // However, the chain developer can set a default app.toml value for their 15 | // validators here. 16 | // 17 | // In summary: 18 | // - if you leave srvCfg.MinGasPrices = "", all validators MUST tweak 19 | // their own app.toml config, 20 | // - if you set srvCfg.MinGasPrices non-empty, validators CAN tweak their 21 | // own app.toml to override, or use this default value. 22 | cfg.MinGasPrices = "0umars" 23 | 24 | return template, cfg 25 | } 26 | -------------------------------------------------------------------------------- /cmd/marsd/genwasm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/client" 5 | "github.com/spf13/cobra" 6 | 7 | wasmcli "github.com/CosmWasm/wasmd/x/wasm/client/cli" 8 | ) 9 | 10 | func addGenesisWasmMsgCmd(defaultNodeHome string) *cobra.Command { 11 | txCmd := &cobra.Command{ 12 | Use: "add-wasm-message", 13 | Short: "Wasm genesis subcommands", 14 | DisableFlagParsing: true, 15 | SuggestionsMinimumDistance: 2, 16 | RunE: client.ValidateCmd, 17 | } 18 | 19 | genesisIO := wasmcli.NewDefaultGenesisIO() 20 | 21 | txCmd.AddCommand( 22 | wasmcli.GenesisStoreCodeCmd(defaultNodeHome, genesisIO), 23 | wasmcli.GenesisInstantiateContractCmd(defaultNodeHome, genesisIO), 24 | wasmcli.GenesisExecuteContractCmd(defaultNodeHome, genesisIO), 25 | wasmcli.GenesisListContractsCmd(defaultNodeHome, genesisIO), 26 | wasmcli.GenesisListCodesCmd(defaultNodeHome, genesisIO), 27 | ) 28 | 29 | return txCmd 30 | } 31 | -------------------------------------------------------------------------------- /cmd/marsd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" 7 | 8 | marsapp "github.com/mars-protocol/hub/v2/app" 9 | ) 10 | 11 | func main() { 12 | setAddressPrefixes(marsapp.AccountAddressPrefix) 13 | rootCmd := NewRootCmd(marsapp.MakeEncodingConfig()) 14 | if err := svrcmd.Execute(rootCmd, "MARS", marsapp.DefaultNodeHome); err != nil { 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Mars Hub - REST API", 5 | "description": "REST interface for query and transaction services" 6 | }, 7 | "apis": [ 8 | { 9 | "url": "./tmp-swagger-gen/cosmos/auth/v1beta1/query.swagger.json", 10 | "operationIds": { 11 | "rename": { 12 | "Params": "AuthParams" 13 | } 14 | } 15 | }, 16 | { 17 | "url": "./tmp-swagger-gen/cosmos/authz/v1beta1/query.swagger.json" 18 | }, 19 | { 20 | "url": "./tmp-swagger-gen/cosmos/bank/v1beta1/query.swagger.json", 21 | "operationIds": { 22 | "rename": { 23 | "Params": "BankParams" 24 | } 25 | } 26 | }, 27 | { 28 | "url": "./tmp-swagger-gen/cosmos/base/tendermint/v1beta1/query.swagger.json" 29 | }, 30 | { 31 | "url": "./tmp-swagger-gen/cosmos/distribution/v1beta1/query.swagger.json", 32 | "operationIds": { 33 | "rename": { 34 | "Params": "DistributionParams" 35 | } 36 | } 37 | }, 38 | { 39 | "url": "./tmp-swagger-gen/cosmos/evidence/v1beta1/query.swagger.json" 40 | }, 41 | { 42 | "url": "./tmp-swagger-gen/cosmos/feegrant/v1beta1/query.swagger.json" 43 | }, 44 | { 45 | "url": "./tmp-swagger-gen/cosmos/gov/v1beta1/query.swagger.json", 46 | "operationIds": { 47 | "rename": { 48 | "Params": "GovParams" 49 | } 50 | } 51 | }, 52 | { 53 | "url": "./tmp-swagger-gen/cosmos/params/v1beta1/query.swagger.json", 54 | "operationIds": { 55 | "rename": { 56 | "Params": "ParamsParams" 57 | } 58 | } 59 | }, 60 | { 61 | "url": "./tmp-swagger-gen/cosmos/slashing/v1beta1/query.swagger.json", 62 | "operationIds": { 63 | "rename": { 64 | "Params": "SlashingParams" 65 | } 66 | } 67 | }, 68 | { 69 | "url": "./tmp-swagger-gen/cosmos/staking/v1beta1/query.swagger.json", 70 | "operationIds": { 71 | "rename": { 72 | "Params": "StakingParams", 73 | "DelegatorValidators": "StakingDelegatorValidators" 74 | } 75 | } 76 | }, 77 | { 78 | "url": "./tmp-swagger-gen/cosmos/tx/v1beta1/service.swagger.json", 79 | "dereference": { 80 | "circular": "ignore" 81 | } 82 | }, 83 | { 84 | "url": "./tmp-swagger-gen/cosmos/upgrade/v1beta1/query.swagger.json" 85 | }, 86 | { 87 | "url": "./tmp-swagger-gen/ibc/core/channel/v1/query.swagger.json" 88 | }, 89 | { 90 | "url": "./tmp-swagger-gen/ibc/core/client/v1/query.swagger.json", 91 | "operationIds": { 92 | "rename": { 93 | "Params": "IBCClientParams", 94 | "UpgradedConsensusState": "IBCClientUpgradedConsensusState" 95 | } 96 | } 97 | }, 98 | { 99 | "url": "./tmp-swagger-gen/ibc/core/connection/v1/query.swagger.json" 100 | }, 101 | { 102 | "url": "./tmp-swagger-gen/ibc/applications/transfer/v1/query.swagger.json", 103 | "operationIds": { 104 | "rename": { 105 | "Params": "IBCTransferParams" 106 | } 107 | } 108 | }, 109 | { 110 | "url": "./tmp-swagger-gen/cosmwasm/wasm/v1/query.swagger.json" 111 | }, 112 | { 113 | "url": "./tmp-swagger-gen/mars/incentives/v1beta1/query.swagger.json" 114 | }, 115 | { 116 | "url": "./tmp-swagger-gen/mars/safety/v1beta1/query.swagger.json" 117 | } 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import "embed" 4 | 5 | // Swagger is the data of the swagger page generated by protobuf 6 | // 7 | //go:embed swagger.yml 8 | var Swagger embed.FS 9 | -------------------------------------------------------------------------------- /docs/handler.go: -------------------------------------------------------------------------------- 1 | // Forked from 2 | // https://github.com/tendermint/starport/blob/v0.19.2-alpha/starport/pkg/openapiconsole/console.go 3 | 4 | package docs 5 | 6 | import ( 7 | "embed" 8 | "html/template" 9 | "net/http" 10 | ) 11 | 12 | //go:embed index.tpl 13 | var index embed.FS 14 | 15 | // Handler returns an http handler that servers OpenAPI console for an OpenAPI 16 | // spec at specURL. 17 | func Handler(title, specURL string) http.HandlerFunc { 18 | t, err := template.ParseFS(index, "index.tpl") 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | return func(w http.ResponseWriter, req *http.Request) { 24 | err := t.Execute(w, struct { 25 | Title string 26 | URL string 27 | }{ 28 | title, 29 | specURL, 30 | }) 31 | if err != nil { 32 | panic(err) // this might not be the right way to check this error. 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ .Title }} 6 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /proto/buf.gen.go.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - name: gocosmos 4 | out: . 5 | opt: plugins=grpc,Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types 6 | - name: grpc-gateway 7 | out: . 8 | opt: logtostderr=true,allow_colon_final_segments=true 9 | -------------------------------------------------------------------------------- /proto/buf.gen.swagger.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - name: swagger 4 | out: ./tmp-swagger-gen 5 | opt: logtostderr=true,fqn_for_swagger_name=true,simple_operation_ids=true 6 | -------------------------------------------------------------------------------- /proto/buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v1 3 | deps: 4 | - remote: buf.build 5 | owner: cosmos 6 | repository: cosmos-proto 7 | commit: 1935555c206d4afb9e94615dfd0fad31 8 | - remote: buf.build 9 | owner: cosmos 10 | repository: cosmos-sdk 11 | commit: e32a5c313be34bb28b1d3274d72d6286 12 | - remote: buf.build 13 | owner: cosmos 14 | repository: gogo-proto 15 | commit: 34d970b699f84aa382f3c29773a60836 16 | - remote: buf.build 17 | owner: googleapis 18 | repository: googleapis 19 | commit: 75b4300737fb4efca0831636be94e517 20 | -------------------------------------------------------------------------------- /proto/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | name: buf.build/mars-protocol/hub 3 | deps: 4 | - buf.build/cosmos/cosmos-proto 5 | - buf.build/cosmos/cosmos-sdk 6 | - buf.build/cosmos/gogo-proto 7 | - buf.build/googleapis/googleapis 8 | breaking: 9 | use: 10 | - FILE 11 | lint: 12 | use: 13 | - DEFAULT 14 | - COMMENTS 15 | - FILE_LOWER_SNAKE_CASE 16 | except: 17 | - UNARY_RPC 18 | - COMMENT_FIELD 19 | - SERVICE_SUFFIX 20 | - PACKAGE_VERSION_SUFFIX 21 | - RPC_REQUEST_STANDARD_NAME 22 | -------------------------------------------------------------------------------- /proto/mars/envoy/v1beta1/genesis.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.envoy.v1beta1; 3 | 4 | option go_package = "github.com/mars-protocol/hub/x/envoy/types"; 5 | 6 | // GenesisState defines the module's genesis state. 7 | message GenesisState {} 8 | -------------------------------------------------------------------------------- /proto/mars/envoy/v1beta1/query.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.envoy.v1beta1; 3 | 4 | option go_package = "github.com/mars-protocol/hub/x/envoy/types"; 5 | 6 | import "gogoproto/gogo.proto"; 7 | import "google/api/annotations.proto"; 8 | 9 | // Query defines the module's gRPC query service. 10 | service Query { 11 | // Account returns the interchain account owned by the module on a given 12 | // connection pair. 13 | rpc Account(QueryAccountRequest) returns (QueryAccountResponse) { 14 | option (google.api.http).get = "/mars/envoy/v1beta1/account/{connection_id}"; 15 | } 16 | 17 | // Accounts returns all interchain accounts owned by the module. 18 | rpc Accounts(QueryAccountsRequest) returns (QueryAccountsResponse) { 19 | option (google.api.http).get = "/mars/envoy/v1beta1/accounts"; 20 | } 21 | } 22 | 23 | //------------------------------------------------------------------------------ 24 | // Account 25 | //------------------------------------------------------------------------------ 26 | 27 | // QueryAccountRequest is the request type for the Query/Account RPC method. 28 | message QueryAccountRequest { 29 | // ConnectionId identified the connection associated with the interchain 30 | // account. 31 | string connection_id = 1 [(gogoproto.moretags) = "yaml:\"connection_id\""]; 32 | } 33 | 34 | // QueryAccountResponse is the response type for the Query/Account RPC method. 35 | message QueryAccountResponse { 36 | AccountInfo account = 1; 37 | } 38 | 39 | //------------------------------------------------------------------------------ 40 | // Accounts 41 | //------------------------------------------------------------------------------ 42 | 43 | // QueryAccountsRequest is the request type for the Query/Accounts RPC method. 44 | message QueryAccountsRequest {} 45 | 46 | // QueryAccountsResponse is the response type for Query/Accounts RPC method. 47 | message QueryAccountsResponse { 48 | repeated AccountInfo accounts = 1; 49 | } 50 | 51 | //------------------------------------------------------------------------------ 52 | // Other types 53 | //------------------------------------------------------------------------------ 54 | 55 | // AccountInfo describes an interchain account, including its address and info 56 | // of the controller and host chains. 57 | message AccountInfo { 58 | ChainInfo controller = 1; 59 | ChainInfo host = 2; 60 | string address = 3; 61 | } 62 | 63 | // ChainInfo describes the IBC connection/port/channel on either the controller 64 | // or host chain. 65 | message ChainInfo { 66 | string client_id = 1; 67 | string connection_id = 2; 68 | string port_id = 3; 69 | string channel_id = 4; 70 | 71 | // TODO: add chain id? 72 | } 73 | -------------------------------------------------------------------------------- /proto/mars/envoy/v1beta1/tx.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.envoy.v1beta1; 3 | 4 | option go_package = "github.com/mars-protocol/hub/x/envoy/types"; 5 | 6 | import "cosmos/base/v1beta1/coin.proto"; 7 | import "cosmos/msg/v1/msg.proto"; 8 | import "cosmos_proto/cosmos.proto"; 9 | import "gogoproto/gogo.proto"; 10 | import "google/protobuf/any.proto"; 11 | 12 | // Msg defines the module's gRPC message service. 13 | service Msg { 14 | option (cosmos.msg.v1.service) = true; 15 | 16 | // RegisterAccount creates a new interchain account on the given connection, 17 | // or if an interchain account already exists but its channel is closed (due 18 | // to a packet having timed out), open a new channel for this account. 19 | rpc RegisterAccount(MsgRegisterAccount) returns (MsgRegisterAccountResponse); 20 | 21 | // SendFunds is a governance operation for sending funds to an interchain 22 | // account via ICS-20. 23 | // 24 | // The envoy module will first attempt to use the balance held in its own 25 | // module account. If the balance is not sufficient, it will attempt to draw 26 | // the difference from the community pool. 27 | rpc SendFunds(MsgSendFunds) returns (MsgSendFundsResponse); 28 | 29 | // SendMessages is a governance operation for sending one or more messages to 30 | // the host chain to be executed by the interchain account. 31 | rpc SendMessages(MsgSendMessages) returns (MsgSendMessagesResponse); 32 | } 33 | 34 | //------------------------------------------------------------------------------ 35 | // RegisterAccount 36 | //------------------------------------------------------------------------------ 37 | 38 | // MsgRegisterAccount is the request type for the Msg/RegisterAccount RPC method. 39 | message MsgRegisterAccount { 40 | option (cosmos.msg.v1.signer) = "sender"; 41 | 42 | // Sender is the account executing this message. 43 | string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; 44 | 45 | // ConnectionId identifies the connection on which the interchain account is 46 | // to be registered. 47 | string connection_id = 2 [(gogoproto.moretags) = "yaml:\"connection_id\""]; 48 | } 49 | 50 | // MsgRegisterAccountResponse is the response type for the Msg/RegisterAccount 51 | // RPC method. 52 | message MsgRegisterAccountResponse {} 53 | 54 | //------------------------------------------------------------------------------ 55 | // SendFunds 56 | //------------------------------------------------------------------------------ 57 | 58 | // MsgSendFunds is the request type for the Msg/SendFunds RPC method. 59 | // 60 | // This message is typically executed via a governance proposal with the gov 61 | // module being the executing authority. 62 | // 63 | // We do not need to specify the recipient address in this message, as it can be 64 | // deduced from the channel id. 65 | message MsgSendFunds { 66 | option (cosmos.msg.v1.signer) = "authority"; 67 | 68 | // Authority is the account executing this message. 69 | // It is typically the x/gov module account. 70 | string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; 71 | 72 | // ChannelId identifies the channel through which the transfer is to be sent. 73 | // 74 | // Unlike other messages of this module which only requires specifying the 75 | // connection id, we have to specify the channel id here, because there can be 76 | // multiple transfer channels associated with the same connection. 77 | string channel_id = 2 [(gogoproto.moretags) = "yaml:\"channel_id\""]; 78 | 79 | // Amount is the coins that are to be sent. 80 | // 81 | // Here we support multiple coins in one proposal. As ICS-20 specs only allow 82 | // one denom per packet, we will have one packet per denom. 83 | repeated cosmos.base.v1beta1.Coin amount = 3 [ 84 | (gogoproto.nullable) = false, 85 | (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" 86 | ]; 87 | } 88 | 89 | // MsgSendFundsResponse is the respones type for the Msg/SendFunds RPC method. 90 | message MsgSendFundsResponse {} 91 | 92 | //------------------------------------------------------------------------------ 93 | // SendMessages 94 | //------------------------------------------------------------------------------ 95 | 96 | // MsgSendMessages is the request type for the Msg/SendMessages RPC method. 97 | // 98 | // This message is typically executed via a governance proposal with the gov 99 | // module being the executing authority. 100 | message MsgSendMessages { 101 | option (cosmos.msg.v1.signer) = "authority"; 102 | 103 | // Authority is the account executing this message. 104 | // It is typically the x/gov module account. 105 | string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; 106 | 107 | // ConnectionId identifies the connection through which the messages are to 108 | // be sent. 109 | string connection_id = 2 [(gogoproto.moretags) = "yaml:\"connection_id\""]; 110 | 111 | // Messages is an array of one or more messages that are to be executed by the 112 | // interchain account. 113 | repeated google.protobuf.Any messages = 3; 114 | } 115 | 116 | // MsgSendMessagesResponse is the response type for the Msg/SendMessages RPC 117 | // method. 118 | message MsgSendMessagesResponse {} 119 | -------------------------------------------------------------------------------- /proto/mars/incentives/v1beta1/genesis.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.incentives.v1beta1; 3 | 4 | import "gogoproto/gogo.proto"; 5 | import "mars/incentives/v1beta1/store.proto"; 6 | 7 | option go_package = "github.com/mars-protocol/hub/x/incentives/types"; 8 | 9 | // GenesisState defines the incentives module's genesis state 10 | message GenesisState { 11 | // NextScheduleId is the id for the next incentives schedule to be created 12 | uint64 next_schedule_id = 1 [(gogoproto.moretags) = "yaml:\"next_schedule_id\""]; 13 | 14 | // Schedules is an array of active incentives schedules 15 | repeated Schedule schedules = 2 [(gogoproto.nullable) = false]; 16 | } 17 | -------------------------------------------------------------------------------- /proto/mars/incentives/v1beta1/query.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.incentives.v1beta1; 3 | 4 | import "cosmos/base/query/v1beta1/pagination.proto"; 5 | import "gogoproto/gogo.proto"; 6 | import "google/api/annotations.proto"; 7 | import "mars/incentives/v1beta1/store.proto"; 8 | 9 | option go_package = "github.com/mars-protocol/hub/x/incentives/types"; 10 | 11 | // Query defines the gRPC querier service for the incentives module 12 | service Query { 13 | // Schedule queries an incentives schedule by identifier 14 | rpc Schedule(QueryScheduleRequest) returns (QueryScheduleResponse) { 15 | option (google.api.http).get = "/mars/incentives/v1beta1/schedule/{id}"; 16 | } 17 | 18 | // Schedules queries all incentives schedules 19 | rpc Schedules(QuerySchedulesRequest) returns (QuerySchedulesResponse) { 20 | option (google.api.http).get = "/mars/incentives/v1beta1/schedules"; 21 | } 22 | } 23 | 24 | // QueryScheduleRequest is the request type for the Query/Schedule RPC method 25 | message QueryScheduleRequest { 26 | option (gogoproto.equal) = false; 27 | option (gogoproto.goproto_getters) = false; 28 | 29 | // ID is the identifier of the incentives schedule to be queried 30 | uint64 id = 1; 31 | } 32 | 33 | // QueryScheduleResponse is the response type for the Query/Schedule RPC method 34 | message QueryScheduleResponse { 35 | // Schedule is the parameters of the incentives schedule 36 | Schedule schedule = 1 [(gogoproto.nullable) = false]; 37 | } 38 | 39 | // QuerySchedulesRequest is the request type for the Query/Schedules RPC method 40 | message QuerySchedulesRequest { 41 | option (gogoproto.equal) = false; 42 | option (gogoproto.goproto_getters) = false; 43 | 44 | // Pagination defines an optional pagination for the request 45 | cosmos.base.query.v1beta1.PageRequest pagination = 1; 46 | } 47 | 48 | // QueryScheduleResponse is the response type for the Query/Schedules RPC method 49 | message QuerySchedulesResponse { 50 | // Schedule is the parameters of the incentives schedule 51 | repeated Schedule schedules = 1 [(gogoproto.nullable) = false]; 52 | 53 | // Pagination defines the pagination in the response 54 | cosmos.base.query.v1beta1.PageResponse pagination = 2; 55 | } 56 | -------------------------------------------------------------------------------- /proto/mars/incentives/v1beta1/store.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.incentives.v1beta1; 3 | 4 | import "cosmos/base/v1beta1/coin.proto"; 5 | import "gogoproto/gogo.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | 8 | option go_package = "github.com/mars-protocol/hub/x/incentives/types"; 9 | 10 | // Schedule defines the parameters of an incentives releasing schedule 11 | message Schedule { 12 | // Id is the identifier of this incentives schedule 13 | uint64 id = 1; 14 | 15 | // StartTime is the UNIX timestamp of which this incentives schedule shall begin 16 | google.protobuf.Timestamp start_time = 2 [ 17 | (gogoproto.stdtime) = true, 18 | (gogoproto.nullable) = false, 19 | (gogoproto.moretags) = "yaml:\"start_time\"" 20 | ]; 21 | 22 | // EndTime is the UNIX timestamp of which this incentives schedule shall finish 23 | google.protobuf.Timestamp end_time = 3 [ 24 | (gogoproto.stdtime) = true, 25 | (gogoproto.nullable) = false, 26 | (gogoproto.moretags) = "yaml:\"end_time\"" 27 | ]; 28 | 29 | // TotalAmount is the total amount of coins that shall be released to stakers 30 | // throughout the span of this incentives schedule 31 | repeated cosmos.base.v1beta1.Coin total_amount = 4 [ 32 | (gogoproto.nullable) = false, 33 | (gogoproto.moretags) = "yaml:\"total_amount\"", 34 | (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" 35 | ]; 36 | 37 | // ReleasedAmount is the amount of coins that have already been released to 38 | // the stakers as part of this incentives schedule 39 | repeated cosmos.base.v1beta1.Coin released_amount = 5 [ 40 | (gogoproto.nullable) = false, 41 | (gogoproto.moretags) = "yaml:\"released_amount\"", 42 | (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /proto/mars/incentives/v1beta1/tx.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.incentives.v1beta1; 3 | 4 | import "cosmos/base/v1beta1/coin.proto"; 5 | import "cosmos/msg/v1/msg.proto"; 6 | import "cosmos_proto/cosmos.proto"; 7 | import "gogoproto/gogo.proto"; 8 | import "google/protobuf/timestamp.proto"; 9 | 10 | option go_package = "github.com/mars-protocol/hub/x/incentives/types"; 11 | 12 | // Msg defines the incentives module's Msg service 13 | service Msg { 14 | option (cosmos.msg.v1.service) = true; 15 | 16 | // CreateSchedule is a governance operation for creating a new incentives 17 | // schedule. 18 | rpc CreateSchedule(MsgCreateSchedule) returns (MsgCreateScheduleResponse); 19 | 20 | // TerminateSchedule is a governance operation for terminating one or more 21 | // existing incentives schedules. 22 | rpc TerminateSchedules(MsgTerminateSchedules) returns (MsgTerminateSchedulesResponse); 23 | } 24 | 25 | // MsgCreateSchedule defines the message for creating a new incentives schedule. 26 | // 27 | // This message is typically executed via a governance proposal with the gov 28 | // module being the executing authority. 29 | message MsgCreateSchedule { 30 | option (cosmos.msg.v1.signer) = "authority"; 31 | 32 | // Authority is the account executing the safety fund spend. 33 | // It should be the gov module account. 34 | string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; 35 | 36 | // StartTime is the timestamp at which this incentives schedule shall begin. 37 | google.protobuf.Timestamp start_time = 2 [ 38 | (gogoproto.stdtime) = true, 39 | (gogoproto.nullable) = false, 40 | (gogoproto.moretags) = "yaml:\"start_time\"" 41 | ]; 42 | 43 | // EndTime is the timestamp at which this incentives schedule shall finish. 44 | google.protobuf.Timestamp end_time = 3 [ 45 | (gogoproto.stdtime) = true, 46 | (gogoproto.nullable) = false, 47 | (gogoproto.moretags) = "yaml:\"end_time\"" 48 | ]; 49 | 50 | // Amount is the total amount of coins that shall be released to stakers 51 | // throughout the span of this incentives schedule. 52 | repeated cosmos.base.v1beta1.Coin amount = 4 [ 53 | (gogoproto.nullable) = false, 54 | (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" 55 | ]; 56 | } 57 | 58 | // MsgCreateScheduleResponse defines the response to executing a 59 | // MsgCreateSchedule message. 60 | message MsgCreateScheduleResponse {} 61 | 62 | // MsgTerminateSchedules defines the message for terminating one or more 63 | // existing incentives schedules. 64 | // 65 | // This message is typically executed via a governance proposal with the gov 66 | // module being the executing authority. 67 | message MsgTerminateSchedules { 68 | option (cosmos.msg.v1.signer) = "authority"; 69 | 70 | // Authority is the account executing the safety fund spend. 71 | // It should be the gov module account. 72 | string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; 73 | 74 | // Ids is the array of identifiers of the incentives schedules which are to be 75 | // terminated. 76 | repeated uint64 ids = 2; 77 | } 78 | 79 | // MsgTerminateSchedulesResponse defines the response to executing a 80 | // MsgTerminateSchedules message. 81 | message MsgTerminateSchedulesResponse { 82 | // RefundedAmount is the unreleased incentives that were refunded to the 83 | // community pool. 84 | repeated cosmos.base.v1beta1.Coin refunded_amount = 5 [ 85 | (gogoproto.nullable) = false, 86 | (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" 87 | ]; 88 | } 89 | -------------------------------------------------------------------------------- /proto/mars/safety/v1beta1/genesis.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.safety.v1beta1; 3 | 4 | option go_package = "github.com/mars-protocol/hub/x/safety/types"; 5 | 6 | // GenesisState defines the safety module's genesis state 7 | message GenesisState {} 8 | -------------------------------------------------------------------------------- /proto/mars/safety/v1beta1/query.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.safety.v1beta1; 3 | 4 | import "cosmos/base/v1beta1/coin.proto"; 5 | import "gogoproto/gogo.proto"; 6 | import "google/api/annotations.proto"; 7 | 8 | option go_package = "github.com/mars-protocol/hub/x/safety/types"; 9 | 10 | // Query defines the gRPC querier service for the safety fund module 11 | service Query { 12 | // Balances queries coins available in the safety fund 13 | rpc Balances(QueryBalancesRequest) returns (QueryBalancesResponse) { 14 | option (google.api.http).get = "/mars/safety/v1beta1/balances"; 15 | } 16 | } 17 | 18 | // QueBalancesRequest is the request type of the QuerBalancesRPC method 19 | message QueryBalancesRequest {} 20 | 21 | // QueBalancesResponse is the response type of the QuerBalancesRPC method 22 | message QueryBalancesResponse { 23 | // Balances is the coins available in the safety fund 24 | repeated cosmos.base.v1beta1.Coin balances = 1 [ 25 | (gogoproto.nullable) = false, 26 | (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /proto/mars/safety/v1beta1/tx.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package mars.safety.v1beta1; 3 | 4 | import "cosmos/base/v1beta1/coin.proto"; 5 | import "cosmos/msg/v1/msg.proto"; 6 | import "cosmos_proto/cosmos.proto"; 7 | import "gogoproto/gogo.proto"; 8 | 9 | option go_package = "github.com/mars-protocol/hub/x/safety/types"; 10 | 11 | // Msg defines the safety module's Msg service 12 | service Msg { 13 | option (cosmos.msg.v1.service) = true; 14 | 15 | // SafetyFundSpend is a governance operation for sending tokens from the 16 | // safety fund module account to the designated recipient. 17 | // 18 | // NOTE: For now, this is just a copy of the distribution module's 19 | // `CommunityFundSpend` method. The recipient is expected to be a multisig 20 | // consisting of trusted community members who are respondible for using the 21 | // funds appropriately to cover any bad debt. 22 | // 23 | // In the long term, the goal is that the module 24 | // is able to detect bad debts incurred in the outposts via interchain query, 25 | // and automatically dispense the appropriate amount of funds, without having 26 | // to go through the governance process. 27 | rpc SafetyFundSpend(MsgSafetyFundSpend) returns (MsgSafetyFundSpendResponse); 28 | } 29 | 30 | // MsgSafetyFundSpend defines the message for sending tokens from the safety 31 | // fund to a designated recipient. 32 | // 33 | // This message is typically executed via a governance proposal with the gov 34 | // module being the executing authority. 35 | message MsgSafetyFundSpend { 36 | option (cosmos.msg.v1.signer) = "authority"; 37 | 38 | // Authority is the account executing the safety fund spend. 39 | // It should be the gov module account. 40 | string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; 41 | 42 | // Recipient is the account to receive the funds 43 | string recipient = 2; 44 | 45 | // Amount is the coins that are to be released from the safety funds 46 | repeated cosmos.base.v1beta1.Coin amount = 3 [ 47 | (gogoproto.nullable) = false, 48 | (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" 49 | ]; 50 | } 51 | 52 | // MsgSafetyFundSpendResponse defines the response to executing a 53 | // MsgSafetyFundSpend message. 54 | message MsgSafetyFundSpendResponse {} 55 | -------------------------------------------------------------------------------- /scripts/protoc-swagger-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This script is intended to be run inside the osmolabs/osmo-proto-gen:v0.8 4 | # docker container: https://hub.docker.com/r/osmolabs/osmo-proto-gen 5 | 6 | set -eo pipefail 7 | 8 | # The directory where the final output are to be stored 9 | docs_dir="./docs" 10 | 11 | # The directory where temporary swagger files are to be stored before they are 12 | # combined. Will be deleted in the end 13 | tmp_dir="./tmp-swagger-gen" 14 | if [ -d $tmp_dir ]; then 15 | rm -rf $tmp_dir 16 | fi 17 | mkdir -p $tmp_dir 18 | 19 | # Third-party proto dependencies 20 | # sh doesn't support arrays like bash does, but it does support comma-separated 21 | # strings: https://unix.stackexchange.com/a/323535 22 | deps="github.com/cosmos/cosmos-sdk" 23 | deps="$deps github.com/cosmos/ibc-go/v6" 24 | deps="$deps github.com/CosmWasm/wasmd" 25 | 26 | # Download dependencies in go.mod 27 | # Necessary for the `go list` commands in the next step to work 28 | echo "Downloading dependencies..." 29 | for dep in $deps; do 30 | echo $dep 31 | go mod download $dep 32 | done 33 | 34 | # Directories that contain protobuf files that are to be transpiled into swagger 35 | # These include Mars modules and third party modules and services 36 | dirs="./proto" 37 | for dep in $deps; do 38 | dep_dir=$(go list -f '{{ .Dir }}' -m $dep) 39 | dirs="$dirs ${dep_dir}/proto" 40 | done 41 | proto_dirs=$(find $dirs -path -prune -o -name '*.proto' -print0 | xargs -0 -n1 dirname | sort | uniq) 42 | 43 | # Generate swagger files for `query.proto` and `service.proto` 44 | for dir in $proto_dirs; do 45 | for file in $(find "${dir}" -maxdepth 1 \( -name 'query.proto' -o -name 'service.proto' \)); do 46 | echo $file 47 | buf generate --template ./proto/buf.gen.swagger.yaml $file 48 | done 49 | done 50 | 51 | # Combine swagger files 52 | # Uses nodejs package `swagger-combine`. 53 | # All the individual swagger files need to be configured in `config.json` for merging 54 | echo "Combining swagger files..." 55 | swagger-combine ${docs_dir}/config.json \ 56 | -o ${docs_dir}/swagger.yml \ 57 | -f yaml \ 58 | --continueOnConflictingPaths true \ 59 | --includeDefinitions true 60 | 61 | # Clean swagger files 62 | rm -rf $tmp_dir 63 | -------------------------------------------------------------------------------- /scripts/protocgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This script is intended to be run inside the osmolabs/osmo-proto-gen:v0.8 4 | # docker container: https://hub.docker.com/r/osmolabs/osmo-proto-gen 5 | 6 | set -eo pipefail 7 | 8 | proto_dirs=$(find ./proto -path -prune -o -name '*.proto' -print0 | xargs -0 -n1 dirname | sort | uniq) 9 | for dir in $proto_dirs; do 10 | for file in $(find "${dir}" -maxdepth 1 -name '*.proto'); do 11 | if grep "option go_package" $file &> /dev/null ; then 12 | echo $file 13 | buf generate --template ./proto/buf.gen.go.yaml $file 14 | fi 15 | done 16 | done 17 | 18 | # move proto files to the right places 19 | if [ -d "./github.com/mars-protocol/hub" ]; then 20 | cp -r github.com/mars-protocol/hub/* ./ 21 | rm -rf github.com 22 | fi 23 | 24 | go mod tidy 25 | -------------------------------------------------------------------------------- /tests/e2e/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # E2E tests for Envoy module 4 | 5 | This directory contains end-to-end tests for the Envoy module. 6 | 7 | The tests are written in [Typescript][1] to be executable in [Deno runtime][2]. 8 | 9 | ## Executing 10 | 11 | This end-to-end test is set up as a [GitHub Actions workflow][3]. 12 | 13 | There are two ways to run this _workflow_ on code changes. 14 | 15 | ### CI/CD 16 | 17 | Push commits to any GitHub repository will trigger this workflow on GitHub actions. 18 | 19 | ### Locally 20 | 21 | [`act`][4] can be used to test this workflow on a local machine. 22 | 23 | ```sh 24 | act -j envoy 25 | ``` 26 | 27 | ### Manually 28 | 29 | It is also possible to run this test manually, outside of the Deno runtime, directing interacting with the node and relayer software. See [`MANUAL.md`][5] for instructions. 30 | 31 | ## Description 32 | 33 | This script spawns up one `marsd` and one `wasmd` with a ibc-transafer channel opened between them and `hermes` relayer, relaying ibc-packets between them. 34 | 35 | Then the script performs assertions for the transaction and query API of the Envoy module. 36 | 37 | - Account Registration: ICA registration of Envoy module account on the counterparty blockchain. It also asserts the query APIs. 38 | - Send Funds: Send funds from the Envoy module account to its ICA on the counterparty blockchain. 39 | - Submit ICA Transactions: Submit transactions from the Envoy module account to its ICA on the counterparty blockchain. 40 | 41 | _Send Funds_ and _Submit ICA Transactions_ can not be submitted directly as a signed transaction. So they are executed through accepted governance proposals. 42 | 43 | 44 | Deno is preferred because, unlike other programming languages, Deno [resolves dependencies][6] on the fly and fetches them if needed. This declutters project space and avoids extra dependency installation steps. 45 | 46 | 47 | [1]: https://www.typescriptlang.org 48 | [2]: https://deno.land 49 | [3]: ../../.github/workflows/e2e.yml 50 | [4]: https://github.com/nektos/act 51 | [5]: ./MANUAL.md 52 | [6]: https://deno.land/manual/examples/manage_dependencies 53 | -------------------------------------------------------------------------------- /tests/e2e/configs.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mars-protocol/hub/717902c1a1365c65f20920b71d238a3dd4c2d789/tests/e2e/configs.tar.gz -------------------------------------------------------------------------------- /tests/e2e/send_funds.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": [ 3 | { 4 | "@type": "/mars.envoy.v1beta1.MsgSendFunds", 5 | "authority": "mars10d07y265gmmuvt4z0w9aw880jnsr700j8l2urg", 6 | "channel_id": "channel-0", 7 | "amount": [ 8 | { 9 | "denom": "uastro", 10 | "amount": "42069" 11 | }, 12 | { 13 | "denom": "umars", 14 | "amount": "69420" 15 | } 16 | ] 17 | } 18 | ], 19 | "metadata": "{\"title\":\"Send funds\",\"summary\":\"Send 42069 uastro and 69420 umars to the governance-controlled interchain account by channel-0.\"}", 20 | "deposit": "10000000umars" 21 | } 22 | -------------------------------------------------------------------------------- /tests/e2e/send_messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": [ 3 | { 4 | "@type": "/mars.envoy.v1beta1.MsgSendMessages", 5 | "authority": "mars10d07y265gmmuvt4z0w9aw880jnsr700j8l2urg", 6 | "connection_id": "connection-0", 7 | "messages": [ 8 | { 9 | "@type": "/cosmwasm.wasm.v1.MsgExecuteContract", 10 | "sender": "wasm1jwdap5t78w4na2vmdcaszysqcqkhy0suh4nsg0lce70j7g50f2uqkrnyz4", 11 | "contract": "wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d", 12 | "msg": { 13 | "update_ownership": "accept_ownership" 14 | } 15 | } 16 | ] 17 | } 18 | ], 19 | "metadata": "{\"title\":\"Accept contract ownership\",\"summary\":\"Instruct the governance-controlled interchain account on connection-0 to accept the contract ownership.\"}", 20 | "deposit": "10000000umars" 21 | } 22 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | // This file uses the recommended method for tracking developer tools in a Go 5 | // module. 6 | // 7 | // REF: https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 8 | package tools 9 | 10 | import _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 11 | -------------------------------------------------------------------------------- /utils/cmp.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Contains checks whether an array of strings contains an element. 4 | func Contains(s []string, str string) bool { 5 | for _, v := range s { 6 | if v == str { 7 | return true 8 | } 9 | } 10 | 11 | return false 12 | } 13 | -------------------------------------------------------------------------------- /utils/math.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import sdk "github.com/cosmos/cosmos-sdk/types" 4 | 5 | // SaturateSub subtracts a set of coins from another. If the amount goes below 6 | // zero, it's set to zero. 7 | // 8 | // Example: 9 | // {2A, 3B, 4C} - {1A, 5B, 3D} = {1A, 4C} 10 | func SaturateSub(coinsA sdk.Coins, coinsB sdk.Coins) sdk.Coins { 11 | return coinsA.Sub(coinsA.Min(coinsB)...) 12 | } 13 | -------------------------------------------------------------------------------- /utils/strconv.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // UintArrayToString joins an array of uint64 numbers into a string with the 9 | // given delimiter. 10 | // 11 | // Forked from https://stackoverflow.com/questions/37532255/one-liner-to-transform-int-into-string 12 | func UintArrayToString(uints []uint64, delim string) string { 13 | return strings.Trim(strings.ReplaceAll(fmt.Sprint(uints), " ", delim), "[]") 14 | } 15 | -------------------------------------------------------------------------------- /x/envoy/README.md: -------------------------------------------------------------------------------- 1 | # Envoy 2 | 3 | The Envoy module acts as the owner of interchain accounts (ICAs) on other chains. It provides governance-gated message types for creating, sending funds, or messages to these accounts. The ICAs are to be set as owners/admins of Mars [Outpost][1] and [Rover][2] contracts on those chains, allowing Mars Hub governance to remotely govern those contracts. 4 | 5 | ## Acknowledgements 6 | 7 | - This module is adapted from the `intertx` module found in the [interchain-accounts-demo][3] repo. 8 | - We thank Informal Systems for reviewing and contributing open source code to this module. 9 | 10 | [1]: https://github.com/mars-protocol/outposts 11 | [2]: https://github.com/mars-protocol/rover 12 | [3]: https://github.com/cosmos/interchain-accounts-demo 13 | -------------------------------------------------------------------------------- /x/envoy/client/cli/query.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/cosmos/cosmos-sdk/client" 7 | "github.com/cosmos/cosmos-sdk/client/flags" 8 | 9 | "github.com/mars-protocol/hub/v2/x/envoy/types" 10 | ) 11 | 12 | // GetQueryCmd returns the parent command for all envoy module query commands. 13 | func GetQueryCmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: types.ModuleName, 16 | Short: "Querying commands for the envoy module", 17 | DisableFlagParsing: true, 18 | SuggestionsMinimumDistance: 2, 19 | RunE: client.ValidateCmd, 20 | } 21 | 22 | cmd.AddCommand( 23 | getAccountCmd(), 24 | getAccountsCmd(), 25 | ) 26 | 27 | return cmd 28 | } 29 | 30 | func getAccountCmd() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "account", 33 | Short: "Query the interchain account associated with a connection id", 34 | Args: cobra.ExactArgs(1), 35 | RunE: func(cmd *cobra.Command, args []string) error { 36 | clientCtx, err := client.GetClientQueryContext(cmd) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | queryClient := types.NewQueryClient(clientCtx) 42 | 43 | res, err := queryClient.Account(cmd.Context(), &types.QueryAccountRequest{ConnectionId: args[0]}) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | return clientCtx.PrintProto(res) 49 | }, 50 | } 51 | 52 | flags.AddQueryFlagsToCmd(cmd) 53 | 54 | return cmd 55 | } 56 | 57 | func getAccountsCmd() *cobra.Command { 58 | cmd := &cobra.Command{ 59 | Use: "accounts", 60 | Short: "Query all interchain account owned by the envoy module", 61 | Args: cobra.NoArgs, 62 | RunE: func(cmd *cobra.Command, args []string) error { 63 | clientCtx, err := client.GetClientQueryContext(cmd) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | queryClient := types.NewQueryClient(clientCtx) 69 | 70 | res, err := queryClient.Accounts(cmd.Context(), &types.QueryAccountsRequest{}) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return clientCtx.PrintProto(res) 76 | }, 77 | } 78 | 79 | flags.AddQueryFlagsToCmd(cmd) 80 | 81 | return cmd 82 | } 83 | -------------------------------------------------------------------------------- /x/envoy/client/cli/tx.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/cosmos/cosmos-sdk/client" 7 | "github.com/cosmos/cosmos-sdk/client/flags" 8 | "github.com/cosmos/cosmos-sdk/client/tx" 9 | 10 | "github.com/mars-protocol/hub/v2/x/envoy/types" 11 | ) 12 | 13 | // GetTxCmd returns the parent command for all envoy module tx commands. 14 | func GetTxCmd() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: types.ModuleName, 17 | Short: "Envoy transaction subcommands", 18 | RunE: client.ValidateCmd, 19 | } 20 | 21 | cmd.AddCommand( 22 | getRegisterAccountCmd(), 23 | ) 24 | 25 | return cmd 26 | } 27 | 28 | func getRegisterAccountCmd() *cobra.Command { 29 | cmd := &cobra.Command{ 30 | Use: "register-account [connection-id]", 31 | Short: "Register module-owned interchain account on the given connection", 32 | Args: cobra.ExactArgs(1), 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | clientCtx, err := client.GetClientTxContext(cmd) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | msg := &types.MsgRegisterAccount{ 40 | Sender: clientCtx.GetFromAddress().String(), 41 | ConnectionId: args[0], 42 | } 43 | 44 | return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) 45 | }, 46 | } 47 | 48 | flags.AddTxFlagsToCmd(cmd) 49 | 50 | return cmd 51 | } 52 | -------------------------------------------------------------------------------- /x/envoy/keeper/genesis.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | 6 | "github.com/mars-protocol/hub/v2/x/envoy/types" 7 | ) 8 | 9 | // InitGenesis initializes the envoy module's storage according to the 10 | // provided genesis state. 11 | // 12 | // NOTE: we call `GetModuleAccount` instead of `SetModuleAccount` because the 13 | // "get" function automatically sets the module account if it doesn't exist. 14 | func (k Keeper) InitGenesis(ctx sdk.Context, _ *types.GenesisState) { 15 | // set module account 16 | k.accountKeeper.GetModuleAccount(ctx, types.ModuleName) 17 | } 18 | 19 | // ExportGenesis returns a genesis state for a given context and keeper. 20 | func (k Keeper) ExportGenesis(_ sdk.Context) *types.GenesisState { 21 | return &types.GenesisState{} 22 | } 23 | -------------------------------------------------------------------------------- /x/envoy/keeper/genesis_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 9 | 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 12 | 13 | marsapp "github.com/mars-protocol/hub/v2/app" 14 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 15 | "github.com/mars-protocol/hub/v2/x/envoy/types" 16 | ) 17 | 18 | var mockGenesisState = &types.GenesisState{} 19 | 20 | func setupGenesisTest() (ctx sdk.Context, app *marsapp.MarsApp) { 21 | app = marsapptesting.MakeSimpleMockApp() 22 | ctx = app.BaseApp.NewContext(false, tmproto.Header{}) 23 | 24 | app.EnvoyKeeper.InitGenesis(ctx, mockGenesisState) 25 | 26 | return ctx, app 27 | } 28 | 29 | func TestInitGenesis(t *testing.T) { 30 | ctx, app := setupGenesisTest() 31 | 32 | // make sure that the module account is registered at the auth module 33 | acc := app.AccountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName)) 34 | require.NotNil(t, acc) 35 | } 36 | 37 | func TestExportGenesis(t *testing.T) { 38 | ctx, app := setupGenesisTest() 39 | 40 | exported := app.EnvoyKeeper.ExportGenesis(ctx) 41 | require.Equal(t, exported, mockGenesisState) 42 | } 43 | -------------------------------------------------------------------------------- /x/envoy/keeper/keeper.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tendermint/tendermint/libs/log" 7 | 8 | "github.com/cosmos/cosmos-sdk/baseapp" 9 | "github.com/cosmos/cosmos-sdk/codec" 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | 12 | authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" 13 | bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" 14 | distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" 15 | 16 | icacontrollerkeeper "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/controller/keeper" 17 | icatypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/types" 18 | ibcchannelkeeper "github.com/cosmos/ibc-go/v6/modules/core/04-channel/keeper" 19 | 20 | "github.com/mars-protocol/hub/v2/x/envoy/types" 21 | ) 22 | 23 | // Keeper is the envoy module's keeper. 24 | type Keeper struct { 25 | cdc codec.Codec 26 | 27 | accountKeeper authkeeper.AccountKeeper 28 | bankKeeper bankkeeper.Keeper 29 | distrKeeper distrkeeper.Keeper 30 | channelKeeper ibcchannelkeeper.Keeper 31 | icaControllerKeeper icacontrollerkeeper.Keeper 32 | 33 | // The baseapp's message service router. 34 | // We use this to dispatch messages upon successful governance proposals. 35 | router *baseapp.MsgServiceRouter 36 | 37 | // Accounts who can execute envoy module messages. 38 | // Typically, this includes the gov module or other module accounts. 39 | authorities []string 40 | } 41 | 42 | // NewKeeper creates a new envoy module keeper. 43 | func NewKeeper( 44 | cdc codec.Codec, accountKeeper authkeeper.AccountKeeper, 45 | bankKeeper bankkeeper.Keeper, distrKeeper distrkeeper.Keeper, 46 | channelKeeper ibcchannelkeeper.Keeper, icaControllerKeeper icacontrollerkeeper.Keeper, 47 | router *baseapp.MsgServiceRouter, authorities []string, 48 | ) Keeper { 49 | // ensure envoy module account is set 50 | if addr := accountKeeper.GetModuleAddress(types.ModuleName); addr == nil { 51 | panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) 52 | } 53 | 54 | // make sure the codec is ProtoCodec 55 | // ICA controller only accepts ProtoCodec for encoding messages: 56 | // https://github.com/cosmos/ibc-go/blob/v6.1.0/modules/apps/27-interchain-accounts/types/codec.go#L32 57 | if _, ok := cdc.(*codec.ProtoCodec); !ok { 58 | panic(fmt.Sprintf("%s module keeper only accepts ProtoCodec; found %T", types.ModuleName, cdc)) 59 | } 60 | 61 | return Keeper{ 62 | cdc: cdc, 63 | accountKeeper: accountKeeper, 64 | bankKeeper: bankKeeper, 65 | distrKeeper: distrKeeper, 66 | channelKeeper: channelKeeper, 67 | icaControllerKeeper: icaControllerKeeper, 68 | router: router, 69 | authorities: authorities, 70 | } 71 | } 72 | 73 | // Logger returns a module-specific logger. 74 | func (k Keeper) Logger(ctx sdk.Context) log.Logger { 75 | return ctx.Logger().With("module", "x/"+types.ModuleName) 76 | } 77 | 78 | // GetModuleAddress returns the envoy module account's address. 79 | func (k Keeper) GetModuleAddress() sdk.AccAddress { 80 | return k.accountKeeper.GetModuleAddress(types.ModuleName) 81 | } 82 | 83 | // GetOwnerAndPortID is a convenience method that returns the envoy module 84 | // account, which acts as the owner of interchain accounts, as well as the ICA 85 | // controller port ID associated with it. 86 | func (k Keeper) GetOwnerAndPortID() (sdk.AccAddress, string, error) { 87 | owner := k.GetModuleAddress() 88 | portID, err := icatypes.NewControllerPortID(owner.String()) 89 | return owner, portID, err 90 | } 91 | 92 | // executeMsg executes message using the baseapp's message router. 93 | func (k Keeper) executeMsg(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { 94 | handler := k.router.Handler(msg) 95 | return handler(ctx, msg) 96 | } 97 | -------------------------------------------------------------------------------- /x/envoy/keeper/query_server_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | icatypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/types" 5 | ibctesting "github.com/cosmos/ibc-go/v6/testing" 6 | 7 | "github.com/mars-protocol/hub/v2/x/envoy/keeper" 8 | "github.com/mars-protocol/hub/v2/x/envoy/types" 9 | ) 10 | 11 | func (suite *KeeperTestSuite) TestQueryAccount() { 12 | suite.SetupTest() 13 | 14 | registerInterchainAccount(suite.path1, owner.String()) 15 | 16 | ctx := suite.hub.GetContext() 17 | app := getMarsApp(suite.hub) 18 | queryServer := keeper.NewQueryServerImpl(app.EnvoyKeeper) 19 | 20 | // query account at outpost 1 - should succeed 21 | res, err := queryServer.Account(ctx, &types.QueryAccountRequest{ 22 | ConnectionId: suite.path1.EndpointA.ConnectionID, 23 | }) 24 | suite.Require().NoError(err) 25 | 26 | // set address as empty since we don't care about its particular value 27 | res.Account.Address = "" 28 | suite.Require().Equal(composeAccountInfoFromPath(suite.path1), res.Account) 29 | 30 | // query account at outpost 2 - should fail 31 | res, err = queryServer.Account(ctx, &types.QueryAccountRequest{ 32 | ConnectionId: suite.path2.EndpointA.ConnectionID, 33 | }) 34 | suite.Require().Error(err) 35 | suite.Require().Nil(res) 36 | } 37 | 38 | func (suite *KeeperTestSuite) TestQueryAccounts() { 39 | suite.SetupTest() 40 | 41 | registerInterchainAccount(suite.path1, owner.String()) 42 | registerInterchainAccount(suite.path2, owner.String()) 43 | 44 | ctx := suite.hub.GetContext() 45 | app := getMarsApp(suite.hub) 46 | queryServer := keeper.NewQueryServerImpl(app.EnvoyKeeper) 47 | 48 | res, err := queryServer.Accounts(ctx, &types.QueryAccountsRequest{}) 49 | suite.Require().NoError(err) 50 | 51 | res.Accounts[0].Address = "" 52 | suite.Require().Equal(composeAccountInfoFromPath(suite.path1), res.Accounts[0]) 53 | 54 | res.Accounts[1].Address = "" 55 | suite.Require().Equal(composeAccountInfoFromPath(suite.path2), res.Accounts[1]) 56 | } 57 | 58 | func composeAccountInfoFromPath(path *ibctesting.Path) *types.AccountInfo { 59 | return &types.AccountInfo{ 60 | Controller: &types.ChainInfo{ 61 | ClientId: path.EndpointA.ClientID, 62 | ConnectionId: path.EndpointA.ConnectionID, 63 | PortId: portID, 64 | ChannelId: path.EndpointA.ChannelID, 65 | }, 66 | Host: &types.ChainInfo{ 67 | ClientId: path.EndpointB.ClientID, 68 | ConnectionId: path.EndpointB.ConnectionID, 69 | PortId: icatypes.HostPortID, 70 | ChannelId: path.EndpointB.ChannelID, 71 | }, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /x/envoy/module.go: -------------------------------------------------------------------------------- 1 | package envoy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 10 | "github.com/spf13/cobra" 11 | 12 | abci "github.com/tendermint/tendermint/abci/types" 13 | 14 | "github.com/cosmos/cosmos-sdk/client" 15 | "github.com/cosmos/cosmos-sdk/codec" 16 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 17 | sdk "github.com/cosmos/cosmos-sdk/types" 18 | "github.com/cosmos/cosmos-sdk/types/module" 19 | 20 | "github.com/mars-protocol/hub/v2/x/envoy/client/cli" 21 | "github.com/mars-protocol/hub/v2/x/envoy/keeper" 22 | "github.com/mars-protocol/hub/v2/x/envoy/types" 23 | ) 24 | 25 | var ( 26 | _ module.AppModule = AppModule{} 27 | _ module.AppModuleBasic = AppModuleBasic{} 28 | ) 29 | 30 | //------------------------------------------------------------------------------ 31 | // AppModuleBasic 32 | //------------------------------------------------------------------------------ 33 | 34 | // AppModuleBasic defines the basic application module used by the module 35 | type AppModuleBasic struct{} 36 | 37 | func (AppModuleBasic) Name() string { 38 | return types.ModuleName 39 | } 40 | 41 | func (AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) { 42 | types.RegisterInterfaces(registry) 43 | } 44 | 45 | func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { 46 | return cdc.MustMarshalJSON(types.DefaultGenesisState()) 47 | } 48 | 49 | func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { 50 | var gs types.GenesisState 51 | if err := cdc.UnmarshalJSON(bz, &gs); err != nil { 52 | return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) 53 | } 54 | 55 | return gs.Validate() 56 | } 57 | 58 | func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { 59 | if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil { 60 | panic(err) 61 | } 62 | } 63 | 64 | func (AppModuleBasic) GetTxCmd() *cobra.Command { 65 | return cli.GetTxCmd() 66 | } 67 | 68 | func (AppModuleBasic) GetQueryCmd() *cobra.Command { 69 | return cli.GetQueryCmd() 70 | } 71 | 72 | //------------------------------------------------------------------------------ 73 | // AppModule 74 | //------------------------------------------------------------------------------ 75 | 76 | // AppModule implements an application module for the envoy module 77 | type AppModule struct { 78 | AppModuleBasic 79 | 80 | keeper keeper.Keeper 81 | } 82 | 83 | // NewAppModule creates a new AppModule object 84 | func NewAppModule(keeper keeper.Keeper) AppModule { 85 | return AppModule{AppModuleBasic{}, keeper} 86 | } 87 | 88 | func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) { 89 | } 90 | 91 | func (am AppModule) RegisterServices(cfg module.Configurator) { 92 | types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) 93 | types.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServerImpl(am.keeper)) 94 | } 95 | 96 | func (AppModule) ConsensusVersion() uint64 { 97 | return 1 98 | } 99 | 100 | func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) { 101 | } 102 | 103 | func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { 104 | return []abci.ValidatorUpdate{} 105 | } 106 | 107 | func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { 108 | var genesisState types.GenesisState 109 | cdc.MustUnmarshalJSON(data, &genesisState) 110 | 111 | am.keeper.InitGenesis(ctx, &genesisState) 112 | 113 | return []abci.ValidatorUpdate{} 114 | } 115 | 116 | func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { 117 | gs := am.keeper.ExportGenesis(ctx) 118 | return cdc.MustMarshalJSON(gs) 119 | } 120 | 121 | //------------------------------------------------------------------------------ 122 | // Migration 123 | //------------------------------------------------------------------------------ 124 | 125 | // InitModule is similar to InitGenesis, but used during chain upgrades. 126 | func (am AppModule) InitModule(ctx sdk.Context) { 127 | am.keeper.InitGenesis(ctx, &types.GenesisState{}) 128 | } 129 | 130 | //------------------------------------------------------------------------------ 131 | // Deprecated stuff 132 | //------------------------------------------------------------------------------ 133 | 134 | // deprecated 135 | func (AppModuleBasic) RegisterLegacyAminoCodec(_ *codec.LegacyAmino) { 136 | } 137 | 138 | // deprecated 139 | func (AppModuleBasic) RegisterRESTRoutes(_ client.Context, _ *mux.Router) { 140 | } 141 | 142 | // deprecated 143 | func (AppModule) Route() sdk.Route { 144 | return sdk.Route{} 145 | } 146 | 147 | // deprecated 148 | func (AppModule) QuerierRoute() string { 149 | return types.QuerierRoute 150 | } 151 | 152 | // deprecated 153 | func (AppModule) LegacyQuerierHandler(*codec.LegacyAmino) sdk.Querier { 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /x/envoy/spec/README.md: -------------------------------------------------------------------------------- 1 | # TLA+ Specification for Mars Hub's Envoy module 2 | 3 | Requires [Apalache model checker](https://apalache.informal.systems). 4 | 5 | ## Invariants 6 | 7 | | ID | Invariant | Description | 8 | | -------------- | ----------------- | ------------------------------------------------------------------ | 9 | | `invariant-CS` | `ConstantSupply` | Token supply on the chain never changes. | 10 | | `invariant-PB` | `PositiveBalance` | Balances are always non-negative. | 11 | | `invariant-VA` | `ValidAuthority` | SendFunds and SendMessages have the correct authority. | 12 | | `invariant-IE` | `ICAExists` | SendFunds and SendMessages are submitted to an existing ICAccount. | 13 | | `invariant-PL` | `NoPacketLoss` | IBC packets of SendFunds and SendMessages are in IBC queue. | 14 | 15 | `InvAll` is a conjunction of all of them. So to check all of them together, 16 | 17 | ```sh 18 | apalache-mc check --inv=InvAll envoy.tla 19 | ``` 20 | 21 | ## Examples 22 | 23 | | ID | Property | Invariant | Description | 24 | | ------------ | ------------ | -------------- | ----------------------------------------------------------------- | 25 | | `example-AS` | `AllSuccess` | `ExAllSuccess` | Example trace has minimum 5 states and all the actions succeeded. | 26 | 27 | ### Views 28 | 29 | | ID | Operator | Description | 30 | | --------- | ------------ | ---------------------------------------- | 31 | | `view-AT` | `ActionType` | Projects a state to action message type. | 32 | 33 | To generate an example satisfying `AllSuccess`, 34 | 35 | ```sh 36 | apalache-mc check --inv=ExAllSuccess --max-error=3 --view=ActionType --out-dir=runs envoy.tla 37 | ``` 38 | 39 | The counter-examples would be present at `./runs/envoy.tla//violation*.tla`. 40 | -------------------------------------------------------------------------------- /x/envoy/types/codec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | "github.com/cosmos/cosmos-sdk/types/msgservice" 7 | ) 8 | 9 | func RegisterInterfaces(registry codectypes.InterfaceRegistry) { 10 | registry.RegisterImplementations( 11 | (*sdk.Msg)(nil), 12 | &MsgRegisterAccount{}, 13 | &MsgSendFunds{}, 14 | &MsgSendMessages{}, 15 | ) 16 | 17 | msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) 18 | } 19 | -------------------------------------------------------------------------------- /x/envoy/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "cosmossdk.io/errors" 4 | 5 | var ( 6 | ErrInvalidProposalAmount = errors.Register(ModuleName, 2, "invalid envoy module proposal amount") 7 | ErrInvalidProposalAuthority = errors.Register(ModuleName, 3, "invalid envoy module proposal authority") 8 | ErrInvalidProposalMsg = errors.Register(ModuleName, 4, "invalid envoy module proposal messages") 9 | ErrMultihopUnsupported = errors.Register(ModuleName, 5, "multihop channels are not supported") 10 | ErrUnauthorized = errors.Register(ModuleName, 6, "unauthorized") 11 | ) 12 | -------------------------------------------------------------------------------- /x/envoy/types/genesis.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // DefaultGenesisState returns the module's default genesis state. 4 | func DefaultGenesisState() *GenesisState { 5 | return &GenesisState{} 6 | } 7 | 8 | // Validate validates the given instance of the module's genesis state. 9 | func (gs GenesisState) Validate() error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /x/envoy/types/keys.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | // ModuleName is the module's name 5 | ModuleName = "envoy" 6 | 7 | // StoreKey is the module's store key 8 | StoreKey = ModuleName 9 | 10 | // RouterKey is the module's message route 11 | RouterKey = ModuleName 12 | 13 | // QuerierRoute is the module's querier route 14 | QuerierRoute = ModuleName 15 | ) 16 | -------------------------------------------------------------------------------- /x/envoy/types/tx.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | sdktx "github.com/cosmos/cosmos-sdk/types/tx" 7 | ) 8 | 9 | var ( 10 | _ sdk.Msg = &MsgRegisterAccount{} 11 | _ sdk.Msg = &MsgSendFunds{} 12 | _ sdk.Msg = &MsgSendMessages{} 13 | 14 | // IMPORTANT: must implement this interface so that the GetCachedValue 15 | // method will work. 16 | // 17 | // docs: 18 | // https://docs.cosmos.network/main/core/encoding#interface-encoding-and-usage-of-any 19 | // 20 | // example in gov v1: 21 | // https://github.com/cosmos/cosmos-sdk/blob/v0.46.7/x/gov/types/v1/msgs.go#L97 22 | _ codectypes.UnpackInterfacesMessage = MsgSendMessages{} 23 | ) 24 | 25 | //------------------------------------------------------------------------------ 26 | // MsgRegisterAccount 27 | //------------------------------------------------------------------------------ 28 | 29 | func (m *MsgRegisterAccount) ValidateBasic() error { 30 | // the sender address must be valid 31 | if _, err := sdk.AccAddressFromBech32(m.Sender); err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func (m *MsgRegisterAccount) GetSigners() []sdk.AccAddress { 39 | // we have already asserted that the sender address is valid in 40 | // ValidateBasic, so can ignore the error here 41 | addr, _ := sdk.AccAddressFromBech32(m.Sender) 42 | return []sdk.AccAddress{addr} 43 | } 44 | 45 | //------------------------------------------------------------------------------ 46 | // MsgSendFunds 47 | //------------------------------------------------------------------------------ 48 | 49 | func (m *MsgSendFunds) ValidateBasic() error { 50 | // the authority address must be valid 51 | if _, err := sdk.AccAddressFromBech32(m.Authority); err != nil { 52 | return ErrInvalidProposalAuthority.Wrap(err.Error()) 53 | } 54 | 55 | // the coins amount must not be empty 56 | if m.Amount.Empty() { 57 | return ErrInvalidProposalAmount.Wrap("amount cannot be empty") 58 | } 59 | 60 | // the coins amount must be valid 61 | if err := m.Amount.Validate(); err != nil { 62 | return ErrInvalidProposalAmount.Wrap(err.Error()) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (m *MsgSendFunds) GetSigners() []sdk.AccAddress { 69 | // we have already asserted that the authority address is valid in 70 | // ValidateBasic, so can ignore the error here 71 | addr, _ := sdk.AccAddressFromBech32(m.Authority) 72 | return []sdk.AccAddress{addr} 73 | } 74 | 75 | //------------------------------------------------------------------------------ 76 | // MsgSendMessages 77 | //------------------------------------------------------------------------------ 78 | 79 | func (m *MsgSendMessages) ValidateBasic() error { 80 | // the authority address must be valid 81 | if _, err := sdk.AccAddressFromBech32(m.Authority); err != nil { 82 | return ErrInvalidProposalAuthority.Wrap(err.Error()) 83 | } 84 | 85 | // the messages must each implement the sdk.Msg interface 86 | msgs, err := sdktx.GetMsgs(m.Messages, sdk.MsgTypeURL(m)) 87 | if err != nil { 88 | return ErrInvalidProposalMsg.Wrap(err.Error()) 89 | } 90 | 91 | // there must be at least one message 92 | if len(msgs) < 1 { 93 | return ErrInvalidProposalMsg.Wrap("proposal must contain at least one message") 94 | } 95 | 96 | // ideally, we want to check each message: 97 | // 98 | // 1. is valid (run msg.ValidateBasic) 99 | // 2. has only one signer 100 | // 3. this one signer is the interchain account 101 | // 102 | // unfortunately, these are not possible: 103 | // 104 | // - for 1 and 2, the signer addresses has the host chain's bech prefix, 105 | // this would cause ValidateBasic and GetSigners to fail, despite the 106 | // message is perfectly valid. 107 | // - for 3, this is a stateful check (we need to query the ICA's address) 108 | // while in ValidateBasic we can only do stateless checks. 109 | return nil 110 | } 111 | 112 | func (m *MsgSendMessages) GetSigners() []sdk.AccAddress { 113 | // we have already asserted that the authority address is valid in 114 | // ValidateBasic, so can ignore the error here 115 | addr, _ := sdk.AccAddressFromBech32(m.Authority) 116 | return []sdk.AccAddress{addr} 117 | } 118 | 119 | func (m MsgSendMessages) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { 120 | return sdktx.UnpackInterfaces(unpacker, m.Messages) 121 | } 122 | -------------------------------------------------------------------------------- /x/envoy/types/tx_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 11 | govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" 12 | 13 | "github.com/mars-protocol/hub/v2/x/envoy/types" 14 | ) 15 | 16 | var ( 17 | testAuthority = authtypes.NewModuleAddress(types.ModuleName) 18 | testSender, _ = sdk.AccAddressFromBech32("cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs") 19 | testConnectionId = "connection-0" 20 | testChannelId = "channel-0" 21 | testValidMsg, _ = codectypes.NewAnyWithValue(govv1.NewMsgVote(testAuthority, 1, govv1.OptionYes, "")) 22 | testInvalidMsg = &codectypes.Any{TypeUrl: "/test.MsgInvalidTest", Value: []byte{}} 23 | ) 24 | 25 | func TestValidateBasic(t *testing.T) { 26 | testCases := []struct { 27 | name string 28 | msg sdk.Msg 29 | expPass bool 30 | }{ 31 | { 32 | "MsgRegisterAccount - success", 33 | &types.MsgRegisterAccount{ 34 | Sender: testSender.String(), 35 | ConnectionId: testConnectionId, 36 | }, 37 | true, 38 | }, 39 | { 40 | "MsgRegisterAccount - owner address is empty", 41 | &types.MsgRegisterAccount{ 42 | Sender: "", 43 | ConnectionId: testConnectionId, 44 | }, 45 | false, 46 | }, 47 | { 48 | "MsgRegisterAccount - owner address is invalid", 49 | &types.MsgRegisterAccount{ 50 | Sender: "larry", 51 | ConnectionId: testConnectionId, 52 | }, 53 | false, 54 | }, 55 | { 56 | "MsgSendFunds - success", 57 | &types.MsgSendFunds{ 58 | Authority: testAuthority.String(), 59 | ChannelId: testChannelId, 60 | Amount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(12345))), 61 | }, 62 | true, 63 | }, 64 | { 65 | "MsgSendFunds - coin amount is empty", 66 | &types.MsgSendFunds{ 67 | Authority: testAuthority.String(), 68 | ChannelId: testChannelId, 69 | Amount: sdk.NewCoins(), 70 | }, 71 | false, 72 | }, 73 | { 74 | "MsgSendFunds - coin amount is invalid", 75 | &types.MsgSendFunds{ 76 | Authority: testAuthority.String(), 77 | ChannelId: testChannelId, 78 | // denoms not sorted alphabetically 79 | Amount: []sdk.Coin{ 80 | sdk.NewCoin("umars", sdk.NewInt(12345)), 81 | sdk.NewCoin("uastro", sdk.NewInt(23456)), 82 | }, 83 | }, 84 | false, 85 | }, 86 | { 87 | "MsgSendMessages - success", 88 | &types.MsgSendMessages{ 89 | Authority: testAuthority.String(), 90 | ConnectionId: testConnectionId, 91 | Messages: []*codectypes.Any{testValidMsg}, 92 | }, 93 | true, 94 | }, 95 | { 96 | "MsgSendMessages - messages is empty", 97 | &types.MsgSendMessages{ 98 | Authority: testAuthority.String(), 99 | ConnectionId: testConnectionId, 100 | Messages: []*codectypes.Any{}, 101 | }, 102 | false, 103 | }, 104 | { 105 | "MsgSendMessages - message does not implement sdk.Msg interface", 106 | &types.MsgSendMessages{ 107 | Authority: testAuthority.String(), 108 | ConnectionId: testConnectionId, 109 | Messages: []*codectypes.Any{testInvalidMsg}, 110 | }, 111 | false, 112 | }, 113 | } 114 | 115 | for _, tc := range testCases { 116 | err := tc.msg.ValidateBasic() 117 | 118 | if tc.expPass { 119 | require.NoError(t, err, "expect success but failed: name = %s", tc.name) 120 | } else { 121 | require.Error(t, err, "expect error but succeeded: name = %s", tc.name) 122 | } 123 | } 124 | } 125 | 126 | func TestGetSigners(t *testing.T) { 127 | testCases := []struct { 128 | name string 129 | msg sdk.Msg 130 | expSigner sdk.AccAddress 131 | }{ 132 | { 133 | "MsgRegisterAccount", 134 | &types.MsgRegisterAccount{ 135 | Sender: testSender.String(), 136 | ConnectionId: testConnectionId, 137 | }, 138 | testSender, 139 | }, 140 | { 141 | "MsgSendFunds", 142 | &types.MsgSendFunds{ 143 | Authority: testAuthority.String(), 144 | ChannelId: testChannelId, 145 | Amount: sdk.NewCoins(), 146 | }, 147 | testAuthority, 148 | }, 149 | { 150 | "MsgSendMessages", 151 | &types.MsgSendMessages{ 152 | Authority: testAuthority.String(), 153 | ConnectionId: testConnectionId, 154 | Messages: []*codectypes.Any{}, 155 | }, 156 | testAuthority, 157 | }, 158 | } 159 | 160 | for _, tc := range testCases { 161 | require.Equal(t, []sdk.AccAddress{tc.expSigner}, tc.msg.GetSigners(), "incorrect sender: name = %s", tc.name) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /x/gov/README.md: -------------------------------------------------------------------------------- 1 | # Custom Gov 2 | 3 | The `customgov` module is wrapper around Cosmos SDK's vanilla `gov` module, inheriting most of its functionalities, with two changes: 4 | 5 | - A custom vote tallying logic. Namely, tokens locked in the [vesting contract](https://github.com/mars-protocol/periphery/tree/main/contracts/vesting) count towards one's governance voting power. 6 | - Type check proposal and vote metadata. 7 | 8 | ## Tallying 9 | 10 | Let's illustrate this by an example. 11 | 12 | Consider a blockchain with only one validator, as well as two users, Alice and Bob, who have the following amounts of tokens: 13 | 14 | | user | staked | vesting | 15 | | ----- | ------ | ------- | 16 | | Alice | 30 | 21 | 17 | | Bob | 49 | 0 | 18 | 19 | Assume the validator votes YES on a proposal, while neither Alice or Bob votes. In this case, the validator will vote on behalf of Alice and Bob's staked tokens. The vote will pass with 30 + 49 = 79 tokens voting YES and the rest 21 tokens not voting. 20 | 21 | If Alice votes NO, this overrides the validator's voting. The vote will be defeated by 49 tokens voting YES vs 51 tokens voting NO. 22 | 23 | ### Note 24 | 25 | Currently, the module assumes the vesting contract is the first contract to be deployed on the chain, i.e. having the code ID of 1 and instance ID of 1. The module uses this info [to derive the contract's address](https://github.com/mars-protocol/hub/blob/2d233fe074b008c49cf26362e1446d888fc81ca0/custom/gov/keeper/tally.go#L12-L15). Developers must make sure this is the case in the chain's genesis state. 26 | 27 | Why not make it a configurable parameter? Because doing so involves modifying gov module's `Params` type definition which breaks a bunch of things, which we prefer not to. 28 | 29 | ## Metadata 30 | 31 | From Cosmos SDK v0.46, governance proposals no longer have a "title" and a "description", but instead a "metadata" which can be an arbitrary string. According to [the docs](https://docs.cosmos.network/main/modules/gov#proposal-3), the recommended way to provide the metadata is to store it off-chain, and only upload an IPFS hash on-chain. Therefore, the vanilla gov module: 32 | 33 | - Has a default 255 character limit for the metadata string 34 | - Does not enforce a schema of the metadata string 35 | 36 | In Mars `customgov`, we want to storage the metadata on-chain. In order for this to work, we increase the length limit to `u64::MAX`, essentially without a limit. Additionally, we implement type checks for the metadata. Specifically, 37 | 38 | - For proposal metadata, we assert that it is non-empty and conforms to this schema (defined in TypeScript): 39 | 40 | ```typescript 41 | type ProposalMetadata = { 42 | title: string; 43 | authors?: string[]; 44 | summary: string; 45 | details?: string; 46 | proposal_forum_url?: string; 47 | vote_option_context?: string; 48 | }; 49 | ``` 50 | 51 | We make `title` and `summary` mandatory and the other fields optional, because from sdk 0.47 [proposals will have mandatory title and summary fields](https://github.com/cosmos/cosmos-sdk/blob/v0.47.0-rc1/proto/cosmos/gov/v1/gov.proto#L85-L93). Once Mars Hub upgrades to sdk 0.47, we can make these two fields optional as well. 52 | 53 | - For vote metadata, we assert that it is either an empty string (it's ok if a voter doesn't want to provide a rationale for their vote), or if it's not empty, conforms to this schema: 54 | 55 | ```typescript 56 | type VoteMetadata = { 57 | justification?: string; 58 | }; 59 | ``` 60 | -------------------------------------------------------------------------------- /x/gov/abci.go: -------------------------------------------------------------------------------- 1 | package gov 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/cosmos/cosmos-sdk/telemetry" 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | 10 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 11 | govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" 12 | 13 | "github.com/mars-protocol/hub/v2/x/gov/keeper" 14 | ) 15 | 16 | // EndBlocker called at the end of every block, processing proposals 17 | // 18 | // This is pretty much the same as the vanilla gov EndBlocker, except for we 19 | // replace the `Tally` function with our own implementation. 20 | func EndBlocker(ctx sdk.Context, keeper keeper.Keeper) { 21 | defer telemetry.ModuleMeasureSince(govtypes.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker) 22 | 23 | logger := keeper.Logger(ctx) 24 | 25 | // Delete dead proposals from store and returns theirs deposits. 26 | // A proposal is dead when it's inactive and didn't get enough deposit on 27 | // time to get into voting phase. 28 | keeper.IterateInactiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal govv1.Proposal) bool { 29 | keeper.DeleteProposal(ctx, proposal.Id) 30 | keeper.RefundAndDeleteDeposits(ctx, proposal.Id) 31 | 32 | // called when proposal become inactive 33 | keeper.AfterProposalFailedMinDeposit(ctx, proposal.Id) 34 | 35 | ctx.EventManager().EmitEvent( 36 | sdk.NewEvent( 37 | govtypes.EventTypeInactiveProposal, 38 | sdk.NewAttribute(govtypes.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)), 39 | sdk.NewAttribute(govtypes.AttributeKeyProposalResult, govtypes.AttributeValueProposalDropped), 40 | ), 41 | ) 42 | 43 | logger.Info( 44 | "proposal did not meet minimum deposit; deleted", 45 | "proposal", proposal.Id, 46 | "min_deposit", sdk.NewCoins(keeper.GetDepositParams(ctx).MinDeposit...).String(), 47 | "total_deposit", sdk.NewCoins(proposal.TotalDeposit...).String(), 48 | ) 49 | 50 | return false 51 | }) 52 | 53 | // fetch active proposals whose voting periods have ended (are passed the block time) 54 | keeper.IterateActiveProposalsQueue(ctx, ctx.BlockHeader().Time, func(proposal govv1.Proposal) bool { 55 | var tagValue, logMsg string 56 | 57 | // IMPORTANT: use our custom implementation of tally logics 58 | passes, burnDeposits, tallyResults := keeper.Tally(ctx, proposal) 59 | 60 | if burnDeposits { 61 | keeper.DeleteAndBurnDeposits(ctx, proposal.Id) 62 | } else { 63 | keeper.RefundAndDeleteDeposits(ctx, proposal.Id) 64 | } 65 | 66 | if passes { 67 | var ( 68 | idx int 69 | events sdk.Events 70 | msg sdk.Msg 71 | ) 72 | 73 | // attempt to execute all messages within the passed proposal 74 | // Messages may mutate state thus we use a cached context. If one of 75 | // the handlers fails, no state mutation is written and the error 76 | // message is logged. 77 | cacheCtx, writeCache := ctx.CacheContext() 78 | messages, err := proposal.GetMsgs() 79 | if err == nil { 80 | for idx, msg = range messages { 81 | handler := keeper.Router().Handler(msg) 82 | 83 | var res *sdk.Result 84 | res, err = handler(cacheCtx, msg) 85 | if err != nil { 86 | break 87 | } 88 | 89 | events = append(events, res.GetEvents()...) 90 | } 91 | } 92 | 93 | // `err == nil` when all handlers passed. 94 | // Or else, `idx` and `err` are populated with the msg index and error. 95 | if err == nil { 96 | proposal.Status = govv1.StatusPassed 97 | tagValue = govtypes.AttributeValueProposalPassed 98 | logMsg = "passed" 99 | 100 | // write state to the underlying multi-store 101 | writeCache() 102 | 103 | // propagate the msg events to the current context 104 | ctx.EventManager().EmitEvents(events) 105 | } else { 106 | proposal.Status = govv1.StatusFailed 107 | tagValue = govtypes.AttributeValueProposalFailed 108 | logMsg = fmt.Sprintf("passed, but msg %d (%s) failed on execution: %s", idx, sdk.MsgTypeURL(msg), err) 109 | } 110 | } else { 111 | proposal.Status = govv1.StatusRejected 112 | tagValue = govtypes.AttributeValueProposalRejected 113 | logMsg = "rejected" 114 | } 115 | 116 | proposal.FinalTallyResult = &tallyResults 117 | 118 | keeper.SetProposal(ctx, proposal) 119 | keeper.RemoveFromActiveProposalQueue(ctx, proposal.Id, *proposal.VotingEndTime) 120 | 121 | // when proposal become active 122 | keeper.AfterProposalVotingPeriodEnded(ctx, proposal.Id) 123 | 124 | logger.Info( 125 | "proposal tallied", 126 | "proposal", proposal.Id, 127 | "results", logMsg, 128 | ) 129 | 130 | ctx.EventManager().EmitEvent( 131 | sdk.NewEvent( 132 | govtypes.EventTypeActiveProposal, 133 | sdk.NewAttribute(govtypes.AttributeKeyProposalID, fmt.Sprintf("%d", proposal.Id)), 134 | sdk.NewAttribute(govtypes.AttributeKeyProposalResult, tagValue), 135 | ), 136 | ) 137 | 138 | return false 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /x/gov/keeper/keeper.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/baseapp" 5 | "github.com/cosmos/cosmos-sdk/codec" 6 | storetypes "github.com/cosmos/cosmos-sdk/store/types" 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | 9 | govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper" 10 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 11 | govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" 12 | 13 | wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" 14 | ) 15 | 16 | // Keeper defines the custom governance module Keeper 17 | // 18 | // NOTE: Keeper wraps the vanilla gov keeper to inherit most of its functions. 19 | // However, we include an additional dependency, the wasm keeper, which is 20 | // needed for our custom vote tallying logic. 21 | type Keeper struct { 22 | govkeeper.Keeper 23 | 24 | storeKey storetypes.StoreKey 25 | 26 | stakingKeeper govtypes.StakingKeeper // gov keeper has `sk` as a private field; we can't access it when tallying 27 | wasmKeeper wasmtypes.ViewKeeper 28 | } 29 | 30 | // NewKeeper returns a custom gov keeper 31 | // 32 | // NOTE: compared to the vanilla gov keeper's constructor function, here we 33 | // require an additional wasm keeper, which is needed for our custom vote 34 | // tallying logic. 35 | func NewKeeper( 36 | cdc codec.BinaryCodec, key storetypes.StoreKey, paramSpace govtypes.ParamSubspace, 37 | accountKeeper govtypes.AccountKeeper, bankKeeper govtypes.BankKeeper, stakingKeeper govtypes.StakingKeeper, 38 | wasmKeeper wasmtypes.ViewKeeper, legacyRouter govv1beta1.Router, router *baseapp.MsgServiceRouter, 39 | config govtypes.Config, 40 | ) Keeper { 41 | return Keeper{ 42 | Keeper: govkeeper.NewKeeper(cdc, key, paramSpace, accountKeeper, bankKeeper, stakingKeeper, legacyRouter, router, config), 43 | storeKey: key, 44 | stakingKeeper: stakingKeeper, 45 | wasmKeeper: wasmKeeper, 46 | } 47 | } 48 | 49 | // deleteVote deletes a vote from a given proposalID and voter from the store 50 | // 51 | // NOTE: the vanilla gov module does not make the `deleteVote` function public, 52 | // so in order to delete votes, we need to redefine the function here. 53 | // 54 | // TODO: As of sdk 0.46.7 this is still not made public... I should make a PR 55 | func (k Keeper) deleteVote(ctx sdk.Context, proposalID uint64, voterAddr sdk.AccAddress) { 56 | store := ctx.KVStore(k.storeKey) 57 | store.Delete(govtypes.VoteKey(proposalID, voterAddr)) 58 | } 59 | -------------------------------------------------------------------------------- /x/gov/keeper/keeper_test.go: -------------------------------------------------------------------------------- 1 | // There is no test in this file. 2 | // We have the setup script used by other tests in this package here. 3 | 4 | package keeper_test 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/require" 12 | 13 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 14 | 15 | sdk "github.com/cosmos/cosmos-sdk/types" 16 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 17 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 18 | govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" 19 | stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" 20 | 21 | wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" 22 | 23 | marsapp "github.com/mars-protocol/hub/v2/app" 24 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 25 | 26 | "github.com/mars-protocol/hub/v2/x/gov/testdata" 27 | "github.com/mars-protocol/hub/v2/x/gov/types" 28 | ) 29 | 30 | var mockSchedule = &types.Schedule{ 31 | StartTime: 10000, 32 | Cliff: 0, 33 | Duration: 1, 34 | } 35 | 36 | // VotingPower defines the composition of a mock account's voting power 37 | type VotingPower struct { 38 | Staked int64 39 | Vesting int64 40 | } 41 | 42 | func setupTest(t *testing.T, votingPowers []VotingPower) (ctx sdk.Context, app *marsapp.MarsApp, proposal govv1.Proposal, valoper sdk.AccAddress, voters []sdk.AccAddress) { 43 | accts := marsapptesting.MakeRandomAccounts(len(votingPowers) + 2) 44 | deployer := accts[0] 45 | valoper = accts[1] 46 | voters = accts[2:] 47 | 48 | // calculate the sum of tokens staked and in vesting 49 | totalStaked := sdk.ZeroInt() 50 | totalVesting := sdk.ZeroInt() 51 | for _, votingPower := range votingPowers { 52 | totalStaked = totalStaked.Add(sdk.NewInt(votingPower.Staked)) 53 | totalVesting = totalVesting.Add(sdk.NewInt(votingPower.Vesting)) 54 | } 55 | 56 | // set mars token balance for deployer and voters 57 | balances := []banktypes.Balance{{ 58 | Address: deployer.String(), 59 | Coins: sdk.NewCoins(sdk.NewCoin(marsapp.BondDenom, totalVesting)), 60 | }} 61 | for idx, votingPower := range votingPowers { 62 | balances = append(balances, banktypes.Balance{ 63 | Address: voters[idx].String(), 64 | Coins: sdk.NewCoins(sdk.NewCoin(marsapp.BondDenom, sdk.NewInt(votingPower.Staked))), 65 | }) 66 | } 67 | 68 | app = marsapptesting.MakeMockApp(accts, balances, []sdk.AccAddress{valoper}, sdk.NewCoins()) 69 | ctx = app.BaseApp.NewContext(false, tmproto.Header{Time: time.Now()}) 70 | 71 | // register voter accounts at the auth module 72 | for _, voter := range voters { 73 | app.AccountKeeper.SetAccount(ctx, authtypes.NewBaseAccountWithAddress(voter)) 74 | } 75 | 76 | // voters make delegations 77 | for idx, votingPower := range votingPowers { 78 | if votingPower.Staked > 0 { 79 | val, found := app.StakingKeeper.GetValidator(ctx, sdk.ValAddress(valoper)) 80 | require.True(t, found) 81 | 82 | _, err := app.StakingKeeper.Delegate( 83 | ctx, 84 | voters[idx], 85 | sdk.NewInt(votingPower.Staked), 86 | stakingtypes.Unbonded, 87 | val, 88 | true, // true means it's a delegation, not a redelegation 89 | ) 90 | require.NoError(t, err) 91 | } 92 | } 93 | 94 | contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper) 95 | 96 | // store vesting contract code 97 | codeID, _, err := contractKeeper.Create(ctx, deployer, testdata.VestingWasm, nil) 98 | require.NoError(t, err) 99 | 100 | // instantiate vesting contract 101 | instantiateMsg, err := json.Marshal(&types.InstantiateMsg{ 102 | Owner: deployer.String(), 103 | UnlockSchedule: mockSchedule, 104 | }) 105 | require.NoError(t, err) 106 | 107 | contractAddr, _, err := contractKeeper.Instantiate( 108 | ctx, 109 | codeID, 110 | deployer, 111 | nil, 112 | instantiateMsg, 113 | "mars/vesting", 114 | sdk.NewCoins(), 115 | ) 116 | require.NoError(t, err) 117 | 118 | // create a vesting positions for voters 119 | for idx, votingPower := range votingPowers { 120 | if votingPower.Vesting > 0 { 121 | executeMsg, err := json.Marshal(&types.ExecuteMsg{ 122 | CreatePosition: &types.CreatePosition{ 123 | User: voters[idx].String(), 124 | VestSchedule: mockSchedule, 125 | }, 126 | }) 127 | require.NoError(t, err) 128 | 129 | _, err = contractKeeper.Execute( 130 | ctx, 131 | contractAddr, 132 | deployer, 133 | executeMsg, 134 | sdk.NewCoins(sdk.NewCoin(marsapp.BondDenom, sdk.NewInt(votingPower.Vesting))), 135 | ) 136 | require.NoError(t, err) 137 | } 138 | } 139 | 140 | // create a governance proposal 141 | // 142 | // typically it requires a minimum deposit to make the proposal enter voting 143 | // period, but here we forcibly set the status as StatusVotingPeriod. 144 | // 145 | // typically we require the proposal's metadata to conform to a schema, but 146 | // it's not necessary here as we're not creating the proposal through the 147 | // msgServer. 148 | proposal, err = govv1.NewProposal([]sdk.Msg{}, 1, "", time.Now(), time.Now()) 149 | proposal.Status = govv1.StatusVotingPeriod 150 | require.NoError(t, err) 151 | 152 | app.GovKeeper.SetProposal(ctx, proposal) 153 | 154 | return ctx, app, proposal, valoper, voters 155 | } 156 | -------------------------------------------------------------------------------- /x/gov/keeper/msg_server_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 11 | govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" 12 | govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" 13 | 14 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 15 | 16 | "github.com/mars-protocol/hub/v2/x/gov/keeper" 17 | "github.com/mars-protocol/hub/v2/x/gov/types" 18 | ) 19 | 20 | func TestProposalMetadataTypeCheck(t *testing.T) { 21 | ctx, app, _, _, _ := setupTest(t, []VotingPower{{Staked: 1_000_000, Vesting: 0}}) 22 | 23 | testCases := []struct { 24 | name string 25 | metadataStr string 26 | expPass bool 27 | }{ 28 | { 29 | "a valid proposal metadata", 30 | `{ 31 | "title": "Mock Proposal", 32 | "authors": ["Larry Engineer "], 33 | "summary": "Mock proposal for testing purposes", 34 | "details": "This is a mock-up proposal for use in the unit tests of Mars Hub's gov module.", 35 | "proposal_forum_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", 36 | "vote_option_context": "Vote yes if you like this proposal, Vote no if you don't like it." 37 | }`, 38 | true, 39 | }, 40 | { 41 | "a valid metadata with missing optional fields", 42 | `{ 43 | "title": "Mock Proposal", 44 | "summary": "Mock proposal for testing purposes" 45 | }`, 46 | true, 47 | }, 48 | { 49 | "an invalid metadata with mandatory fields missing", 50 | `{ 51 | "title": "Mock Proposal", 52 | "details": "This is a mock-up proposal for use in the unit tests of Mars Hub's gov module." 53 | }`, 54 | false, 55 | }, 56 | { 57 | "extra unexpected fields are accepted", 58 | `{ 59 | "title": "Mock Proposal", 60 | "summary": "Mock proposal for testing purposes", 61 | "foo": "bar" 62 | }`, 63 | true, 64 | }, 65 | { 66 | "empty proposal metadata string is not accepted", 67 | "", 68 | false, 69 | }, 70 | } 71 | 72 | msgServer := keeper.NewMsgServerImpl(app.GovKeeper) 73 | 74 | for _, tc := range testCases { 75 | _, err := msgServer.SubmitProposal(ctx, newMsgSubmitProposal(t, tc.metadataStr)) 76 | 77 | if tc.expPass { 78 | require.NoError(t, err, "expect success but failed: name = %s", tc.name) 79 | } else { 80 | require.Error(t, err, "expect error but succeeded: name = %s", tc.name) 81 | } 82 | } 83 | } 84 | 85 | func TestVoteMetadataTypeCheck(t *testing.T) { 86 | ctx, app, _, _, _ := setupTest(t, []VotingPower{{Staked: 1_000_000, Vesting: 0}}) 87 | 88 | testCases := []struct { 89 | name string 90 | metadataStr string 91 | expPass bool 92 | }{ 93 | { 94 | "a valid vote metadata", 95 | `{"justification":"I like the proposal"}`, 96 | true, 97 | }, 98 | { 99 | "a valid metadata with missing optional fields", 100 | "{}", 101 | true, 102 | }, 103 | { 104 | "extra unexpected fields are accepted", 105 | `{"foo":"bar"}`, 106 | true, 107 | }, 108 | { 109 | "empty metadata string is accepted", 110 | "", 111 | true, 112 | }, 113 | } 114 | 115 | msgServer := keeper.NewMsgServerImpl(app.GovKeeper) 116 | 117 | for _, tc := range testCases { 118 | _, err := msgServer.Vote(ctx, newMsgVote(tc.metadataStr)) 119 | 120 | if tc.expPass { 121 | require.NoError(t, err, "expect success but failed: name = %s", tc.name) 122 | } else { 123 | require.Error(t, err, "expect error but succeeded: name = %s", tc.name) 124 | } 125 | } 126 | } 127 | 128 | func TestLegacyProposalMetadata(t *testing.T) { 129 | ctx, app, _, _, _ := setupTest(t, []VotingPower{{Staked: 1_000_000, Vesting: 0}}) 130 | 131 | macc := app.AccountKeeper.GetModuleAddress(govtypes.ModuleName) 132 | 133 | addrs := marsapptesting.MakeRandomAccounts(1) 134 | proposer := addrs[0] 135 | 136 | msgServer := keeper.NewMsgServerImpl(app.GovKeeper) 137 | legacyMsgServer := keeper.NewLegacyMsgServerImpl(macc.String(), msgServer) 138 | 139 | content := govv1beta1.NewTextProposal( 140 | "Test community pool spend proposal", 141 | "This is a mock proposal for testing the conversion of v1beta1 to v1 proposal", 142 | ) 143 | 144 | expectedMetadataStr, err := json.Marshal(&types.ProposalMetadata{ 145 | Title: content.GetTitle(), 146 | Summary: content.GetDescription(), 147 | }) 148 | require.NoError(t, err) 149 | 150 | legacyMsg, err := govv1beta1.NewMsgSubmitProposal(content, sdk.NewCoins(), proposer) 151 | require.NoError(t, err) 152 | 153 | _, err = legacyMsgServer.SubmitProposal(ctx, legacyMsg) 154 | require.NoError(t, err) 155 | 156 | proposal, found := app.GovKeeper.GetProposal(ctx, 1) 157 | require.Equal(t, true, found) 158 | require.Equal(t, string(expectedMetadataStr), proposal.Metadata) 159 | } 160 | 161 | func newMsgSubmitProposal(t *testing.T, metadataStr string) *govv1.MsgSubmitProposal { 162 | addrs := marsapptesting.MakeRandomAccounts(1) 163 | proposer := addrs[0] 164 | 165 | proposal, err := govv1.NewMsgSubmitProposal([]sdk.Msg{}, sdk.NewCoins(), proposer.String(), metadataStr) 166 | require.NoError(t, err) 167 | 168 | return proposal 169 | } 170 | 171 | func newMsgVote(metadataStr string) *govv1.MsgVote { 172 | addrs := marsapptesting.MakeRandomAccounts(1) 173 | voter := addrs[0] 174 | 175 | return govv1.NewMsgVote(voter, 1, govv1.VoteOption_VOTE_OPTION_YES, metadataStr) 176 | } 177 | -------------------------------------------------------------------------------- /x/gov/keeper/query_server_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/cosmos/cosmos-sdk/baseapp" 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" 12 | ) 13 | 14 | func TestQueryServer(t *testing.T) { 15 | ctx, app, proposal, valoper, voters := setupTest(t, []VotingPower{ 16 | {Staked: 30_000_000, Vesting: 21_000_000}, 17 | {Staked: 48_000_000, Vesting: 0}, 18 | }) 19 | 20 | // validator votes yes 21 | // voters[0] votes no, overriding the validator's vote 22 | // this should results in 49 yes vs 51 no 23 | // in comparison, with the vanilla tallying logic, the result would be 49 yes vs 30 no 24 | app.GovKeeper.AddVote(ctx, proposal.Id, valoper, govv1.NewNonSplitVoteOption(govv1.OptionYes), "") 25 | app.GovKeeper.AddVote(ctx, proposal.Id, voters[0], govv1.NewNonSplitVoteOption(govv1.OptionNo), "") 26 | 27 | queryClient := govv1.NewQueryClient(&baseapp.QueryServiceTestHelper{ 28 | Ctx: ctx, 29 | GRPCQueryRouter: app.GRPCQueryRouter(), 30 | }) 31 | 32 | // query proposal - for this one we use the vanilla logic 33 | { 34 | res, err := queryClient.Proposal(context.Background(), &govv1.QueryProposalRequest{ProposalId: proposal.Id}) 35 | require.NoError(t, err) 36 | require.Equal(t, proposal.String(), res.Proposal.String()) 37 | } 38 | 39 | // query votes - for this one we use the vanilla logic 40 | { 41 | res, err := queryClient.Vote(context.Background(), &govv1.QueryVoteRequest{ProposalId: proposal.Id, Voter: voters[0].String()}) 42 | require.NoError(t, err) 43 | require.Equal(t, 1, len(res.Vote.Options)) 44 | require.Equal(t, &govv1.WeightedVoteOption{Option: govv1.OptionNo, Weight: sdk.NewDec(1).String()}, res.Vote.Options[0]) 45 | } 46 | 47 | // query tally result - this one is replaced with our custom logic 48 | { 49 | res, err := queryClient.TallyResult(context.Background(), &govv1.QueryTallyResultRequest{ProposalId: proposal.Id}) 50 | require.NoError(t, err) 51 | require.Equal(t, "49000000", res.Tally.YesCount) 52 | require.Equal(t, "51000000", res.Tally.NoCount) 53 | require.Equal(t, "0", res.Tally.NoWithVetoCount) 54 | require.Equal(t, "0", res.Tally.AbstainCount) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /x/gov/keeper/vesting.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "cosmossdk.io/math" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | 11 | wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" 12 | 13 | "github.com/mars-protocol/hub/v2/x/gov/types" 14 | ) 15 | 16 | // queryVotingPowers queries the vesting contract of user voting powers based on 17 | // the given query msg 18 | func queryVotingPowers(ctx sdk.Context, k wasmtypes.ViewKeeper, contractAddr sdk.AccAddress, query *types.VotingPowersQuery) (types.VotingPowersResponse, error) { 19 | var votingPowersResponse types.VotingPowersResponse 20 | 21 | req, err := json.Marshal(&types.QueryMsg{VotingPowers: query}) 22 | if err != nil { 23 | return nil, types.ErrFailedToQueryVesting.Wrapf("failed to marshal query request: %s", err) 24 | } 25 | 26 | res, err := k.QuerySmart(ctx, contractAddr, req) 27 | if err != nil { 28 | return nil, types.ErrFailedToQueryVesting.Wrapf("query returned error: %s", err) 29 | } 30 | 31 | err = json.Unmarshal(res, &votingPowersResponse) 32 | if err != nil { 33 | return nil, types.ErrFailedToQueryVesting.Wrapf("failed to unmarshal query response: %s", err) 34 | } 35 | 36 | return votingPowersResponse, nil 37 | } 38 | 39 | // incrementVotingPowers increments the voting power counter based on the 40 | // contract query response 41 | // 42 | // NOTE: This function modifies the `tokensInVesting` and `totalTokensInVesting` 43 | // variables in place. This is what we typically do in Rust (passing a &mut) but 44 | // doesn't seem to by very idiomatic in Go. But it works so I'm gonna keep it 45 | // this way. 46 | func incrementVotingPowers(votingPowersResponse types.VotingPowersResponse, tokensInVesting map[string]math.Int, totalTokensInVesting *math.Int) error { 47 | for _, item := range votingPowersResponse { 48 | if _, ok := tokensInVesting[item.User]; ok { 49 | return types.ErrFailedToQueryVesting.Wrapf("query response contains duplicate address: %s", item.User) 50 | } 51 | 52 | tokensInVesting[item.User] = math.Int(item.VotingPower) 53 | *totalTokensInVesting = totalTokensInVesting.Add(math.Int(item.VotingPower)) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // GetTokensInVesting queries the vesting contract for an array of users who 60 | // have tokens locked in the contract and their respective amount, as well as 61 | // computing the total amount of locked tokens. 62 | func GetTokensInVesting(ctx sdk.Context, k wasmtypes.ViewKeeper, contractAddr sdk.AccAddress) (map[string]math.Int, math.Int, error) { 63 | tokensInVesting := make(map[string]math.Int) 64 | totalTokensInVesting := sdk.ZeroInt() 65 | 66 | votingPowersResponse, err := queryVotingPowers(ctx, k, contractAddr, &types.VotingPowersQuery{}) 67 | if err != nil { 68 | return nil, sdk.ZeroInt(), err 69 | } 70 | 71 | if err = incrementVotingPowers(votingPowersResponse, tokensInVesting, &totalTokensInVesting); err != nil { 72 | return nil, sdk.ZeroInt(), err 73 | } 74 | 75 | for { 76 | count := len(votingPowersResponse) 77 | if count == 0 { 78 | break 79 | } 80 | 81 | startAfter := votingPowersResponse[count-1].User 82 | 83 | votingPowersResponse, err = queryVotingPowers(ctx, k, contractAddr, &types.VotingPowersQuery{StartAfter: startAfter}) 84 | if err != nil { 85 | return nil, sdk.ZeroInt(), err 86 | } 87 | 88 | if err = incrementVotingPowers(votingPowersResponse, tokensInVesting, &totalTokensInVesting); err != nil { 89 | return nil, sdk.ZeroInt(), err 90 | } 91 | } 92 | 93 | return tokensInVesting, totalTokensInVesting, nil 94 | } 95 | 96 | // MustGetTokensInVesting is the same with `GetTokensInVesting`, but panics on 97 | // error. 98 | func MustGetTokensInVesting(ctx sdk.Context, k wasmtypes.ViewKeeper, contractAddr sdk.AccAddress) (map[string]math.Int, math.Int) { 99 | tokensInVesting, totalTokensInVesting, err := GetTokensInVesting(ctx, k, contractAddr) 100 | if err != nil { 101 | panic(fmt.Sprintf("failed to tally vote: %s", err)) 102 | } 103 | 104 | return tokensInVesting, totalTokensInVesting 105 | } 106 | -------------------------------------------------------------------------------- /x/gov/keeper/vesting_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 11 | 12 | sdk "github.com/cosmos/cosmos-sdk/types" 13 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 14 | 15 | wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" 16 | 17 | marsapp "github.com/mars-protocol/hub/v2/app" 18 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 19 | 20 | "github.com/mars-protocol/hub/v2/x/gov/keeper" 21 | "github.com/mars-protocol/hub/v2/x/gov/testdata" 22 | "github.com/mars-protocol/hub/v2/x/gov/types" 23 | ) 24 | 25 | func TestQueryVotingPowers(t *testing.T) { 26 | // generate random addresses 27 | accts := marsapptesting.MakeRandomAccounts(4) 28 | validator := accts[0] 29 | deployer := accts[1] 30 | voters := accts[2:] 31 | 32 | // create mock app and context 33 | app := marsapptesting.MakeMockApp( 34 | accts, 35 | []banktypes.Balance{{ 36 | Address: deployer.String(), 37 | Coins: sdk.NewCoins(sdk.NewCoin(marsapp.BondDenom, sdk.NewInt(50000000))), 38 | }}, 39 | []sdk.AccAddress{validator}, 40 | sdk.NewCoins(), 41 | ) 42 | ctx := app.BaseApp.NewContext(false, tmproto.Header{Time: time.Unix(10000, 0)}) // block time is required for testing 43 | 44 | // take the wasm keeper from the app 45 | contractKeeper := wasmkeeper.NewDefaultPermissionKeeper(app.WasmKeeper) 46 | 47 | // store vesting contract code 48 | codeID, _, err := contractKeeper.Create(ctx, deployer, testdata.VestingWasm, nil) 49 | require.NoError(t, err) 50 | 51 | // instantiate vesting contract 52 | instantiateMsg, err := json.Marshal(&types.InstantiateMsg{ 53 | Owner: deployer.String(), 54 | UnlockSchedule: &types.Schedule{ 55 | StartTime: 10000, 56 | Cliff: 0, 57 | Duration: 1, 58 | }, 59 | }) 60 | require.NoError(t, err) 61 | 62 | contractAddr, _, err := contractKeeper.Instantiate( 63 | ctx, 64 | codeID, 65 | deployer, 66 | nil, 67 | instantiateMsg, 68 | "mars/vesting", 69 | sdk.NewCoins(), 70 | ) 71 | require.NoError(t, err) 72 | 73 | // create vesting position for voters[0] with 30_000_000 umars 74 | executeMsg, err := json.Marshal(&types.ExecuteMsg{ 75 | CreatePosition: &types.CreatePosition{ 76 | User: voters[0].String(), 77 | VestSchedule: &types.Schedule{ 78 | StartTime: 10000, 79 | Cliff: 0, 80 | Duration: 20000, 81 | }, 82 | }, 83 | }) 84 | require.NoError(t, err) 85 | 86 | _, err = contractKeeper.Execute( 87 | ctx, 88 | contractAddr, 89 | deployer, 90 | executeMsg, 91 | sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(30000000))), 92 | ) 93 | require.NoError(t, err) 94 | 95 | // create vesting position for voters[1] with 20_000_000 umars 96 | executeMsg, err = json.Marshal(&types.ExecuteMsg{ 97 | CreatePosition: &types.CreatePosition{ 98 | User: voters[1].String(), 99 | VestSchedule: &types.Schedule{ 100 | StartTime: 0, 101 | Cliff: 0, 102 | Duration: 20000, 103 | }, 104 | }, 105 | }) 106 | require.NoError(t, err) 107 | 108 | _, err = contractKeeper.Execute( 109 | ctx, 110 | contractAddr, 111 | deployer, 112 | executeMsg, 113 | sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(20000000))), 114 | ) 115 | require.NoError(t, err) 116 | 117 | // voters should have 50_000_000 umars locked in vesting combined 118 | tokensInVesting, totalTokensInVesting, err := keeper.GetTokensInVesting(ctx, app.WasmKeeper, contractAddr) 119 | require.NoError(t, err) 120 | require.Equal(t, sdk.NewInt(50000000), totalTokensInVesting) 121 | require.Equal(t, sdk.NewInt(30000000), tokensInVesting[voters[0].String()]) 122 | require.Equal(t, sdk.NewInt(20000000), tokensInVesting[voters[1].String()]) 123 | 124 | // set time to 20000 125 | ctx = ctx.WithBlockTime(time.Unix(20000, 0)) 126 | 127 | // voters[0] is able to withdraw half of their vested tokens, i.e. 15_000_000 umars 128 | executeMsg, err = json.Marshal(&types.ExecuteMsg{ 129 | Withdraw: &types.Withdraw{}, 130 | }) 131 | require.NoError(t, err) 132 | 133 | _, err = contractKeeper.Execute( 134 | ctx, 135 | contractAddr, 136 | voters[0], 137 | executeMsg, 138 | sdk.NewCoins(), 139 | ) 140 | require.NoError(t, err) 141 | 142 | tokensInVesting, totalTokensInVesting, err = keeper.GetTokensInVesting(ctx, app.WasmKeeper, contractAddr) 143 | require.NoError(t, err) 144 | require.Equal(t, sdk.NewInt(35000000), totalTokensInVesting) 145 | require.Equal(t, sdk.NewInt(15000000), tokensInVesting[voters[0].String()]) 146 | require.Equal(t, sdk.NewInt(20000000), tokensInVesting[voters[1].String()]) 147 | } 148 | -------------------------------------------------------------------------------- /x/gov/module.go: -------------------------------------------------------------------------------- 1 | package gov 2 | 3 | import ( 4 | abci "github.com/tendermint/tendermint/abci/types" 5 | 6 | "github.com/cosmos/cosmos-sdk/codec" 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | "github.com/cosmos/cosmos-sdk/types/module" 9 | 10 | "github.com/cosmos/cosmos-sdk/x/gov" 11 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 12 | govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" 13 | govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" 14 | 15 | "github.com/mars-protocol/hub/v2/x/gov/keeper" 16 | ) 17 | 18 | // AppModule must implement the `module.AppModule` interface 19 | var _ module.AppModule = AppModule{} 20 | 21 | // AppModule implements an application module for the custom gov module 22 | // 23 | // NOTE: our custom AppModule wraps the vanilla `gov.AppModule` to inherit most 24 | // of its functions. However, we overwrite the `EndBlock` function to replace it 25 | // with our custom vote tallying logic. 26 | type AppModule struct { 27 | gov.AppModule 28 | 29 | keeper keeper.Keeper 30 | accountKeeper govtypes.AccountKeeper 31 | } 32 | 33 | // NewAppModule creates a new AppModule object 34 | func NewAppModule(cdc codec.Codec, keeper keeper.Keeper, ak govtypes.AccountKeeper, bk govtypes.BankKeeper) AppModule { 35 | return AppModule{ 36 | AppModule: gov.NewAppModule(cdc, keeper.Keeper, ak, bk), 37 | keeper: keeper, 38 | accountKeeper: ak, 39 | } 40 | } 41 | 42 | // EndBlock returns the end blocker for the gov module. It returns no validator 43 | // updates. 44 | // 45 | // NOTE: this overwrites the vanilla gov module EndBlocker with our custom vote 46 | // tallying logic. 47 | func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { 48 | EndBlocker(ctx, am.keeper) 49 | return []abci.ValidatorUpdate{} 50 | } 51 | 52 | // RegisterServices registers module services. 53 | // 54 | // NOTE: this overwrites the vanilla gov module RegisterServices function 55 | func (am AppModule) RegisterServices(cfg module.Configurator) { 56 | macc := am.accountKeeper.GetModuleAddress(govtypes.ModuleName).String() 57 | 58 | // msg server - use the vanilla implementation 59 | // The changes we've made to execution are in EndBlocker, so the msgServer 60 | // doesn't need to be changed. 61 | msgServer := keeper.NewMsgServerImpl(am.keeper) 62 | govv1beta1.RegisterMsgServer(cfg.MsgServer(), keeper.NewLegacyMsgServerImpl(macc, msgServer)) 63 | govv1.RegisterMsgServer(cfg.MsgServer(), msgServer) 64 | 65 | // query server - use our custom implementation 66 | queryServer := keeper.NewQueryServerImpl(am.keeper) 67 | govv1beta1.RegisterQueryServer(cfg.QueryServer(), keeper.NewLegacyQueryServerImpl(queryServer)) 68 | govv1.RegisterQueryServer(cfg.QueryServer(), queryServer) 69 | } 70 | -------------------------------------------------------------------------------- /x/gov/testdata/data.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import _ "embed" 4 | 5 | // VestingWasm is bytecode of the [`mars-vesting`](https://github.com/mars-protocol/hub-periphery/tree/main/contracts/vesting) contract 6 | //go:embed vesting.wasm 7 | var VestingWasm []byte 8 | -------------------------------------------------------------------------------- /x/gov/testdata/vesting.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mars-protocol/hub/717902c1a1365c65f20920b71d238a3dd4c2d789/x/gov/testdata/vesting.wasm -------------------------------------------------------------------------------- /x/gov/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "cosmossdk.io/errors" 5 | 6 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 7 | ) 8 | 9 | // NOTE: The latest version (v0.47.0) of the vanilla gov module already uses 10 | // error codes 2-16, so we start from 17. 11 | // https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/types/errors.go 12 | var ( 13 | ErrFailedToQueryVesting = errors.Register(govtypes.ModuleName, 17, "failed to query vesting contract") 14 | ErrInvalidMetadata = errors.Register(govtypes.ModuleName, 18, "invalid proposal or vote metadata") 15 | ) 16 | -------------------------------------------------------------------------------- /x/gov/types/metadata.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // ProposalMetadata defines the required schema for proposal metadata. 8 | type ProposalMetadata struct { 9 | Title string `json:"title"` 10 | Authors []string `json:"authors,omitempty"` 11 | Summary string `json:"summary"` 12 | Details string `json:"details,omitempty"` 13 | ProposalForumURL string `json:"proposal_forum_url,omitempty"` 14 | VoteOptionContext string `json:"vote_option_context,omitempty"` 15 | } 16 | 17 | // VoteMetadata defines the required schema for vote metadata. 18 | type VoteMetadata struct { 19 | Justification string `json:"justification,omitempty"` 20 | } 21 | 22 | // UnmarshalProposalMetadata unmarshals a string into ProposalMetadata. 23 | // 24 | // Golang's JSON unmarshal function doesn't check for missing fields. Instead, 25 | // for example, if the "title" field here in ProposalMetadata is missing, the 26 | // json.Unmarshal simply returns metadata.Title = "" instead of throwing an 27 | // error. 28 | // 29 | // Here's the equivalent Rust code for comparison, which properly throws an 30 | // error is a required field is missing: 31 | // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0e2eadad38b7cd212962b1a0e7a6da44 32 | // 33 | // Therefore, we have to implement our own unmarshal function which checks for 34 | // missing fields. 35 | func UnmarshalProposalMetadata(metadataStr string) (*ProposalMetadata, error) { 36 | var metadata ProposalMetadata 37 | 38 | if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil { 39 | return nil, ErrInvalidMetadata.Wrap(err.Error()) 40 | } 41 | 42 | if metadata.Title == "" { 43 | return nil, ErrInvalidMetadata.Wrap("missing field `title`") 44 | } 45 | 46 | if metadata.Summary == "" { 47 | return nil, ErrInvalidMetadata.Wrap("missing field `summary`") 48 | } 49 | 50 | return &metadata, nil 51 | } 52 | 53 | // UnmarshalVoteMetadata unmarshals a string into VoteMetdata. 54 | func UnmarshalVoteMetadata(metadataStr string) (*VoteMetadata, error) { 55 | var metadata VoteMetadata 56 | 57 | if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil { 58 | return nil, ErrInvalidMetadata.Wrap(err.Error()) 59 | } 60 | 61 | return &metadata, nil 62 | } 63 | -------------------------------------------------------------------------------- /x/gov/types/vesting.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "cosmossdk.io/math" 4 | 5 | // Schedule corresponding to the Rust type `mars_vesting::msg::Schedule` 6 | type Schedule struct { 7 | StartTime uint64 `json:"start_time"` 8 | Cliff uint64 `json:"cliff"` 9 | Duration uint64 `json:"duration"` 10 | } 11 | 12 | // InstantiateMsg corresponding to the Rust type `mars_vesting::msg::InstantiateMsg` 13 | type InstantiateMsg struct { 14 | Owner string `json:"owner"` 15 | UnlockSchedule *Schedule `json:"unlock_schedule"` 16 | } 17 | 18 | // ExecuteMsg corresponding to the Rust enum `mars_vesting::msg::ExecuteMsg` 19 | // 20 | // NOTE: For covenience, we don't include other enum variants, as they are not 21 | // needed here. 22 | type ExecuteMsg struct { 23 | CreatePosition *CreatePosition `json:"create_position,omitempty"` 24 | Withdraw *Withdraw `json:"withdraw,omitempty"` 25 | } 26 | 27 | // CreatePosition corresponding to the Rust enum variant `mars_vesting::msg::ExecuteMsg::CreatePosition` 28 | type CreatePosition struct { 29 | User string `json:"user"` 30 | VestSchedule *Schedule `json:"vest_schedule"` 31 | } 32 | 33 | // Withdraw corresponding to the Rust enum variant `mars_vesting::msg::ExecuteMsg::Withdraw` 34 | type Withdraw struct{} 35 | 36 | // QueryMsg corresponding to the Rust enum `mars_vesting::msg::QueryMsg` 37 | // 38 | // NOTE: For covenience, we don't include other enum variants, as they are not 39 | // needed here. 40 | type QueryMsg struct { 41 | VotingPower *VotingPowerQuery `json:"voting_power,omitempty"` 42 | VotingPowers *VotingPowersQuery `json:"voting_powers,omitempty"` 43 | } 44 | 45 | // VotingPowerQuery corresponding to the Rust enum variant `mars_vesting::msg::QueryMsg::VotingPower` 46 | type VotingPowerQuery struct { 47 | User string `json:"user,omitempty"` 48 | } 49 | 50 | // VotingPowersQuery corresponding to the Rust enum variant `mars_vesting::msg::QueryMsg::VotingPowers` 51 | type VotingPowersQuery struct { 52 | StartAfter string `json:"start_after,omitempty"` 53 | Limit uint32 `json:"limit,omitempty"` 54 | } 55 | 56 | // VotingPowerResponseItem corresponding to the `voting_powers` query's respons 57 | // type's repeating element 58 | type VotingPowerResponse struct { 59 | User string `json:"user"` 60 | VotingPower math.Uint `json:"voting_power"` 61 | } 62 | 63 | // VotingPowerResponse corresponding to the response type of the `voting_powers` 64 | // query 65 | type VotingPowersResponse []VotingPowerResponse 66 | -------------------------------------------------------------------------------- /x/incentives/README.md: -------------------------------------------------------------------------------- 1 | # Incentives 2 | 3 | The incentives module manages incentivization for MARS stakers. **Not to be confused** with incentives for lending/borrowing activities, which are managed by a wasm contract deployed at each Outpost. 4 | 5 | The release of incentives are defined by **Schedules**. Each incentives schedule consists three (3) parameters: 6 | 7 | - `StartTime` 8 | - `EndTime` 9 | - `TotalAmount` 10 | 11 | Between the timespan defined by `StartTime` and `EndTime`, coins specified by `TotalAmount` will be released as staking rewards _linearly_, in the `BeginBlocker` of each block. Each validator _who have signed the previous block_ gets a portion of the block reward pro-rata according to their voting power. 12 | 13 | A new schedule can be created upon a successful `CreateIncentivesScheduleProposal`. The incentives module will withdraw the coins corresponding to `TotalAmount` from the community pool to its module account. Conversely, an active schedule can be cancelled upon a successful `TerminateIncentivesScheduleProposal`. All coins yet to be distributed will be returned to the community pool. 14 | 15 | There can be multiple schedules active at the same time, each identified by a `uint64`. Each schedule can release multiple coins, not limited to the MARS token. 16 | -------------------------------------------------------------------------------- /x/incentives/abci.go: -------------------------------------------------------------------------------- 1 | package incentives 2 | 3 | import ( 4 | "time" 5 | 6 | abci "github.com/tendermint/tendermint/abci/types" 7 | 8 | "github.com/cosmos/cosmos-sdk/telemetry" 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | 11 | marsutils "github.com/mars-protocol/hub/v2/utils" 12 | 13 | "github.com/mars-protocol/hub/v2/x/incentives/keeper" 14 | "github.com/mars-protocol/hub/v2/x/incentives/types" 15 | ) 16 | 17 | // BeginBlocker distributes block rewards to validators who have signed the 18 | // previous block. 19 | func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) { 20 | defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker) 21 | 22 | ids, totalBlockReward := k.ReleaseBlockReward(ctx, req.LastCommitInfo.Votes) 23 | 24 | if !totalBlockReward.IsZero() { 25 | k.Logger(ctx).Info( 26 | "released incentives", 27 | "ids", marsutils.UintArrayToString(ids, ","), 28 | "amount", totalBlockReward.String(), 29 | ) 30 | } 31 | 32 | ctx.EventManager().EmitEvent( 33 | sdk.NewEvent( 34 | types.EventTypeIncentivesReleased, 35 | sdk.NewAttribute(types.AttributeKeySchedules, marsutils.UintArrayToString(ids, ",")), 36 | sdk.NewAttribute(sdk.AttributeKeyAmount, totalBlockReward.String()), 37 | ), 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /x/incentives/client/cli/query.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/cosmos/cosmos-sdk/client" 9 | "github.com/cosmos/cosmos-sdk/client/flags" 10 | 11 | "github.com/mars-protocol/hub/v2/x/incentives/types" 12 | ) 13 | 14 | // GetQueryCmd returns the parent command for all incentives module query commands 15 | func GetQueryCmd() *cobra.Command { 16 | cmd := &cobra.Command{ 17 | Use: types.ModuleName, 18 | Short: "Querying commands for the incentives module", 19 | DisableFlagParsing: true, 20 | SuggestionsMinimumDistance: 2, 21 | RunE: client.ValidateCmd, 22 | } 23 | 24 | cmd.AddCommand( 25 | getScheduleCmd(), 26 | getSchedulesCmd(), 27 | ) 28 | 29 | return cmd 30 | } 31 | 32 | func getScheduleCmd() *cobra.Command { 33 | cmd := &cobra.Command{ 34 | Use: "schedule", 35 | Short: "Query an incentives schedule by identifier", 36 | Args: cobra.ExactArgs(1), 37 | RunE: func(cmd *cobra.Command, args []string) error { 38 | clientCtx, err := client.GetClientQueryContext(cmd) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | id, err := strconv.ParseUint(args[0], 10, 64) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | queryClient := types.NewQueryClient(clientCtx) 49 | 50 | res, err := queryClient.Schedule(cmd.Context(), &types.QueryScheduleRequest{Id: id}) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return clientCtx.PrintProto(res) 56 | }, 57 | } 58 | 59 | flags.AddQueryFlagsToCmd(cmd) 60 | 61 | return cmd 62 | } 63 | 64 | func getSchedulesCmd() *cobra.Command { 65 | cmd := &cobra.Command{ 66 | Use: "schedules", 67 | Short: "Query all incentives schedules", 68 | Args: cobra.NoArgs, 69 | RunE: func(cmd *cobra.Command, args []string) error { 70 | clientCtx, err := client.GetClientQueryContext(cmd) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | pageReq, err := client.ReadPageRequest(cmd.Flags()) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | queryClient := types.NewQueryClient(clientCtx) 81 | 82 | res, err := queryClient.Schedules(cmd.Context(), &types.QuerySchedulesRequest{Pagination: pageReq}) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | return clientCtx.PrintProto(res) 88 | }, 89 | } 90 | 91 | flags.AddQueryFlagsToCmd(cmd) 92 | flags.AddPaginationFlagsToCmd(cmd, "schedules") 93 | 94 | return cmd 95 | } 96 | -------------------------------------------------------------------------------- /x/incentives/keeper/genesis.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | 6 | "github.com/mars-protocol/hub/v2/x/incentives/types" 7 | ) 8 | 9 | // InitGenesis initializes the incentives module's storage according to the 10 | // provided genesis state. 11 | // 12 | // NOTE: we call `GetModuleAccount` instead of `SetModuleAccount` because the 13 | // "get" function automatically sets the module account if it doesn't exist. 14 | func (k Keeper) InitGenesis(ctx sdk.Context, gs *types.GenesisState) { 15 | // set module account 16 | k.accountKeeper.GetModuleAccount(ctx, types.ModuleName) 17 | 18 | // set incentives schedules 19 | for _, schedule := range gs.Schedules { 20 | k.SetSchedule(ctx, schedule) 21 | } 22 | 23 | // set next schedule id 24 | k.SetNextScheduleID(ctx, gs.NextScheduleId) 25 | } 26 | 27 | // ExportGenesis returns a genesis state for a given context and keeper 28 | func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { 29 | nextScheduleID := k.GetNextScheduleID(ctx) 30 | 31 | schedules := []types.Schedule{} 32 | k.IterateSchedules(ctx, func(schedule types.Schedule) bool { 33 | schedules = append(schedules, schedule) 34 | return false 35 | }) 36 | 37 | return &types.GenesisState{ 38 | NextScheduleId: nextScheduleID, 39 | Schedules: schedules, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /x/incentives/keeper/genesis_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 9 | 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | 12 | marsapp "github.com/mars-protocol/hub/v2/app" 13 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 14 | ) 15 | 16 | func setupGenesisTest() (ctx sdk.Context, app *marsapp.MarsApp) { 17 | app = marsapptesting.MakeSimpleMockApp() 18 | ctx = app.BaseApp.NewContext(false, tmproto.Header{}) 19 | 20 | app.IncentivesKeeper.InitGenesis(ctx, &mockGenesisState) 21 | 22 | return ctx, app 23 | } 24 | 25 | func TestInitGenesis(t *testing.T) { 26 | ctx, app := setupGenesisTest() 27 | 28 | nextScheduleId := app.IncentivesKeeper.GetNextScheduleID(ctx) 29 | require.Equal(t, uint64(3), nextScheduleId) 30 | 31 | for _, mockSchedule := range mockSchedules { 32 | schedule, found := app.IncentivesKeeper.GetSchedule(ctx, mockSchedule.Id) 33 | require.True(t, found) 34 | require.Equal(t, mockSchedule.Id, schedule.Id) 35 | require.Equal(t, mockSchedule.TotalAmount, schedule.TotalAmount) 36 | } 37 | } 38 | 39 | func TestExportGenesis(t *testing.T) { 40 | ctx, app := setupGenesisTest() 41 | 42 | exported := app.IncentivesKeeper.ExportGenesis(ctx) 43 | require.Equal(t, mockGenesisState.NextScheduleId, exported.NextScheduleId) 44 | for idx := range mockSchedules { 45 | require.Equal(t, mockSchedules[idx].Id, exported.Schedules[idx].Id) 46 | require.Equal(t, mockSchedules[idx].TotalAmount, exported.Schedules[idx].TotalAmount) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /x/incentives/keeper/invariants.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | 8 | "github.com/mars-protocol/hub/v2/x/incentives/types" 9 | ) 10 | 11 | // RegisterInvariants registers the incentives module invariants 12 | func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { 13 | ir.RegisterRoute(types.ModuleName, "total-unreleased-incentives", TotalUnreleasedIncentives(k)) 14 | } 15 | 16 | // TotalUnreleasedIncentives asserts that the incentives module's coin balances 17 | // match exactly the total amount of unreleased incentives. 18 | func TotalUnreleasedIncentives(k Keeper) sdk.Invariant { 19 | return func(ctx sdk.Context) (string, bool) { 20 | expectedTotal := sdk.NewCoins() 21 | k.IterateSchedules(ctx, func(schedule types.Schedule) bool { 22 | expectedTotal = expectedTotal.Add(schedule.TotalAmount.Sub(schedule.ReleasedAmount...)...) 23 | return false 24 | }) 25 | 26 | maccAddr := k.GetModuleAddress() 27 | actualTotal := k.bankKeeper.GetAllBalances(ctx, maccAddr) 28 | 29 | // NOTE: the actual amount does not necessarily need to be _exactly_ 30 | // equal the expected amount. we allow it as long as it's all greater or 31 | // equal than expected. 32 | // 33 | // the reason is- if we assert exact equality, then anyone can cause the 34 | // invariance to break (and hence halt the chain) by sending some coins 35 | // to the incentives module account. 36 | broken := !actualTotal.IsAllGTE(expectedTotal) 37 | 38 | msg := sdk.FormatInvariant( 39 | types.ModuleName, 40 | "total-unreleased-incentives", 41 | fmt.Sprintf("\tsum of unreleased incentives: %s\n\tmodule account balances: %s", expectedTotal.String(), actualTotal.String()), 42 | ) 43 | 44 | return msg, broken 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /x/incentives/keeper/invariants_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 9 | 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 12 | 13 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 14 | 15 | "github.com/mars-protocol/hub/v2/x/incentives/keeper" 16 | ) 17 | 18 | func TestUnreleasedIncentivesInvariant(t *testing.T) { 19 | app := marsapptesting.MakeSimpleMockApp() 20 | ctx := app.BaseApp.NewContext(false, tmproto.Header{}) 21 | 22 | for _, mockSchedule := range mockSchedulesReleased { 23 | app.IncentivesKeeper.SetSchedule(ctx, mockSchedule) 24 | } 25 | 26 | invariant := keeper.TotalUnreleasedIncentives(app.IncentivesKeeper) 27 | 28 | // set incorrect balances for the incentives module account 29 | maccAddr := app.IncentivesKeeper.GetModuleAddress() 30 | app.BankKeeper.InitGenesis( 31 | ctx, 32 | &banktypes.GenesisState{ 33 | Balances: []banktypes.Balance{{ 34 | Address: maccAddr.String(), 35 | Coins: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(123)), sdk.NewCoin("uastro", sdk.NewInt(456))), 36 | }}, 37 | }, 38 | ) 39 | 40 | msg, broken := invariant(ctx) 41 | require.True(t, broken) 42 | require.Equal( 43 | t, 44 | `incentives: total-unreleased-incentives invariant 45 | sum of unreleased incentives: 7192uastro,8637umars 46 | module account balances: 456uastro,123umars 47 | `, 48 | msg, 49 | ) 50 | 51 | // set the correct balances for the incentives module account 52 | app.BankKeeper.InitGenesis( 53 | ctx, 54 | &banktypes.GenesisState{ 55 | Balances: []banktypes.Balance{{ 56 | Address: maccAddr.String(), 57 | Coins: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(8637)), sdk.NewCoin("uastro", sdk.NewInt(7192))), 58 | }}, 59 | }, 60 | ) 61 | 62 | _, broken = invariant(ctx) 63 | require.False(t, broken) 64 | } 65 | -------------------------------------------------------------------------------- /x/incentives/keeper/keeper.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tendermint/tendermint/libs/log" 7 | 8 | "github.com/cosmos/cosmos-sdk/codec" 9 | "github.com/cosmos/cosmos-sdk/store/prefix" 10 | storetypes "github.com/cosmos/cosmos-sdk/store/types" 11 | sdk "github.com/cosmos/cosmos-sdk/types" 12 | 13 | "github.com/mars-protocol/hub/v2/x/incentives/types" 14 | ) 15 | 16 | // Keeper is the incentives module's keeper 17 | type Keeper struct { 18 | cdc codec.BinaryCodec 19 | storeKey storetypes.StoreKey 20 | 21 | accountKeeper types.AccountKeeper 22 | bankKeeper types.BankKeeper 23 | distrKeeper types.DistrKeeper 24 | stakingKeeper types.StakingKeeper 25 | 26 | authority string 27 | } 28 | 29 | // NewKeeper creates a new incentives module keeper 30 | func NewKeeper( 31 | cdc codec.BinaryCodec, storeKey storetypes.StoreKey, accountKeeper types.AccountKeeper, 32 | bankKeeper types.BankKeeper, distrKeeper types.DistrKeeper, stakingKeeper types.StakingKeeper, 33 | authority string, 34 | ) Keeper { 35 | // ensure incentives module account is set 36 | if addr := accountKeeper.GetModuleAddress(types.ModuleName); addr == nil { 37 | panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) 38 | } 39 | 40 | return Keeper{ 41 | cdc: cdc, 42 | storeKey: storeKey, 43 | accountKeeper: accountKeeper, 44 | bankKeeper: bankKeeper, 45 | distrKeeper: distrKeeper, 46 | stakingKeeper: stakingKeeper, 47 | authority: authority, 48 | } 49 | } 50 | 51 | // Logger returns a module-specific logger. 52 | func (k Keeper) Logger(ctx sdk.Context) log.Logger { 53 | return ctx.Logger().With("module", "x/"+types.ModuleName) 54 | } 55 | 56 | // GetModuleAddress returns the incentives module account's address 57 | func (k Keeper) GetModuleAddress() sdk.AccAddress { 58 | return k.accountKeeper.GetModuleAddress(types.ModuleName) 59 | } 60 | 61 | //------------------------------------------------------------------------------ 62 | // ScheduleId 63 | //------------------------------------------------------------------------------ 64 | 65 | // GetNextScheduleId loads the next schedule id if a new schedule is to be 66 | // created. 67 | // 68 | // NOTE: the id should have been initialized in genesis, so it being undefined 69 | // is a fatal error. we have the module panic in this case, instead of returning 70 | // an error. 71 | func (k Keeper) GetNextScheduleID(ctx sdk.Context) uint64 { 72 | store := ctx.KVStore(k.storeKey) 73 | 74 | bz := store.Get(types.KeyNextScheduleID) 75 | if bz == nil { 76 | panic("stored next schedule id should not have been nil") 77 | } 78 | 79 | return sdk.BigEndianToUint64(bz) 80 | } 81 | 82 | // SetNextScheduleId sets the next schedule id to the provided value 83 | func (k Keeper) SetNextScheduleID(ctx sdk.Context, id uint64) { 84 | store := ctx.KVStore(k.storeKey) 85 | store.Set(types.KeyNextScheduleID, sdk.Uint64ToBigEndian(id)) 86 | } 87 | 88 | // IncrementNextScheduleId increases the next id by one, and returns the 89 | // previous value. 90 | func (k Keeper) IncrementNextScheduleID(ctx sdk.Context) uint64 { 91 | id := k.GetNextScheduleID(ctx) 92 | 93 | k.SetNextScheduleID(ctx, id+1) 94 | 95 | return id 96 | } 97 | 98 | //------------------------------------------------------------------------------ 99 | // Schedule 100 | //------------------------------------------------------------------------------ 101 | 102 | // GetSchedule loads the incentives schedule of the specified id 103 | func (k Keeper) GetSchedule(ctx sdk.Context, id uint64) (schedule types.Schedule, found bool) { 104 | store := ctx.KVStore(k.storeKey) 105 | 106 | bz := store.Get(types.GetScheduleKey(id)) 107 | if bz == nil { 108 | return schedule, false 109 | } 110 | 111 | k.cdc.MustUnmarshal(bz, &schedule) 112 | 113 | return schedule, true 114 | } 115 | 116 | // SetSchedule saves the provided incentives schedule to store 117 | func (k Keeper) SetSchedule(ctx sdk.Context, schedule types.Schedule) { 118 | store := ctx.KVStore(k.storeKey) 119 | store.Set(types.GetScheduleKey(schedule.Id), k.cdc.MustMarshal(&schedule)) 120 | } 121 | 122 | // IterateSchedules iterates over all active schedules, calling the callback 123 | // function with the schedule info. 124 | // The iteration stops if the callback returns false. 125 | func (k Keeper) IterateSchedules(ctx sdk.Context, cb func(types.Schedule) bool) { 126 | store := ctx.KVStore(k.storeKey) 127 | iterator := sdk.KVStorePrefixIterator(store, types.KeySchedule) 128 | 129 | defer iterator.Close() 130 | for ; iterator.Valid(); iterator.Next() { 131 | var schedule types.Schedule 132 | k.cdc.MustUnmarshal(iterator.Value(), &schedule) 133 | 134 | if cb(schedule) { 135 | break 136 | } 137 | } 138 | } 139 | 140 | // DeleteSchedule removes the incentives schedule of the given id from module 141 | // store. 142 | func (k Keeper) DeleteSchedule(ctx sdk.Context, id uint64) { 143 | store := ctx.KVStore(k.storeKey) 144 | store.Delete(types.GetScheduleKey(id)) 145 | } 146 | 147 | // GetSchedulePrefixStore returns a prefix store of all schedules 148 | func (k Keeper) GetSchedulePrefixStore(ctx sdk.Context) prefix.Store { 149 | store := ctx.KVStore(k.storeKey) 150 | return prefix.NewStore(store, types.KeySchedule) 151 | } 152 | -------------------------------------------------------------------------------- /x/incentives/keeper/keeper_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "time" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | 8 | "github.com/mars-protocol/hub/v2/x/incentives/types" 9 | ) 10 | 11 | // there is no unit tests for keeper.go 12 | // 13 | // we simply define the mock variables here, while can be used by other test files 14 | 15 | var mockGenesisState = types.GenesisState{ 16 | NextScheduleId: 3, 17 | Schedules: mockSchedules, 18 | } 19 | 20 | var mockSchedules = []types.Schedule{{ 21 | Id: 1, 22 | StartTime: time.Unix(10000, 0), 23 | EndTime: time.Unix(20000, 0), 24 | TotalAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(12345)), sdk.NewCoin("uastro", sdk.NewInt(69420))), 25 | ReleasedAmount: sdk.NewCoins(), 26 | }, { 27 | Id: 2, 28 | StartTime: time.Unix(15000, 0), 29 | EndTime: time.Unix(30000, 0), 30 | TotalAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(10000))), 31 | ReleasedAmount: sdk.NewCoins(), 32 | }} 33 | 34 | var mockSchedulesReleased = []types.Schedule{{ 35 | Id: 1, 36 | StartTime: time.Unix(10000, 0), 37 | EndTime: time.Unix(20000, 0), 38 | TotalAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(12345)), sdk.NewCoin("uastro", sdk.NewInt(69420))), 39 | ReleasedAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(11066)), sdk.NewCoin("uastro", sdk.NewInt(62228))), 40 | }, { 41 | Id: 2, 42 | StartTime: time.Unix(15000, 0), 43 | EndTime: time.Unix(30000, 0), 44 | TotalAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(10000))), 45 | ReleasedAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(2642))), 46 | }} 47 | -------------------------------------------------------------------------------- /x/incentives/keeper/msg_server.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "context" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | 8 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 9 | 10 | marsutils "github.com/mars-protocol/hub/v2/utils" 11 | 12 | "github.com/mars-protocol/hub/v2/x/incentives/types" 13 | ) 14 | 15 | type msgServer struct{ k Keeper } 16 | 17 | // NewMsgServerImpl creates an implementation of the `MsgServer` interface for 18 | // the given keeper. 19 | func NewMsgServerImpl(k Keeper) types.MsgServer { 20 | return &msgServer{k} 21 | } 22 | 23 | func (ms msgServer) CreateSchedule(goCtx context.Context, req *types.MsgCreateSchedule) (*types.MsgCreateScheduleResponse, error) { 24 | ctx := sdk.UnwrapSDKContext(goCtx) 25 | 26 | if req.Authority != ms.k.authority { 27 | return nil, govtypes.ErrInvalidSigner.Wrapf("expected %s got %s", ms.k.authority, req.Authority) 28 | } 29 | 30 | schedule, err := ms.k.CreateSchedule(ctx, req.StartTime, req.EndTime, req.Amount) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | ms.k.Logger(ctx).Info( 36 | "incentives schedule created", 37 | "id", schedule.Id, 38 | "amount", schedule.TotalAmount.String(), 39 | "startTime", schedule.StartTime.String(), 40 | "endTime", schedule.EndTime.String(), 41 | ) 42 | 43 | return &types.MsgCreateScheduleResponse{}, nil 44 | } 45 | 46 | func (ms msgServer) TerminateSchedules(goCtx context.Context, req *types.MsgTerminateSchedules) (*types.MsgTerminateSchedulesResponse, error) { 47 | ctx := sdk.UnwrapSDKContext(goCtx) 48 | 49 | if req.Authority != ms.k.authority { 50 | return nil, govtypes.ErrInvalidSigner.Wrapf("expected %s got %s", ms.k.authority, req.Authority) 51 | } 52 | 53 | amount, err := ms.k.TerminateSchedules(ctx, req.Ids) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | ms.k.Logger(ctx).Info( 59 | "incentives schedule terminated", 60 | "ids", marsutils.UintArrayToString(req.Ids, ","), 61 | "refundedAmount", amount.String(), 62 | ) 63 | 64 | return &types.MsgTerminateSchedulesResponse{RefundedAmount: amount}, nil 65 | } 66 | -------------------------------------------------------------------------------- /x/incentives/keeper/msg_server_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 10 | 11 | sdk "github.com/cosmos/cosmos-sdk/types" 12 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 13 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 14 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 15 | 16 | marsapp "github.com/mars-protocol/hub/v2/app" 17 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 18 | 19 | "github.com/mars-protocol/hub/v2/x/incentives/keeper" 20 | "github.com/mars-protocol/hub/v2/x/incentives/types" 21 | ) 22 | 23 | var mockSchedule = types.Schedule{ 24 | Id: 1, 25 | StartTime: time.Unix(10000, 0), 26 | EndTime: time.Unix(20000, 0), 27 | TotalAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(12345)), sdk.NewCoin("uastro", sdk.NewInt(69420))), 28 | ReleasedAmount: sdk.Coins(nil), 29 | } 30 | 31 | const ( 32 | govModuleAccount = "mars10d07y265gmmuvt4z0w9aw880jnsr700j8l2urg" 33 | notGovModuleAccount = "mars1z926ax906k0ycsuckele6x5hh66e2m4m09whw6" 34 | ) 35 | 36 | func init() { 37 | sdk.GetConfig().SetBech32PrefixForAccount("mars", "marspub") 38 | } 39 | 40 | func setupMsgServerTest() (ctx sdk.Context, app *marsapp.MarsApp) { 41 | accts := marsapptesting.MakeRandomAccounts(1) 42 | maccAddr := authtypes.NewModuleAddress(types.ModuleName) 43 | 44 | // we give sufficient token amounts to both the community pool and 45 | // incentives module account, so that we can both create or terminate 46 | // schedules. 47 | app = marsapptesting.MakeMockApp( 48 | accts, 49 | []banktypes.Balance{{ 50 | Address: maccAddr.String(), 51 | Coins: mockSchedule.TotalAmount, 52 | }}, 53 | accts, 54 | mockSchedule.TotalAmount, 55 | ) 56 | ctx = app.BaseApp.NewContext(false, tmproto.Header{Time: time.Unix(16667, 0)}) 57 | 58 | return ctx, app 59 | } 60 | 61 | func TestCreateScheduleProposalPassed(t *testing.T) { 62 | ctx, app := setupMsgServerTest() 63 | 64 | msgServer := keeper.NewMsgServerImpl(app.IncentivesKeeper) 65 | req := &types.MsgCreateSchedule{ 66 | Authority: govModuleAccount, 67 | StartTime: mockSchedule.StartTime, 68 | EndTime: mockSchedule.EndTime, 69 | Amount: mockSchedule.TotalAmount, 70 | } 71 | _, err := msgServer.CreateSchedule(ctx, req) 72 | require.NoError(t, err) 73 | 74 | _, found := app.IncentivesKeeper.GetSchedule(ctx, 1) 75 | require.True(t, found) 76 | } 77 | 78 | func TestTerminateSchedulesProposalPassed(t *testing.T) { 79 | ctx, app := setupMsgServerTest() 80 | 81 | app.IncentivesKeeper.SetSchedule(ctx, mockSchedule) 82 | 83 | msgServer := keeper.NewMsgServerImpl(app.IncentivesKeeper) 84 | req := &types.MsgTerminateSchedules{ 85 | Authority: govModuleAccount, 86 | Ids: []uint64{1}, 87 | } 88 | _, err := msgServer.TerminateSchedules(ctx, req) 89 | require.NoError(t, err) 90 | 91 | _, found := app.IncentivesKeeper.GetSchedule(ctx, 1) 92 | require.False(t, found) 93 | } 94 | 95 | func TestNotAuthority(t *testing.T) { 96 | ctx, app := setupMsgServerTest() 97 | 98 | msgServer := keeper.NewMsgServerImpl(app.IncentivesKeeper) 99 | req := &types.MsgCreateSchedule{ 100 | Authority: notGovModuleAccount, 101 | StartTime: mockSchedule.StartTime, 102 | EndTime: mockSchedule.EndTime, 103 | Amount: mockSchedule.TotalAmount, 104 | } 105 | _, err := msgServer.CreateSchedule(ctx, req) 106 | require.Error(t, err, govtypes.ErrInvalidSigner) 107 | } 108 | -------------------------------------------------------------------------------- /x/incentives/keeper/query_server.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc/codes" 7 | "google.golang.org/grpc/status" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 11 | query "github.com/cosmos/cosmos-sdk/types/query" 12 | 13 | "github.com/mars-protocol/hub/v2/x/incentives/types" 14 | ) 15 | 16 | type queryServer struct{ k Keeper } 17 | 18 | // NewQueryServerImpl creates an implementation of the `QueryServer` interface 19 | // for the given keeper. 20 | func NewQueryServerImpl(k Keeper) types.QueryServer { 21 | return &queryServer{k} 22 | } 23 | 24 | func (qs queryServer) Schedule(goCtx context.Context, req *types.QueryScheduleRequest) (*types.QueryScheduleResponse, error) { 25 | if req == nil { 26 | return nil, status.Error(codes.InvalidArgument, "empty request") 27 | } 28 | 29 | ctx := sdk.UnwrapSDKContext(goCtx) 30 | 31 | schedule, found := qs.k.GetSchedule(ctx, req.Id) 32 | if !found { 33 | return nil, sdkerrors.ErrNotFound.Wrapf("incentives schedule not found for id %d", req.Id) 34 | } 35 | 36 | return &types.QueryScheduleResponse{Schedule: schedule}, nil 37 | } 38 | 39 | func (qs queryServer) Schedules(goCtx context.Context, req *types.QuerySchedulesRequest) (*types.QuerySchedulesResponse, error) { 40 | if req == nil { 41 | return nil, status.Error(codes.InvalidArgument, "empty request") 42 | } 43 | 44 | ctx := sdk.UnwrapSDKContext(goCtx) 45 | 46 | schedules := []types.Schedule{} 47 | 48 | pageRes, err := query.Paginate(qs.k.GetSchedulePrefixStore(ctx), req.Pagination, func(_, value []byte) error { 49 | var schedule types.Schedule 50 | err := qs.k.cdc.Unmarshal(value, &schedule) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | schedules = append(schedules, schedule) 56 | 57 | return nil 58 | }) 59 | if err != nil { 60 | return nil, status.Errorf(codes.InvalidArgument, "paginate: %v", err) 61 | } 62 | 63 | return &types.QuerySchedulesResponse{Schedules: schedules, Pagination: pageRes}, nil 64 | } 65 | -------------------------------------------------------------------------------- /x/incentives/keeper/query_server_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 9 | 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | "github.com/cosmos/cosmos-sdk/types/query" 12 | 13 | marsapp "github.com/mars-protocol/hub/v2/app" 14 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 15 | 16 | "github.com/mars-protocol/hub/v2/x/incentives/keeper" 17 | "github.com/mars-protocol/hub/v2/x/incentives/types" 18 | ) 19 | 20 | func setupQueryServerTest() (ctx sdk.Context, app *marsapp.MarsApp) { 21 | app = marsapptesting.MakeSimpleMockApp() 22 | ctx = app.BaseApp.NewContext(false, tmproto.Header{}) 23 | 24 | for _, schedule := range mockSchedules { 25 | app.IncentivesKeeper.SetSchedule(ctx, schedule) 26 | } 27 | 28 | return ctx, app 29 | } 30 | 31 | func TestEmptyQuery(t *testing.T) { 32 | ctx, app := setupQueryServerTest() 33 | 34 | queryServer := keeper.NewQueryServerImpl(app.IncentivesKeeper) 35 | 36 | _, err := queryServer.Schedule(sdk.WrapSDKContext(ctx), nil) 37 | require.Errorf(t, err, "empty request") 38 | 39 | _, err = queryServer.Schedules(sdk.WrapSDKContext(ctx), nil) 40 | require.Errorf(t, err, "empty request") 41 | } 42 | 43 | func TestQuerySchedule(t *testing.T) { 44 | ctx, app := setupQueryServerTest() 45 | 46 | queryServer := keeper.NewQueryServerImpl(app.IncentivesKeeper) 47 | 48 | // NOTE: the mock schedules use `time.UTC` while the schedules in the responses use `time.Local`. 49 | // i can't figure out how to enforce the response to return UTC, so here we just compare the id 50 | // and total amount, and skip the times 51 | for _, mockSchedule := range mockSchedules { 52 | res, err := queryServer.Schedule(sdk.WrapSDKContext(ctx), &types.QueryScheduleRequest{Id: mockSchedule.Id}) 53 | require.NoError(t, err) 54 | require.Equal(t, res.Schedule.Id, mockSchedule.Id) 55 | require.Equal(t, res.Schedule.TotalAmount, mockSchedule.TotalAmount) 56 | } 57 | } 58 | 59 | func TestQuerySchedules(t *testing.T) { 60 | ctx, app := setupQueryServerTest() 61 | 62 | queryServer := keeper.NewQueryServerImpl(app.IncentivesKeeper) 63 | 64 | pageReq := &query.PageRequest{ 65 | Key: nil, 66 | Limit: 1, 67 | CountTotal: false, 68 | } 69 | res, err := queryServer.Schedules(sdk.WrapSDKContext(ctx), &types.QuerySchedulesRequest{Pagination: pageReq}) 70 | require.NoError(t, err) 71 | require.Equal(t, 1, len(res.Schedules)) 72 | 73 | pageReq = &query.PageRequest{ 74 | Key: res.Pagination.NextKey, 75 | Limit: 1, 76 | CountTotal: false, 77 | } 78 | res, err = queryServer.Schedules(sdk.WrapSDKContext(ctx), &types.QuerySchedulesRequest{Pagination: pageReq}) 79 | require.NoError(t, err) 80 | require.Equal(t, 1, len(res.Schedules)) 81 | } 82 | -------------------------------------------------------------------------------- /x/incentives/keeper/reward.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | abci "github.com/tendermint/tendermint/abci/types" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | 8 | distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" 9 | 10 | "github.com/mars-protocol/hub/v2/x/incentives/types" 11 | ) 12 | 13 | func newDecFromInt64(i int64) sdk.Dec { 14 | return sdk.NewDecFromInt(sdk.NewInt(i)) 15 | } 16 | 17 | // ReleaseBlockReward handles the release of incentives. Returns the total 18 | // amount of block reward released and the list of relevant schedule ids. 19 | // 20 | // `bondedVotes` is a list of {validator address, validator voted on last block 21 | // flag} for all validators in the bonded set. 22 | func (k Keeper) ReleaseBlockReward(ctx sdk.Context, bondedVotes []abci.VoteInfo) (ids []uint64, totalBlockReward sdk.Coins) { 23 | currentTime := ctx.BlockTime() 24 | 25 | // iterate through all active schedules, sum up all rewards to be released 26 | // in this block. 27 | // 28 | // If an incentives schedule has been fully released, delete it from the 29 | // store; otherwise, update the released amount and save 30 | ids = []uint64{} 31 | totalBlockReward = sdk.NewCoins() 32 | k.IterateSchedules(ctx, func(schedule types.Schedule) bool { 33 | blockReward := schedule.GetBlockReward(currentTime) 34 | 35 | if !blockReward.Empty() { 36 | ids = append(ids, schedule.Id) 37 | totalBlockReward = totalBlockReward.Add(blockReward...) 38 | } 39 | 40 | if currentTime.After(schedule.EndTime) { 41 | k.DeleteSchedule(ctx, schedule.Id) 42 | } else { 43 | schedule.ReleasedAmount = schedule.ReleasedAmount.Add(blockReward...) 44 | k.SetSchedule(ctx, schedule) 45 | } 46 | 47 | return false 48 | }) 49 | 50 | // exit here if there is no coin to be released 51 | if totalBlockReward.Empty() { 52 | return 53 | } 54 | 55 | // transfer the coins to distribution module account so that they can be 56 | // distributed 57 | err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, distrtypes.ModuleName, totalBlockReward) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | // sum up the total voting power voted in the last block 63 | // 64 | // NOTE: The following code is copied from cosmos-sdk's distribution module 65 | // without change. 66 | // Here the distr module adds up voting power of _all_ validators without 67 | // checking whether the validator has signed the previous block or not. 68 | // In other words, there is no "micro-slashing" for missing single blocks. 69 | // We keep this behavior without change. 70 | // More on this issue: https://twitter.com/larry0x/status/1588189416257880064 71 | totalPower := sdk.ZeroDec() 72 | for _, vote := range bondedVotes { 73 | totalPower = totalPower.Add(newDecFromInt64(vote.Validator.Power)) 74 | } 75 | 76 | // allocate reward to validator who have signed the previous block, pro-rata 77 | // to their voting power 78 | // 79 | // NOTE: AllocateTokensToValidator emits the `reward` event, so we don't 80 | // need to emit separate events 81 | totalBlockRewardDec := sdk.NewDecCoinsFromCoins(totalBlockReward...) 82 | for _, vote := range bondedVotes { 83 | validator := k.stakingKeeper.ValidatorByConsAddr(ctx, vote.Validator.Address) 84 | 85 | power := newDecFromInt64(vote.Validator.Power) 86 | reward := totalBlockRewardDec.MulDec(power).QuoDec(totalPower) 87 | 88 | totalPower = totalPower.Sub(power) 89 | totalBlockRewardDec = totalBlockRewardDec.Sub(reward) 90 | 91 | k.distrKeeper.AllocateTokensToValidator(ctx, validator, reward) 92 | } 93 | 94 | return ids, totalBlockReward 95 | } 96 | -------------------------------------------------------------------------------- /x/incentives/keeper/schedule.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "time" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 8 | 9 | "github.com/mars-protocol/hub/v2/x/incentives/types" 10 | ) 11 | 12 | // CreateSchedule upon a successful CreateIncentivesScheduleProposal, withdraws 13 | // appropriate amount of funds from the community pool, and initializes a new 14 | // schedule in module store. Returns the new schedule that was created. 15 | func (k Keeper) CreateSchedule(ctx sdk.Context, startTime, endTime time.Time, amount sdk.Coins) (schedule types.Schedule, err error) { 16 | id := k.IncrementNextScheduleID(ctx) 17 | 18 | schedule = types.Schedule{ 19 | Id: id, 20 | StartTime: startTime, 21 | EndTime: endTime, 22 | TotalAmount: amount, 23 | ReleasedAmount: sdk.NewCoins(), 24 | } 25 | 26 | k.SetSchedule(ctx, schedule) 27 | 28 | maccAddr := k.GetModuleAddress() 29 | if err := k.distrKeeper.DistributeFromFeePool(ctx, amount, maccAddr); err != nil { 30 | return types.Schedule{}, types.ErrFailedWithdrawFromCommunityPool.Wrap(err.Error()) 31 | } 32 | 33 | return schedule, nil 34 | } 35 | 36 | // TerminateSchedules upon a successful TerminateIncentivesScheduleProposal, 37 | // deletes the schedules specified by the proposal from module store, and 38 | // returns the unreleased funds to the community pool. Returns the funds that 39 | // ware returned. 40 | func (k Keeper) TerminateSchedules(ctx sdk.Context, ids []uint64) (amount sdk.Coins, err error) { 41 | amount = sdk.NewCoins() 42 | 43 | for _, id := range ids { 44 | schedule, found := k.GetSchedule(ctx, id) 45 | if !found { 46 | return sdk.NewCoins(), sdkerrors.ErrKeyNotFound.Wrapf("incentives schedule with id %d does not exist", id) 47 | } 48 | 49 | amount = amount.Add(schedule.TotalAmount.Sub(schedule.ReleasedAmount...)...) 50 | 51 | k.DeleteSchedule(ctx, id) 52 | } 53 | 54 | maccAddr := k.GetModuleAddress() 55 | if err := k.distrKeeper.FundCommunityPool(ctx, amount, maccAddr); err != nil { 56 | return sdk.NewCoins(), types.ErrFailedRefundToCommunityPool.Wrap(err.Error()) 57 | } 58 | 59 | return amount, nil 60 | } 61 | -------------------------------------------------------------------------------- /x/incentives/keeper/schedule_test.go: -------------------------------------------------------------------------------- 1 | package keeper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 9 | 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 12 | banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" 13 | distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" 14 | 15 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 16 | 17 | "github.com/mars-protocol/hub/v2/x/incentives/types" 18 | ) 19 | 20 | func TestCreateSchedule(t *testing.T) { 21 | accts := marsapptesting.MakeRandomAccounts(1) 22 | maccAddr := authtypes.NewModuleAddress(types.ModuleName) 23 | 24 | // schedule 1 is already started, balance is held by incentives module acct 25 | // schedule 2 is not started yet, balance is held by community pool 26 | app := marsapptesting.MakeMockApp( 27 | accts, 28 | []banktypes.Balance{{ 29 | Address: maccAddr.String(), 30 | Coins: mockSchedules[0].TotalAmount, 31 | }}, 32 | accts, 33 | mockSchedules[1].TotalAmount, 34 | ) 35 | ctx := app.BaseApp.NewContext(false, tmproto.Header{}) 36 | 37 | // assume we already have mockSchedule[0] active; 38 | // a successful gov proposal about to add mockSchedule[1] 39 | app.IncentivesKeeper.SetNextScheduleID(ctx, 2) 40 | app.IncentivesKeeper.SetSchedule(ctx, mockSchedules[0]) 41 | 42 | // create the schedule upon a successful governance proposal 43 | _, err := app.IncentivesKeeper.CreateSchedule( 44 | ctx, 45 | mockSchedules[1].StartTime, 46 | mockSchedules[1].EndTime, 47 | mockSchedules[1].TotalAmount, 48 | ) 49 | require.NoError(t, err) 50 | 51 | // next schedule id should have been updated 52 | nextScheduleId := app.IncentivesKeeper.GetNextScheduleID(ctx) 53 | require.Equal(t, uint64(3), nextScheduleId) 54 | 55 | // the new schedule should have been saved 56 | schedule, found := app.IncentivesKeeper.GetSchedule(ctx, 2) 57 | require.True(t, found) 58 | require.Equal(t, mockSchedules[1].Id, schedule.Id) 59 | require.Equal(t, mockSchedules[1].TotalAmount, schedule.TotalAmount) 60 | 61 | // the incentives module account should have been funded 62 | balances := app.BankKeeper.GetAllBalances(ctx, maccAddr) 63 | expectedBalances := mockSchedules[0].TotalAmount.Add(mockSchedules[1].TotalAmount...) 64 | require.Equal(t, expectedBalances, balances) 65 | 66 | // the distribution module account should have been deducted balances 67 | balances = app.BankKeeper.GetAllBalances(ctx, app.AccountKeeper.GetModuleAddress(distrtypes.ModuleName)) 68 | require.Equal(t, sdk.NewCoins(), balances) 69 | 70 | // the fee pool should have been properly updated 71 | feePool := app.DistrKeeper.GetFeePool(ctx) 72 | require.Equal(t, sdk.DecCoins(nil), feePool.CommunityPool) 73 | } 74 | 75 | func TestTerminateSchedule(t *testing.T) { 76 | accts := marsapptesting.MakeRandomAccounts(1) 77 | maccAddr := authtypes.NewModuleAddress(types.ModuleName) 78 | 79 | // for this test case, we assume there are a few ongoing incentives programs. 80 | // compute what should be the remaining balance of the incentives module 81 | // accounts. 82 | amount := sdk.NewCoins() 83 | for _, mockSchedule := range mockSchedulesReleased { 84 | amount = amount.Add(mockSchedule.TotalAmount...).Sub(mockSchedule.ReleasedAmount...) 85 | } 86 | 87 | app := marsapptesting.MakeMockApp( 88 | accts, 89 | []banktypes.Balance{{ 90 | Address: maccAddr.String(), 91 | Coins: amount, 92 | }}, 93 | accts, 94 | sdk.NewCoins(), 95 | ) 96 | ctx := app.BaseApp.NewContext(false, tmproto.Header{}) 97 | 98 | app.IncentivesKeeper.SetNextScheduleID(ctx, 3) 99 | for _, mockSchedule := range mockSchedulesReleased { 100 | app.IncentivesKeeper.SetSchedule(ctx, mockSchedule) 101 | } 102 | 103 | // terminate the schedules upon a successful governance proposal 104 | _, err := app.IncentivesKeeper.TerminateSchedules(ctx, []uint64{1, 2}) 105 | require.NoError(t, err) 106 | 107 | // next schedule id should have not been changed 108 | nextScheduleId := app.IncentivesKeeper.GetNextScheduleID(ctx) 109 | require.Equal(t, uint64(3), nextScheduleId) 110 | 111 | // the two schedules should have been deleted 112 | _, found := app.IncentivesKeeper.GetSchedule(ctx, 1) 113 | require.False(t, found) 114 | _, found = app.IncentivesKeeper.GetSchedule(ctx, 2) 115 | require.False(t, found) 116 | 117 | // the incentives module account should have been deducted balance 118 | balances := app.BankKeeper.GetAllBalances(ctx, maccAddr) 119 | require.Equal(t, sdk.NewCoins(), balances) 120 | 121 | // the distribution module account should have been funded 122 | balances = app.BankKeeper.GetAllBalances(ctx, app.AccountKeeper.GetModuleAddress(distrtypes.ModuleName)) 123 | require.Equal(t, amount, balances) 124 | 125 | // the fee pool should have been properly updated 126 | feePool := app.DistrKeeper.GetFeePool(ctx) 127 | require.Equal(t, sdk.NewDecCoinsFromCoins(amount...), feePool.CommunityPool) 128 | } 129 | -------------------------------------------------------------------------------- /x/incentives/module.go: -------------------------------------------------------------------------------- 1 | package incentives 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 10 | "github.com/spf13/cobra" 11 | 12 | abci "github.com/tendermint/tendermint/abci/types" 13 | 14 | "github.com/cosmos/cosmos-sdk/client" 15 | "github.com/cosmos/cosmos-sdk/codec" 16 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 17 | sdk "github.com/cosmos/cosmos-sdk/types" 18 | "github.com/cosmos/cosmos-sdk/types/module" 19 | 20 | "github.com/mars-protocol/hub/v2/x/incentives/client/cli" 21 | "github.com/mars-protocol/hub/v2/x/incentives/keeper" 22 | "github.com/mars-protocol/hub/v2/x/incentives/types" 23 | ) 24 | 25 | var ( 26 | _ module.AppModule = AppModule{} 27 | _ module.AppModuleBasic = AppModuleBasic{} 28 | ) 29 | 30 | //------------------------------------------------------------------------------ 31 | // AppModuleBasic 32 | //------------------------------------------------------------------------------ 33 | 34 | // AppModuleBasic defines the basic application module used by the module 35 | type AppModuleBasic struct{} 36 | 37 | func (AppModuleBasic) Name() string { 38 | return types.ModuleName 39 | } 40 | 41 | func (AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) { 42 | types.RegisterInterfaces(registry) 43 | } 44 | 45 | func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { 46 | return cdc.MustMarshalJSON(types.DefaultGenesisState()) 47 | } 48 | 49 | func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { 50 | var gs types.GenesisState 51 | if err := cdc.UnmarshalJSON(bz, &gs); err != nil { 52 | return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) 53 | } 54 | 55 | return gs.Validate() 56 | } 57 | 58 | func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { 59 | if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil { 60 | panic(err) 61 | } 62 | } 63 | 64 | func (AppModuleBasic) GetTxCmd() *cobra.Command { 65 | return nil 66 | } 67 | 68 | func (AppModuleBasic) GetQueryCmd() *cobra.Command { 69 | return cli.GetQueryCmd() 70 | } 71 | 72 | //------------------------------------------------------------------------------ 73 | // AppModule 74 | //------------------------------------------------------------------------------ 75 | 76 | // AppModule implements an application module for the safety fund module 77 | type AppModule struct { 78 | AppModuleBasic 79 | 80 | keeper keeper.Keeper 81 | } 82 | 83 | // NewAppModule creates a new AppModule object 84 | func NewAppModule(keeper keeper.Keeper) AppModule { 85 | return AppModule{AppModuleBasic{}, keeper} 86 | } 87 | 88 | func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) { 89 | } 90 | 91 | func (am AppModule) RegisterServices(cfg module.Configurator) { 92 | types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) 93 | types.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServerImpl(am.keeper)) 94 | } 95 | 96 | func (AppModule) ConsensusVersion() uint64 { 97 | return 1 98 | } 99 | 100 | func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { 101 | BeginBlocker(ctx, req, am.keeper) 102 | } 103 | 104 | func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { 105 | return []abci.ValidatorUpdate{} 106 | } 107 | 108 | func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { 109 | var genesisState types.GenesisState 110 | cdc.MustUnmarshalJSON(data, &genesisState) 111 | 112 | am.keeper.InitGenesis(ctx, &genesisState) 113 | 114 | return []abci.ValidatorUpdate{} 115 | } 116 | 117 | func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { 118 | gs := am.keeper.ExportGenesis(ctx) 119 | return cdc.MustMarshalJSON(gs) 120 | } 121 | 122 | //------------------------------------------------------------------------------ 123 | // Deprecated stuff 124 | //------------------------------------------------------------------------------ 125 | 126 | // deprecated 127 | func (AppModuleBasic) RegisterLegacyAminoCodec(_ *codec.LegacyAmino) { 128 | } 129 | 130 | // deprecated 131 | func (AppModuleBasic) RegisterRESTRoutes(_ client.Context, _ *mux.Router) { 132 | } 133 | 134 | // deprecated 135 | func (AppModule) Route() sdk.Route { 136 | return sdk.Route{} 137 | } 138 | 139 | // deprecated 140 | func (AppModule) QuerierRoute() string { 141 | return types.QuerierRoute 142 | } 143 | 144 | // deprecated 145 | func (AppModule) LegacyQuerierHandler(*codec.LegacyAmino) sdk.Querier { 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /x/incentives/module_test.go: -------------------------------------------------------------------------------- 1 | package incentives_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 9 | 10 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 11 | 12 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 13 | 14 | "github.com/mars-protocol/hub/v2/x/incentives/types" 15 | ) 16 | 17 | // TestCreatesModuleAccountAtGenesis asserts that the incentives module account 18 | // is properly registered with the auth module at genesis. 19 | func TestCreatesModuleAccountAtGenesis(t *testing.T) { 20 | app := marsapptesting.MakeSimpleMockApp() 21 | ctx := app.BaseApp.NewContext(false, tmproto.Header{}) 22 | 23 | acc := app.AccountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName)) 24 | require.NotNil(t, acc) 25 | } 26 | -------------------------------------------------------------------------------- /x/incentives/types/codec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | "github.com/cosmos/cosmos-sdk/types/msgservice" 7 | ) 8 | 9 | func RegisterInterfaces(registry codectypes.InterfaceRegistry) { 10 | registry.RegisterImplementations( 11 | (*sdk.Msg)(nil), 12 | &MsgCreateSchedule{}, 13 | &MsgTerminateSchedules{}, 14 | ) 15 | 16 | msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) 17 | } 18 | -------------------------------------------------------------------------------- /x/incentives/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "cosmossdk.io/errors" 4 | 5 | var ( 6 | ErrFailedRefundToCommunityPool = errors.Register(ModuleName, 2, "failed to return funds to community pool") 7 | ErrFailedWithdrawFromCommunityPool = errors.Register(ModuleName, 3, "failed to withdraw funds from community pool") 8 | ErrInvalidProposalAmount = errors.Register(ModuleName, 4, "invalid incentives proposal amount") 9 | ErrInvalidProposalAuthority = errors.Register(ModuleName, 5, "invalid incentives proposal authority") 10 | ErrInvalidProposalIds = errors.Register(ModuleName, 6, "invalid incentives proposal ids") 11 | ErrInvalidProposalStartEndTimes = errors.Register(ModuleName, 7, "invalid incentives proposal start and end times") 12 | ) 13 | -------------------------------------------------------------------------------- /x/incentives/types/events.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | EventTypeIncentivesReleased = "incentives_released" 5 | AttributeKeySchedules = "schedules" 6 | ) 7 | -------------------------------------------------------------------------------- /x/incentives/types/expected_keepers.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | 6 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 7 | stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" 8 | ) 9 | 10 | // AccountKeeper defines the expected interface for the auth module keeper 11 | type AccountKeeper interface { 12 | GetModuleAddress(name string) sdk.AccAddress 13 | GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI 14 | } 15 | 16 | // BankKeeper defines the expected interface for the bank module keeper 17 | type BankKeeper interface { 18 | GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins 19 | MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error 20 | SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error 21 | } 22 | 23 | // DistrKeeper defines the expected interface for the distribution module keeper 24 | type DistrKeeper interface { 25 | AllocateTokensToValidator(ctx sdk.Context, val stakingtypes.ValidatorI, tokens sdk.DecCoins) 26 | DistributeFromFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) error 27 | FundCommunityPool(ctx sdk.Context, amount sdk.Coins, senderAddr sdk.AccAddress) error 28 | } 29 | 30 | // StakingKeeper defines the expected interface for the staking module keeper 31 | type StakingKeeper interface { 32 | ValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) stakingtypes.ValidatorI 33 | } 34 | -------------------------------------------------------------------------------- /x/incentives/types/genesis.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "fmt" 4 | 5 | // DefaultGenesisState returns the default genesis state of the incentives module 6 | func DefaultGenesisState() *GenesisState { 7 | return &GenesisState{ 8 | NextScheduleId: 1, 9 | Schedules: []Schedule{}, 10 | } 11 | } 12 | 13 | // ValidateGenesis validates the given instance of the incentives module's 14 | // genesis state. 15 | // 16 | // for each schedule, 17 | // 18 | // - the id must be smaller than the next schedule id 19 | // 20 | // - the id must not be duplicate 21 | // 22 | // - the end time must be after the start time 23 | // 24 | // - the total amount must be non-zero 25 | // 26 | // - the released amount must be equal or smaller than the total amount 27 | func (gs GenesisState) Validate() error { 28 | seenIds := make(map[uint64]bool) 29 | for _, schedule := range gs.Schedules { 30 | if schedule.Id >= gs.NextScheduleId { 31 | return fmt.Errorf("incentives schedule id %d is not smaller than next schedule id %d", schedule.Id, gs.NextScheduleId) 32 | } 33 | 34 | if seenIds[schedule.Id] { 35 | return fmt.Errorf("incentives schedule has duplicate id %d", schedule.Id) 36 | } 37 | 38 | if !schedule.EndTime.After(schedule.StartTime) { 39 | return fmt.Errorf("incentives schedule %d end time is not after start time", schedule.Id) 40 | } 41 | 42 | if schedule.TotalAmount.Empty() { 43 | return fmt.Errorf("incentives schedule %d has zero total amount", schedule.Id) 44 | } 45 | 46 | if !schedule.TotalAmount.IsAllGTE(schedule.ReleasedAmount) { 47 | return fmt.Errorf("incentives schedule %d total amount is not all greater or equal than released amount", schedule.Id) 48 | } 49 | 50 | seenIds[schedule.Id] = true 51 | } 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /x/incentives/types/genesis_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | 11 | "github.com/mars-protocol/hub/v2/x/incentives/types" 12 | ) 13 | 14 | func getMockGenesisState() types.GenesisState { 15 | return types.GenesisState{ 16 | NextScheduleId: 4, 17 | Schedules: []types.Schedule{ 18 | { 19 | Id: 2, 20 | StartTime: time.Unix(10000, 0), 21 | EndTime: time.Unix(20000, 0), 22 | TotalAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(10000))), 23 | ReleasedAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(7500))), 24 | }, 25 | { 26 | 27 | Id: 3, 28 | StartTime: time.Unix(15000, 0), 29 | EndTime: time.Unix(25000, 0), 30 | TotalAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(20000)), sdk.NewCoin("uastro", sdk.NewInt(30000))), 31 | ReleasedAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(5000)), sdk.NewCoin("uastro", sdk.NewInt(7500))), 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | func TestNextIdTooSmall(t *testing.T) { 38 | gs := getMockGenesisState() 39 | gs.Schedules[1].Id = 5 40 | 41 | require.EqualError(t, gs.Validate(), "incentives schedule id 5 is not smaller than next schedule id 4") 42 | } 43 | 44 | func TestDuplicateId(t *testing.T) { 45 | gs := getMockGenesisState() 46 | gs.Schedules[1].Id = 2 47 | 48 | require.EqualError(t, gs.Validate(), "incentives schedule has duplicate id 2") 49 | } 50 | 51 | func TestEndTimeEarlierThanStart(t *testing.T) { 52 | gs := getMockGenesisState() 53 | gs.Schedules[1].EndTime = time.Unix(15000, 0) 54 | 55 | require.EqualError(t, gs.Validate(), "incentives schedule 3 end time is not after start time") 56 | } 57 | 58 | func TestZeroTotalAmount(t *testing.T) { 59 | gs := getMockGenesisState() 60 | gs.Schedules[1].TotalAmount = sdk.NewCoins() 61 | gs.Schedules[1].ReleasedAmount = sdk.NewCoins() 62 | 63 | require.EqualError(t, gs.Validate(), "incentives schedule 3 has zero total amount") 64 | } 65 | 66 | func TestReleasedAmountGreaterThanTotal(t *testing.T) { 67 | gs := getMockGenesisState() 68 | gs.Schedules[1].ReleasedAmount = sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(69420)), sdk.NewCoin("uastro", sdk.NewInt(7500))) 69 | 70 | require.EqualError(t, gs.Validate(), "incentives schedule 3 total amount is not all greater or equal than released amount") 71 | } 72 | 73 | func TestValidGenesis(t *testing.T) { 74 | gs := getMockGenesisState() 75 | 76 | require.NoError(t, gs.Validate()) 77 | } 78 | -------------------------------------------------------------------------------- /x/incentives/types/keys.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import sdk "github.com/cosmos/cosmos-sdk/types" 4 | 5 | const ( 6 | // ModuleName is the incentives module's name 7 | ModuleName = "incentives" 8 | 9 | // StoreKey is the incentives module's store key 10 | StoreKey = ModuleName 11 | 12 | // RouterKey is the incentives module's message route 13 | RouterKey = ModuleName 14 | 15 | // QuerierRoute is the incentives module's querier route 16 | QuerierRoute = ModuleName 17 | ) 18 | 19 | // Keys for the incentives module substore 20 | // Items are stored with the following key: values 21 | // 22 | // - 0x00: uint64 23 | // - 0x01: Schedule 24 | var ( 25 | KeyNextScheduleID = []byte{0x00} // key for the the next schedule id 26 | KeySchedule = []byte{0x01} // key for the incentives schedules 27 | ) 28 | 29 | // GetScheduleKey creates the key for the incentives schedule of the given id 30 | func GetScheduleKey(id uint64) []byte { 31 | return append(KeySchedule, sdk.Uint64ToBigEndian(id)...) 32 | } 33 | -------------------------------------------------------------------------------- /x/incentives/types/store.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | ) 8 | 9 | func durationToSecondsDec(d time.Duration) sdk.Dec { 10 | return sdk.NewDecFromIntWithPrec(sdk.NewInt(d.Nanoseconds()), 9) 11 | } 12 | 13 | // GetBlockReward calculates the reward to be releaed given a time: 14 | // - if the current time is before the start time, no coin is to be released 15 | // - if the current time is after the end time, all coins are to be released 16 | // - if the current time is betweeen the start and end times, coins are to be 17 | // released linearly 18 | func (s Schedule) GetBlockReward(currentTime time.Time) sdk.Coins { 19 | if s.StartTime.After(currentTime) { 20 | return sdk.NewCoins() 21 | } 22 | 23 | if currentTime.After(s.EndTime) { 24 | return s.TotalAmount.Sub(s.ReleasedAmount...) 25 | } 26 | 27 | timeTotal := durationToSecondsDec(s.EndTime.Sub(s.StartTime)) 28 | timeElapsed := durationToSecondsDec(currentTime.Sub(s.StartTime)) 29 | 30 | blockRewardDec := sdk.NewDecCoinsFromCoins(s.TotalAmount...).MulDec(timeElapsed).QuoDec(timeTotal) 31 | blockReward, _ := blockRewardDec.TruncateDecimal() 32 | 33 | return blockReward.Sub(s.ReleasedAmount...) 34 | } 35 | -------------------------------------------------------------------------------- /x/incentives/types/store_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | 11 | "github.com/mars-protocol/hub/v2/x/incentives/types" 12 | ) 13 | 14 | var mockSchedule = types.Schedule{ 15 | Id: 1, 16 | StartTime: time.Unix(10000, 0), 17 | EndTime: time.Unix(20000, 0), 18 | TotalAmount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(12345)), sdk.NewCoin("uastro", sdk.NewInt(69420))), 19 | ReleasedAmount: sdk.NewCoins(), 20 | } 21 | 22 | func TestGetBlockRewardBeforeStart(t *testing.T) { 23 | blockReward := mockSchedule.GetBlockReward(time.Unix(5000, 0)) 24 | require.Empty(t, blockReward) 25 | } 26 | 27 | func TestGetBlockRewardAfterEnd(t *testing.T) { 28 | blockReward := mockSchedule.GetBlockReward(time.Unix(42069, 0)) 29 | expected := mockSchedule.TotalAmount.Sub(mockSchedule.ReleasedAmount...) 30 | require.Equal(t, expected, blockReward) 31 | } 32 | 33 | func TestGetBlockRewardBetweenStartAndEnd(t *testing.T) { 34 | // NOTE: the default number of decimals used by sdk.Dec is 18 35 | // umars: 12345 * 1e18 * 3333 / 10000 / 1e18 = 4114 36 | // uastro: 69420 * 1e18 * 3333 / 10000 / 1e18 = 23137 37 | blockReward := mockSchedule.GetBlockReward(time.Unix(13333, 0)) 38 | expected := sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(4114)), sdk.NewCoin("uastro", sdk.NewInt(23137))) 39 | require.Equal(t, expected, blockReward) 40 | 41 | // next, try if the already released amount is properly subtracted 42 | mockSchedule.ReleasedAmount = blockReward 43 | 44 | // umars: 12345 * 1e18 * 8964 / 10000 / 1e18 - 4114 = 6952 45 | // uastro: 69420 * 1e18 * 8964 / 10000 / 1e18 - 23137 = 39091 46 | blockReward = mockSchedule.GetBlockReward(time.Unix(18964, 0)) 47 | expected = sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(6952)), sdk.NewCoin("uastro", sdk.NewInt(39091))) 48 | require.Equal(t, expected, blockReward) 49 | } 50 | -------------------------------------------------------------------------------- /x/incentives/types/tx.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | ) 6 | 7 | var ( 8 | _ sdk.Msg = &MsgCreateSchedule{} 9 | _ sdk.Msg = &MsgTerminateSchedules{} 10 | ) 11 | 12 | //------------------------------------------------------------------------------ 13 | // MsgCreateSchedule 14 | //------------------------------------------------------------------------------ 15 | 16 | // ValidateBasic does a sanity check on the provided data 17 | func (m *MsgCreateSchedule) ValidateBasic() error { 18 | // the authority address must be valid 19 | if _, err := sdk.AccAddressFromBech32(m.Authority); err != nil { 20 | return ErrInvalidProposalAuthority.Wrap(err.Error()) 21 | } 22 | 23 | // The start time must be earlier (strictly less than) the end time 24 | if !m.StartTime.Before(m.EndTime) { 25 | return ErrInvalidProposalStartEndTimes 26 | } 27 | 28 | // amount cannot be empty 29 | if m.Amount.Empty() { 30 | return ErrInvalidProposalAmount.Wrap("amount cannot be empty") 31 | } 32 | 33 | // the coins must be valid (unique denoms, non-zero amount, and sorted 34 | // alphabetically) 35 | if err := m.Amount.Validate(); err != nil { 36 | return ErrInvalidProposalAmount.Wrap(err.Error()) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | // GetSigners returns the expected signers for the message 43 | func (m *MsgCreateSchedule) GetSigners() []sdk.AccAddress { 44 | // we have already asserted that the authority address is valid in 45 | // ValidateBasic, so can ignore the error here 46 | addr, _ := sdk.AccAddressFromBech32(m.Authority) 47 | return []sdk.AccAddress{addr} 48 | } 49 | 50 | //------------------------------------------------------------------------------ 51 | // MsgTerminateSchedules 52 | //------------------------------------------------------------------------------ 53 | 54 | // ValidateBasic does a sanity check on the provided data 55 | func (m *MsgTerminateSchedules) ValidateBasic() error { 56 | // the authority address must be valid 57 | if _, err := sdk.AccAddressFromBech32(m.Authority); err != nil { 58 | return ErrInvalidProposalAuthority.Wrap(err.Error()) 59 | } 60 | 61 | // there must be at least one schedule id to be terminated 62 | if len(m.Ids) == 0 { 63 | return ErrInvalidProposalIds 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // GetSigners returns the expected signers for the message 70 | func (m *MsgTerminateSchedules) GetSigners() []sdk.AccAddress { 71 | // we have already asserted that the authority address is valid in 72 | // ValidateBasic, so can ignore the error here 73 | addr, _ := sdk.AccAddressFromBech32(m.Authority) 74 | return []sdk.AccAddress{addr} 75 | } 76 | -------------------------------------------------------------------------------- /x/incentives/types/tx_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | 11 | "github.com/mars-protocol/hub/v2/x/incentives/types" 12 | ) 13 | 14 | const govModuleAccount = "mars10d07y265gmmuvt4z0w9aw880jnsr700j8l2urg" 15 | 16 | var ( 17 | mockMsgCreateSchedule = types.MsgCreateSchedule{ 18 | Authority: govModuleAccount, 19 | StartTime: time.Unix(10000, 0), 20 | EndTime: time.Unix(20000, 0), 21 | Amount: sdk.NewCoins(sdk.NewCoin("umars", sdk.NewInt(10000))), 22 | } 23 | 24 | mockMsgTerminateSchedules = types.MsgTerminateSchedules{ 25 | Authority: govModuleAccount, 26 | Ids: []uint64{1, 2, 3, 4, 5}, 27 | } 28 | ) 29 | 30 | func init() { 31 | sdk.GetConfig().SetBech32PrefixForAccount("mars", "marspub") 32 | } 33 | 34 | func TestValidateCreateScheduleProposal(t *testing.T) { 35 | var msg types.MsgCreateSchedule 36 | 37 | testCases := []struct { 38 | name string 39 | malleate func() 40 | expError error 41 | }{ 42 | { 43 | "succeed", 44 | func() {}, 45 | nil, 46 | }, 47 | { 48 | "fail - end time is earlier than start time", 49 | func() { 50 | msg.EndTime = time.Unix(9999, 0) 51 | }, 52 | types.ErrInvalidProposalStartEndTimes, 53 | }, 54 | { 55 | "fail - amount is empty", 56 | func() { 57 | msg.Amount = sdk.NewCoins() 58 | }, 59 | types.ErrInvalidProposalAmount, 60 | }, 61 | { 62 | "fail - amount contains zero coin", 63 | func() { 64 | msg.Amount = []sdk.Coin{{Denom: "umars", Amount: sdk.NewInt(0)}} 65 | }, 66 | types.ErrInvalidProposalAmount, 67 | }, 68 | { 69 | "fail - amount contains negative coin", 70 | func() { 71 | msg.Amount = []sdk.Coin{{Denom: "umars", Amount: sdk.NewInt(-1)}} 72 | }, 73 | types.ErrInvalidProposalAmount, 74 | }, 75 | { 76 | "fail - coins are out of order", 77 | func() { 78 | msg.Amount = []sdk.Coin{sdk.NewCoin("umars", sdk.NewInt(12345)), sdk.NewCoin("uastro", sdk.NewInt(12345))} 79 | }, 80 | types.ErrInvalidProposalAmount, 81 | }, 82 | { 83 | "fail - duplicate denoms", 84 | func() { 85 | msg.Amount = []sdk.Coin{sdk.NewCoin("uastro", sdk.NewInt(12345)), sdk.NewCoin("uastro", sdk.NewInt(12345))} 86 | }, 87 | types.ErrInvalidProposalAmount, 88 | }, 89 | } 90 | 91 | for _, tc := range testCases { 92 | msg = mockMsgCreateSchedule 93 | tc.malleate() 94 | 95 | if tc.expError != nil { 96 | require.Error(t, msg.ValidateBasic(), tc.expError.Error(), tc.name) 97 | } else { 98 | require.NoError(t, msg.ValidateBasic(), tc.name) 99 | } 100 | } 101 | } 102 | 103 | func TestValidateTerminateScheduleProposal(t *testing.T) { 104 | var msg types.MsgTerminateSchedules 105 | 106 | testCases := []struct { 107 | name string 108 | malleate func() 109 | expError error 110 | }{ 111 | { 112 | "succeed", 113 | func() {}, 114 | nil, 115 | }, 116 | { 117 | "fail - ids are empty", 118 | func() { 119 | msg.Ids = []uint64{} 120 | }, 121 | types.ErrInvalidProposalIds, 122 | }, 123 | } 124 | 125 | for _, tc := range testCases { 126 | msg = mockMsgTerminateSchedules 127 | tc.malleate() 128 | 129 | if tc.expError != nil { 130 | require.Error(t, msg.ValidateBasic(), tc.expError.Error(), tc.name) 131 | } else { 132 | require.NoError(t, msg.ValidateBasic(), tc.name) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /x/safety/README.md: -------------------------------------------------------------------------------- 1 | # Safety Fund 2 | 3 | Currently, safety fund is simply a module account holding funds, which can be spent upon a successful a governance proposal. In the long term, the goal is that the module is able to automatically detect bad debts incurred in the outposts and distribute appropriate amount of funds to cover the shortfall, without having to go through the governance process. 4 | -------------------------------------------------------------------------------- /x/safety/client/cli/query.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/cosmos/cosmos-sdk/client" 7 | "github.com/cosmos/cosmos-sdk/client/flags" 8 | 9 | "github.com/mars-protocol/hub/v2/x/safety/types" 10 | ) 11 | 12 | // GetQueryCmd returns the parent command for all safety module query commands 13 | func GetQueryCmd() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "safety-fund", 16 | Short: "Querying commands for the safety fund module", 17 | DisableFlagParsing: true, 18 | SuggestionsMinimumDistance: 2, 19 | RunE: client.ValidateCmd, 20 | } 21 | 22 | cmd.AddCommand( 23 | getBalancesCmd(), 24 | ) 25 | 26 | return cmd 27 | } 28 | 29 | func getBalancesCmd() *cobra.Command { 30 | cmd := &cobra.Command{ 31 | Use: "balances", 32 | Short: "Query the amount of coins available in the safety fund", 33 | Args: cobra.NoArgs, 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | clientCtx, err := client.GetClientQueryContext(cmd) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | queryClient := types.NewQueryClient(clientCtx) 41 | 42 | res, err := queryClient.Balances(cmd.Context(), &types.QueryBalancesRequest{}) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return clientCtx.PrintProto(res) 48 | }, 49 | } 50 | 51 | flags.AddQueryFlagsToCmd(cmd) 52 | 53 | return cmd 54 | } 55 | -------------------------------------------------------------------------------- /x/safety/keeper/genesis.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | 6 | "github.com/mars-protocol/hub/v2/x/safety/types" 7 | ) 8 | 9 | // InitGenesis initializes the safety module's storage according to the provided 10 | // genesis state. 11 | // 12 | // NOTE: we call `GetModuleAccount` instead of `SetModuleAccount` because the 13 | // "get" function automatically sets the module account if it doesn't exist. 14 | func (k Keeper) InitGenesis(ctx sdk.Context, _ types.GenesisState) { 15 | k.accountKeeper.GetModuleAccount(ctx, types.ModuleName) 16 | } 17 | 18 | // ExportGenesis returns a genesis state for a given context and keeper 19 | func (k Keeper) ExportGenesis(_ sdk.Context) *types.GenesisState { 20 | return &types.GenesisState{} 21 | } 22 | -------------------------------------------------------------------------------- /x/safety/keeper/keeper.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tendermint/tendermint/libs/log" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | 10 | "github.com/mars-protocol/hub/v2/x/safety/types" 11 | ) 12 | 13 | // Keeper is the module's keeper 14 | type Keeper struct { 15 | accountKeeper types.AccountKeeper 16 | bankKeeper types.BankKeeper 17 | authority string 18 | } 19 | 20 | // NewKeeper creates a new Keeper instance 21 | func NewKeeper(accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, authority string) Keeper { 22 | // ensure the module account is set 23 | if accountKeeper.GetModuleAddress(types.ModuleName) == nil { 24 | panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) 25 | } 26 | 27 | return Keeper{accountKeeper, bankKeeper, authority} 28 | } 29 | 30 | // Logger returns a module-specific logger 31 | func (k Keeper) Logger(ctx sdk.Context) log.Logger { 32 | return ctx.Logger().With("module", "x/"+types.ModuleName) 33 | } 34 | 35 | // GetModuleAddress returns the safety fund module account's address 36 | func (k Keeper) GetModuleAddress() sdk.AccAddress { 37 | return k.accountKeeper.GetModuleAddress(types.ModuleName) 38 | } 39 | 40 | // GetBalances returns the amount of coins available in the safety fund 41 | func (k Keeper) GetBalances(ctx sdk.Context) sdk.Coins { 42 | return k.bankKeeper.GetAllBalances(ctx, k.GetModuleAddress()) 43 | } 44 | 45 | // ReleaseFund releases coins from the safety fund to the specified recipient 46 | func (k Keeper) ReleaseFund(ctx sdk.Context, recipient sdk.AccAddress, amount sdk.Coins) error { 47 | return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, recipient, amount) 48 | } 49 | -------------------------------------------------------------------------------- /x/safety/keeper/msg_server.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "context" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | 8 | govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 9 | 10 | "github.com/mars-protocol/hub/v2/x/safety/types" 11 | ) 12 | 13 | type msgServer struct{ k Keeper } 14 | 15 | // NewMsgServerImpl creates an implementation of the `MsgServer` interface for 16 | // the given keeper. 17 | func NewMsgServerImpl(k Keeper) types.MsgServer { 18 | return &msgServer{k} 19 | } 20 | 21 | func (ms msgServer) SafetyFundSpend(goCtx context.Context, req *types.MsgSafetyFundSpend) (*types.MsgSafetyFundSpendResponse, error) { 22 | ctx := sdk.UnwrapSDKContext(goCtx) 23 | 24 | if req.Authority != ms.k.authority { 25 | return nil, govtypes.ErrInvalidSigner.Wrapf("expected %s got %s", ms.k.authority, req.Authority) 26 | } 27 | 28 | recipientAddr, err := sdk.AccAddressFromBech32(req.Recipient) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | if err := ms.k.ReleaseFund(ctx, recipientAddr, req.Amount); err != nil { 34 | return nil, err 35 | } 36 | 37 | ms.k.Logger(ctx).Info( 38 | "released coins from safety fund", 39 | "recipient", req.Recipient, 40 | "amount", req.Amount.String(), 41 | ) 42 | 43 | return &types.MsgSafetyFundSpendResponse{}, nil 44 | } 45 | -------------------------------------------------------------------------------- /x/safety/keeper/query_server.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/grpc/codes" 7 | "google.golang.org/grpc/status" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | 11 | "github.com/mars-protocol/hub/v2/x/safety/types" 12 | ) 13 | 14 | type queryServer struct{ k Keeper } 15 | 16 | // NewQueryServerImpl creates an implementation of the `QueryServer` interface 17 | // for the given keeper. 18 | func NewQueryServerImpl(k Keeper) types.QueryServer { 19 | return &queryServer{k} 20 | } 21 | 22 | func (qs queryServer) Balances(goCtx context.Context, req *types.QueryBalancesRequest) (*types.QueryBalancesResponse, error) { 23 | if req == nil { 24 | return nil, status.Error(codes.InvalidArgument, "empty request") 25 | } 26 | 27 | ctx := sdk.UnwrapSDKContext(goCtx) 28 | 29 | balances := qs.k.GetBalances(ctx) 30 | 31 | return &types.QueryBalancesResponse{Balances: balances}, nil 32 | } 33 | -------------------------------------------------------------------------------- /x/safety/module.go: -------------------------------------------------------------------------------- 1 | package safety 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 10 | "github.com/spf13/cobra" 11 | 12 | abci "github.com/tendermint/tendermint/abci/types" 13 | 14 | "github.com/cosmos/cosmos-sdk/client" 15 | "github.com/cosmos/cosmos-sdk/codec" 16 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 17 | sdk "github.com/cosmos/cosmos-sdk/types" 18 | "github.com/cosmos/cosmos-sdk/types/module" 19 | 20 | "github.com/mars-protocol/hub/v2/x/safety/client/cli" 21 | "github.com/mars-protocol/hub/v2/x/safety/keeper" 22 | "github.com/mars-protocol/hub/v2/x/safety/types" 23 | ) 24 | 25 | var ( 26 | _ module.AppModule = AppModule{} 27 | _ module.AppModuleBasic = AppModuleBasic{} 28 | ) 29 | 30 | //------------------------------------------------------------------------------ 31 | // AppModuleBasic 32 | //------------------------------------------------------------------------------ 33 | 34 | // AppModuleBasic defines the basic application module used by the module 35 | type AppModuleBasic struct{} 36 | 37 | func (AppModuleBasic) Name() string { 38 | return types.ModuleName 39 | } 40 | 41 | func (AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) { 42 | types.RegisterInterfaces(registry) 43 | } 44 | 45 | func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { 46 | return cdc.MustMarshalJSON(types.DefaultGenesisState()) 47 | } 48 | 49 | func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { 50 | var gs types.GenesisState 51 | if err := cdc.UnmarshalJSON(bz, &gs); err != nil { 52 | return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) 53 | } 54 | 55 | return gs.Validate() 56 | } 57 | 58 | func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { 59 | if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil { 60 | panic(err) 61 | } 62 | } 63 | 64 | func (AppModuleBasic) GetTxCmd() *cobra.Command { 65 | return nil 66 | } 67 | 68 | func (AppModuleBasic) GetQueryCmd() *cobra.Command { 69 | return cli.GetQueryCmd() 70 | } 71 | 72 | //------------------------------------------------------------------------------ 73 | // AppModule 74 | //------------------------------------------------------------------------------ 75 | 76 | // AppModule implements an application module for the safety fund module 77 | type AppModule struct { 78 | AppModuleBasic 79 | 80 | keeper keeper.Keeper 81 | } 82 | 83 | // NewAppModule creates a new AppModule object 84 | func NewAppModule(keeper keeper.Keeper) AppModule { 85 | return AppModule{AppModuleBasic{}, keeper} 86 | } 87 | 88 | func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) { 89 | } 90 | 91 | func (am AppModule) RegisterServices(cfg module.Configurator) { 92 | types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper)) 93 | types.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServerImpl(am.keeper)) 94 | } 95 | 96 | func (AppModule) ConsensusVersion() uint64 { 97 | return 1 98 | } 99 | 100 | func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) { 101 | } 102 | 103 | func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { 104 | return []abci.ValidatorUpdate{} 105 | } 106 | 107 | func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { 108 | var genesisState types.GenesisState 109 | cdc.MustUnmarshalJSON(data, &genesisState) 110 | 111 | am.keeper.InitGenesis(ctx, genesisState) 112 | 113 | return []abci.ValidatorUpdate{} 114 | } 115 | 116 | func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { 117 | gs := am.keeper.ExportGenesis(ctx) 118 | return cdc.MustMarshalJSON(gs) 119 | } 120 | 121 | //------------------------------------------------------------------------------ 122 | // Deprecated stuff 123 | //------------------------------------------------------------------------------ 124 | 125 | // deprecated 126 | func (AppModuleBasic) RegisterLegacyAminoCodec(_ *codec.LegacyAmino) { 127 | } 128 | 129 | // deprecated 130 | func (AppModuleBasic) RegisterRESTRoutes(_ client.Context, _ *mux.Router) { 131 | } 132 | 133 | // deprecated 134 | func (AppModule) Route() sdk.Route { 135 | return sdk.Route{} 136 | } 137 | 138 | // deprecated 139 | func (AppModule) QuerierRoute() string { 140 | return types.QuerierRoute 141 | } 142 | 143 | // deprecated 144 | func (AppModule) LegacyQuerierHandler(*codec.LegacyAmino) sdk.Querier { 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /x/safety/module_test.go: -------------------------------------------------------------------------------- 1 | package safety_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 9 | 10 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 11 | 12 | marsapptesting "github.com/mars-protocol/hub/v2/app/testing" 13 | "github.com/mars-protocol/hub/v2/x/safety/types" 14 | ) 15 | 16 | // TestCreatesModuleAccountAtGenesis asserts that the safety module account is 17 | // properly registered with the auth module at genesis. 18 | func TestCreatesModuleAccountAtGenesis(t *testing.T) { 19 | app := marsapptesting.MakeSimpleMockApp() 20 | ctx := app.BaseApp.NewContext(false, tmproto.Header{}) 21 | 22 | acc := app.AccountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName)) 23 | require.NotNil(t, acc) 24 | } 25 | -------------------------------------------------------------------------------- /x/safety/types/codec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | codectypes "github.com/cosmos/cosmos-sdk/codec/types" 5 | sdk "github.com/cosmos/cosmos-sdk/types" 6 | "github.com/cosmos/cosmos-sdk/types/msgservice" 7 | ) 8 | 9 | func RegisterInterfaces(registry codectypes.InterfaceRegistry) { 10 | registry.RegisterImplementations( 11 | (*sdk.Msg)(nil), 12 | &MsgSafetyFundSpend{}, 13 | ) 14 | 15 | msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) 16 | } 17 | -------------------------------------------------------------------------------- /x/safety/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "cosmossdk.io/errors" 4 | 5 | var ( 6 | ErrInvalidProposalAmount = errors.Register(ModuleName, 2, "invalid safety fund spend proposal amount") 7 | ErrInvalidProposalAuthority = errors.Register(ModuleName, 3, "invalid safety fund spend proposal authority") 8 | ErrInvalidProposalRecipient = errors.Register(ModuleName, 4, "invalid safety fund spend proposal recipient") 9 | ) 10 | -------------------------------------------------------------------------------- /x/safety/types/expected_keepers.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | 6 | authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" 7 | ) 8 | 9 | // AccountKeeper defines the expected interface for the auth module keeper 10 | type AccountKeeper interface { 11 | GetModuleAddress(name string) sdk.AccAddress 12 | GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI 13 | } 14 | 15 | // BankKeeper defines the expected interface for the bank module keeper 16 | type BankKeeper interface { 17 | GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins 18 | SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error 19 | } 20 | -------------------------------------------------------------------------------- /x/safety/types/genesis.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // DefaultGenesisState returns the default genesis state of the module 4 | func DefaultGenesisState() *GenesisState { 5 | return &GenesisState{} 6 | } 7 | 8 | // ValidateGenesis validates the given instance of the module's genesis state 9 | func (GenesisState) Validate() error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /x/safety/types/keys.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | // ModuleName is the module's name 5 | ModuleName = "safety" 6 | 7 | // StoreKey is the module's store key 8 | StoreKey = ModuleName 9 | 10 | // RouterKey is the module's message route 11 | RouterKey = ModuleName 12 | 13 | // QuerierRoute is the module's querier route 14 | QuerierRoute = ModuleName 15 | ) 16 | -------------------------------------------------------------------------------- /x/safety/types/tx.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | ) 6 | 7 | var _ sdk.Msg = &MsgSafetyFundSpend{} 8 | 9 | // ValidateBasic does a sanity check on the provided data. 10 | func (m *MsgSafetyFundSpend) ValidateBasic() error { 11 | // the authority address must be valid 12 | if _, err := sdk.AccAddressFromBech32(m.Authority); err != nil { 13 | return ErrInvalidProposalAuthority.Wrap(err.Error()) 14 | } 15 | 16 | // the recipeint address must be valid 17 | if _, err := sdk.AccAddressFromBech32(m.Recipient); err != nil { 18 | return ErrInvalidProposalRecipient.Wrap(err.Error()) 19 | } 20 | 21 | // the coins must be valid (unique denoms, non-zero amount, and sorted 22 | // alphabetically) 23 | if !m.Amount.IsValid() { 24 | return ErrInvalidProposalAmount 25 | } 26 | 27 | return nil 28 | } 29 | 30 | // GetSigners returns the expected signers for the message 31 | func (m *MsgSafetyFundSpend) GetSigners() []sdk.AccAddress { 32 | // we have already asserted that the authority address is valid in 33 | // ValidateBasic, so can ignore the error here 34 | addr, _ := sdk.AccAddressFromBech32(m.Authority) 35 | return []sdk.AccAddress{addr} 36 | } 37 | -------------------------------------------------------------------------------- /x/safety/types/tx_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | // TODO 4 | --------------------------------------------------------------------------------