From 14db7b773a339004f824410b3ec150fd340c91e7 Mon Sep 17 00:00:00 2001 From: JayB Date: Tue, 14 Sep 2021 16:44:46 +0900 Subject: [PATCH] test: add simulation tests (#98) * test: add sim_test * test: add decoder and its unit tests * test: add randomized genesis params * test: add params and unit tests * chore: adding operations * test: add operations and tests * fix: lint checks * fix: nolint check for interBlockCacheOpt * test: nolint unused code * lint: last test * chore: fix broken test and add todo for public plan proposal * chore: fix param changes, randomize epoch days, add farming fee collector, add workflow * fix: broken test * docs: fix backticks location * chore: add migration tests, sort imports, fix db to newDB, update Makefile * chore: fix simulation logic, add minter permission to module account, work in progress to solve harvest issue * feat: adding public plan proposals * chore: adding simulation for public plan proposals work in progress * fix: add check for duplicate name value when creating private plans #101 * test: fix broken tests * chore: remove comments, refactor logic, fixing harvest * build: update github workflow to split simulation jobs * chore: add public plans, remove unused test codes and clean up logics - add request public plan proposal - update request public plan proposal - delete request public plan proposal - comment TODO to improve public plans logic * chore: debugging export and import simulation * fix: update simulation logics for f1 spec * fix: fix test plan dates and expected codes, lint * fix: lint and validation logic of simulation * fix: revert test plan and add validation logic for unstake * chore: remove unused test functions * fix: apply suggested reviews * chore: fix lint Co-authored-by: dongsam --- .github/workflows/sims.yml | 120 ++++++++ .github/workflows/test.yml | 2 +- Makefile | 19 ++ app/app.go | 15 +- app/app_test.go | 239 ++++++++++++++- app/params/weights.go | 16 +- app/sim_test.go | 340 ++++++++++++++++++++++ docs/How-To/client.md | 2 +- go.mod | 1 + x/farming/client/cli/cli_test.go | 50 ++-- x/farming/client/cli/tx.go | 12 +- x/farming/keeper/genesis.go | 3 + x/farming/keeper/genesis_test.go | 6 +- x/farming/keeper/keeper_test.go | 18 -- x/farming/keeper/msg_server.go | 16 + x/farming/keeper/proposal_handler.go | 5 +- x/farming/keeper/proposal_handler_test.go | 4 +- x/farming/keeper/reward_test.go | 3 +- x/farming/keeper/staking.go | 44 ++- x/farming/keeper/staking_test.go | 12 +- x/farming/module.go | 29 +- x/farming/simulation/decoder.go | 10 +- x/farming/simulation/decoder_test.go | 79 ++--- x/farming/simulation/genesis.go | 36 ++- x/farming/simulation/genesis_test.go | 78 +++++ x/farming/simulation/operations.go | 296 +++++++++++++++++-- x/farming/simulation/operations_test.go | 327 +++++++++++++++++++++ x/farming/simulation/params.go | 18 +- x/farming/simulation/params_test.go | 7 +- x/farming/simulation/proposals.go | 201 +++++++++++++ x/farming/simulation/proposals_test.go | 84 ++++++ x/farming/spec/03_state_transitions.md | 1 - x/farming/spec/08_params.md | 5 - x/farming/types/codec.go | 52 ++-- x/farming/types/expected_keepers.go | 6 + x/farming/types/genesis.go | 12 +- x/farming/types/plan.go | 30 +- x/farming/types/plan_test.go | 100 ++++++- x/farming/types/proposal.go | 11 +- 39 files changed, 2084 insertions(+), 225 deletions(-) create mode 100644 .github/workflows/sims.yml create mode 100644 app/sim_test.go create mode 100644 x/farming/simulation/genesis_test.go create mode 100644 x/farming/simulation/operations_test.go create mode 100644 x/farming/simulation/proposals.go create mode 100644 x/farming/simulation/proposals_test.go diff --git a/.github/workflows/sims.yml b/.github/workflows/sims.yml new file mode 100644 index 00000000..aa8dd73a --- /dev/null +++ b/.github/workflows/sims.yml @@ -0,0 +1,120 @@ +name: Sims +# Sims workflow runs multiple types of simulations (nondeterminism, import-export, after-import) +# This workflow will run on all Pull Requests, if a .go, .mod or .sum file have been changed +on: + pull_request: + push: + branches: + - master + - develop + +jobs: + build: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'skip-sims')" + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2.1.3 + with: + go-version: 1.16 + - name: Display go version + run: go version + - run: make build + + install-runsim: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/setup-go@v2.1.3 + with: + go-version: 1.16 + - name: Display go version + run: go version + - name: Install runsim + run: export GO111MODULE="on" && go get github.com/cosmos/tools/cmd/runsim@v1.0.0 + - uses: actions/cache@v2.1.6 + with: + path: ~/go/bin + key: ${{ runner.os }}-go-runsim-binary + + test-sim-nondeterminism: + runs-on: ubuntu-latest + needs: [build, install-runsim] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2.1.3 + with: + go-version: 1.16 + - name: Display go version + run: go version + - uses: technote-space/get-diff-action@v4 + with: + PATTERNS: | + **/**.go + go.mod + go.sum + - uses: actions/cache@v2.1.6 + with: + path: ~/go/bin + key: ${{ runner.os }}-go-runsim-binary + if: env.GIT_DIFF + - name: test-sim-nondeterminism + run: | + make test-sim-nondeterminism + if: env.GIT_DIFF + + # test-sim-import-export: + # runs-on: ubuntu-latest + # needs: [build, install-runsim] + # steps: + # - uses: actions/checkout@v2 + # - uses: actions/setup-go@v2.1.3 + # with: + # go-version: 1.16 + # - name: Display go version + # run: go version + # - uses: technote-space/get-diff-action@v4 + # with: + # SUFFIX_FILTER: | + # **/**.go + # go.mod + # go.sum + # SET_ENV_NAME_INSERTIONS: 1 + # SET_ENV_NAME_LINES: 1 + # - uses: actions/cache@v2.1.6 + # with: + # path: ~/go/bin + # key: ${{ runner.os }}-go-runsim-binary + # if: env.GIT_DIFF + # - name: test-sim-import-export + # run: | + # make test-sim-import-export + # if: env.GIT_DIFF + + # test-sim-after-import: + # runs-on: ubuntu-latest + # needs: [build, install-runsim] + # steps: + # - uses: actions/checkout@v2 + # - uses: actions/setup-go@v2.1.3 + # with: + # go-version: 1.16 + # - name: Display go version + # run: go version + # - uses: technote-space/get-diff-action@v4 + # with: + # SUFFIX_FILTER: | + # **/**.go + # go.mod + # go.sum + # SET_ENV_NAME_INSERTIONS: 1 + # SET_ENV_NAME_LINES: 1 + # - uses: actions/cache@v2.1.6 + # with: + # path: ~/go/bin + # key: ${{ runner.os }}-go-runsim-binary + # if: env.GIT_DIFF + # - name: test-sim-after-import + # run: | + # make test-sim-after-import + # if: env.GIT_DIFF diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5e69b94..b84b9dba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -87,4 +87,4 @@ jobs: done - uses: codecov/codecov-action@v1.0.14 with: - file: ./coverage.txt + file: ./coverage.txt \ No newline at end of file diff --git a/Makefile b/Makefile index 8e709ad0..929fa2c5 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ BINDIR ?= $(GOPATH)/bin DOCKER := $(shell which docker) DOCKER_BUF := $(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace bufbuild/buf BUILDDIR ?= $(CURDIR)/build +SIMAPP = ./app export GO111MODULE = on @@ -154,6 +155,24 @@ benchmark: .PHONY: test test-all test-unit test-race test-cover test-build +test-sim-nondeterminism: + @echo "Running non-determinism test..." + @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ + -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h + +test-sim-import-export: runsim + @echo "Running application import/export simulation. This may take several minutes..." + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 10 5 TestAppImportExport + +test-sim-after-import: runsim + @echo "Running application simulation-after-import. This may take several minutes..." + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 10 5 TestAppSimulationAfterImport + +.PHONY: \ +test-sim-nondeterminism \ +test-sim-import-export \ +test-sim-after-import + ############################################################################### ### Localnet ### ############################################################################### diff --git a/app/app.go b/app/app.go index a8461225..13719dcd 100644 --- a/app/app.go +++ b/app/app.go @@ -113,9 +113,9 @@ var ( mint.AppModuleBasic{}, distr.AppModuleBasic{}, gov.NewAppModuleBasic( - paramsclient.ProposalHandler, distrclient.ProposalHandler, upgradeclient.ProposalHandler, upgradeclient.CancelProposalHandler, + paramsclient.ProposalHandler, distrclient.ProposalHandler, + upgradeclient.ProposalHandler, upgradeclient.CancelProposalHandler, farmingclient.ProposalHandler, - // todo: farming proposal handler ), params.AppModuleBasic{}, crisis.AppModuleBasic{}, @@ -137,7 +137,7 @@ var ( stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, govtypes.ModuleName: {authtypes.Burner}, - farmingtypes.ModuleName: nil, + farmingtypes.ModuleName: {authtypes.Minter}, // todo: farming Staking Reserve Coin TBD } ) @@ -580,6 +580,15 @@ func RegisterSwaggerAPI(ctx client.Context, rtr *mux.Router) { rtr.PathPrefix("/swagger/").Handler(http.StripPrefix("/swagger/", staticServer)) } +// GetMaccPerms returns a copy of the module account permissions +func GetMaccPerms() map[string][]string { + dupMaccPerms := make(map[string][]string) + for k, v := range maccPerms { + dupMaccPerms[k] = v + } + return dupMaccPerms +} + // initParamsKeeper init params keeper and its subspaces func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino, key, tkey sdk.StoreKey) paramskeeper.Keeper { paramsKeeper := paramskeeper.NewKeeper(appCodec, legacyAmino, key, tkey) diff --git a/app/app_test.go b/app/app_test.go index 082cef80..7a457598 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -5,17 +5,51 @@ import ( "os" "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/tests/mocks" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + authzmodule "github.com/cosmos/cosmos-sdk/x/authz/module" + "github.com/cosmos/cosmos-sdk/x/bank" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/capability" + "github.com/cosmos/cosmos-sdk/x/crisis" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/evidence" + feegrantmodule "github.com/cosmos/cosmos-sdk/x/feegrant/module" + "github.com/cosmos/cosmos-sdk/x/genutil" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/mint" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/slashing" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/upgrade" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" dbm "github.com/tendermint/tm-db" + + "github.com/tendermint/farming/x/farming" ) -func TestSimAppExport(t *testing.T) { +func TestSimAppExportAndBlockedAddrs(t *testing.T) { encCfg := MakeTestEncodingConfig() db := dbm.NewMemDB() app := NewFarmingApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, encCfg, EmptyAppOptions{}) + for acc := range maccPerms { + require.True( + t, + app.BankKeeper.BlockedAddr(app.AccountKeeper.GetModuleAddress(acc)), + "ensure that blocked addresses are properly set in bank keeper", + ) + } + genesisState := NewDefaultGenesisState(encCfg.Marshaler) stateBytes, err := json.MarshalIndent(genesisState, "", " ") require.NoError(t, err) @@ -29,8 +63,209 @@ func TestSimAppExport(t *testing.T) { ) app.Commit() - res, err := app.ExportAppStateAndValidators(false, []string{}) + // Making a new app object with the db, so that initchain hasn't been called + app2 := NewFarmingApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, encCfg, EmptyAppOptions{}) + res, err := app2.ExportAppStateAndValidators(false, []string{}) require.NoError(t, err, "ExportAppStateAndValidators should not have an error") + _, err = res.AppState.MarshalJSON() require.NoError(t, err) } + +func TestGetMaccPerms(t *testing.T) { + dup := GetMaccPerms() + require.Equal(t, maccPerms, dup, "duplicated module account permissions differed from actual module account permissions") +} + +func TestRunMigrations(t *testing.T) { + db := dbm.NewMemDB() + encCfg := MakeTestEncodingConfig() + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + app := NewFarmingApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, encCfg, EmptyAppOptions{}) + + // Create a new baseapp and configurator for the purpose of this test. + bApp := baseapp.NewBaseApp(appName, logger, db, encCfg.TxConfig.TxDecoder()) + bApp.SetCommitMultiStoreTracer(nil) + bApp.SetInterfaceRegistry(encCfg.InterfaceRegistry) + app.BaseApp = bApp + app.configurator = module.NewConfigurator(app.appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter()) + + // We register all modules on the Configurator, except x/bank. x/bank will + // serve as the test subject on which we run the migration tests. + // + // The loop below is the same as calling `RegisterServices` on + // ModuleManager, except that we skip x/bank. + for _, module := range app.mm.Modules { + if module.Name() == banktypes.ModuleName { + continue + } + + module.RegisterServices(app.configurator) + } + + // Initialize the chain + app.InitChain(abci.RequestInitChain{}) + app.Commit() + + testCases := []struct { + name string + moduleName string + forVersion uint64 + expRegErr bool // errors while registering migration + expRegErrMsg string + expRunErr bool // errors while running migration + expRunErrMsg string + expCalled int + }{ + { + "cannot register migration for version 0", + "bank", 0, + true, "module migration versions should start at 1: invalid version", false, "", 0, + }, + { + "throws error on RunMigrations if no migration registered for bank", + "", 1, + false, "", true, "no migrations found for module bank: not found", 0, + }, + { + "can register and run migration handler for x/bank", + "bank", 1, + false, "", false, "", 1, + }, + { + "cannot register migration handler for same module & forVersion", + "bank", 1, + true, "another migration for module bank and version 1 already exists: internal logic error", false, "", 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var err error + + // Since it's very hard to test actual in-place store migrations in + // tests (due to the difficulty of maintaining multiple versions of a + // module), we're just testing here that the migration logic is + // called. + called := 0 + + if tc.moduleName != "" { + // Register migration for module from version `forVersion` to `forVersion+1`. + err = app.configurator.RegisterMigration(tc.moduleName, tc.forVersion, func(sdk.Context) error { + called++ + + return nil + }) + + if tc.expRegErr { + require.EqualError(t, err, tc.expRegErrMsg) + + return + } + } + require.NoError(t, err) + + // Run migrations only for bank. That's why we put the initial + // version for bank as 1, and for all other modules, we put as + // their latest ConsensusVersion. + _, err = app.mm.RunMigrations( + app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}), app.configurator, + module.VersionMap{ + "bank": 1, + "auth": auth.AppModule{}.ConsensusVersion(), + "authz": authzmodule.AppModule{}.ConsensusVersion(), + "staking": staking.AppModule{}.ConsensusVersion(), + "mint": mint.AppModule{}.ConsensusVersion(), + "distribution": distribution.AppModule{}.ConsensusVersion(), + "slashing": slashing.AppModule{}.ConsensusVersion(), + "gov": gov.AppModule{}.ConsensusVersion(), + "params": params.AppModule{}.ConsensusVersion(), + "upgrade": upgrade.AppModule{}.ConsensusVersion(), + "vesting": vesting.AppModule{}.ConsensusVersion(), + "feegrant": feegrantmodule.AppModule{}.ConsensusVersion(), + "evidence": evidence.AppModule{}.ConsensusVersion(), + "crisis": crisis.AppModule{}.ConsensusVersion(), + "genutil": genutil.AppModule{}.ConsensusVersion(), + "capability": capability.AppModule{}.ConsensusVersion(), + "farming": farming.AppModule{}.ConsensusVersion(), + }, + ) + if tc.expRunErr { + require.EqualError(t, err, tc.expRunErrMsg) + } else { + require.NoError(t, err) + // Make sure bank's migration is called. + require.Equal(t, tc.expCalled, called) + } + }) + } +} + +func TestInitGenesisOnMigration(t *testing.T) { + db := dbm.NewMemDB() + encCfg := MakeTestEncodingConfig() + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + app := NewFarmingApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, encCfg, EmptyAppOptions{}) + ctx := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) + + // Create a mock module. This module will serve as the new module we're + // adding during a migration. + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + mockModule := mocks.NewMockAppModule(mockCtrl) + mockDefaultGenesis := json.RawMessage(`{"key": "value"}`) + mockModule.EXPECT().DefaultGenesis(gomock.Eq(app.appCodec)).Times(1).Return(mockDefaultGenesis) + mockModule.EXPECT().InitGenesis(gomock.Eq(ctx), gomock.Eq(app.appCodec), gomock.Eq(mockDefaultGenesis)).Times(1).Return(nil) + mockModule.EXPECT().ConsensusVersion().Times(1).Return(uint64(0)) + + app.mm.Modules["mock"] = mockModule + + // Run migrations only for "mock" module. We exclude it from + // the VersionMap to simulate upgrading with a new module. + _, err := app.mm.RunMigrations(ctx, app.configurator, + module.VersionMap{ + "bank": bank.AppModule{}.ConsensusVersion(), + "auth": auth.AppModule{}.ConsensusVersion(), + "authz": authzmodule.AppModule{}.ConsensusVersion(), + "staking": staking.AppModule{}.ConsensusVersion(), + "mint": mint.AppModule{}.ConsensusVersion(), + "distribution": distribution.AppModule{}.ConsensusVersion(), + "slashing": slashing.AppModule{}.ConsensusVersion(), + "gov": gov.AppModule{}.ConsensusVersion(), + "params": params.AppModule{}.ConsensusVersion(), + "upgrade": upgrade.AppModule{}.ConsensusVersion(), + "vesting": vesting.AppModule{}.ConsensusVersion(), + "feegrant": feegrantmodule.AppModule{}.ConsensusVersion(), + "evidence": evidence.AppModule{}.ConsensusVersion(), + "crisis": crisis.AppModule{}.ConsensusVersion(), + "genutil": genutil.AppModule{}.ConsensusVersion(), + "capability": capability.AppModule{}.ConsensusVersion(), + "farming": farming.AppModule{}.ConsensusVersion(), + }, + ) + require.NoError(t, err) +} + +func TestUpgradeStateOnGenesis(t *testing.T) { + encCfg := MakeTestEncodingConfig() + db := dbm.NewMemDB() + app := NewFarmingApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, encCfg, EmptyAppOptions{}) + genesisState := NewDefaultGenesisState(encCfg.Marshaler) + stateBytes, err := json.MarshalIndent(genesisState, "", " ") + require.NoError(t, err) + + // Initialize the chain + app.InitChain( + abci.RequestInitChain{ + Validators: []abci.ValidatorUpdate{}, + AppStateBytes: stateBytes, + }, + ) + + // make sure the upgrade keeper has version map in state + ctx := app.NewContext(false, tmproto.Header{}) + vm := app.UpgradeKeeper.GetModuleVersionMap(ctx) + for v, i := range app.mm.Modules { + require.Equal(t, vm[v], i.ConsensusVersion()) + } +} diff --git a/app/params/weights.go b/app/params/weights.go index 467626c9..9890600d 100644 --- a/app/params/weights.go +++ b/app/params/weights.go @@ -1,10 +1,14 @@ package params const ( - // TODO: farming determine the weights - DefaultWeightMsgCreateFixedAmountPlan int = 0 - DefaultWeightMsgCreateRatioPlan int = 0 - DefaultWeightMsgStake int = 0 - DefaultWeightMsgUnstake int = 0 - DefaultWeightMsgHarvest int = 0 + // farming module simulation operation weights for messages + DefaultWeightMsgCreateFixedAmountPlan int = 10 + DefaultWeightMsgCreateRatioPlan int = 10 + DefaultWeightMsgStake int = 85 + DefaultWeightMsgUnstake int = 30 + DefaultWeightMsgHarvest int = 30 + + DefaultWeightAddPublicPlanProposal int = 10 + DefaultWeightUpdatePublicPlanProposal int = 5 + DefaultWeightDeletePublicPlanProposal int = 5 ) diff --git a/app/sim_test.go b/app/sim_test.go new file mode 100644 index 00000000..fef71fcd --- /dev/null +++ b/app/sim_test.go @@ -0,0 +1,340 @@ +package app + +// DONTCOVER + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/simapp/helpers" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + dbm "github.com/tendermint/tm-db" + + farmingtypes "github.com/tendermint/farming/x/farming/types" +) + +// Get flags every time the simulator is run +func init() { + simapp.GetSimulatorFlags() +} + +type StoreKeysPrefixes struct { + A sdk.StoreKey + B sdk.StoreKey + Prefixes [][]byte +} + +// fauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of +// an IAVLStore for faster simulation speed. +func fauxMerkleModeOpt(bapp *baseapp.BaseApp) { + bapp.SetFauxMerkleMode() +} + +// interBlockCacheOpt returns a BaseApp option function that sets the persistent +// inter-block write-through cache. +// nolint:deadcode,unused,varcheck +func interBlockCacheOpt() func(*baseapp.BaseApp) { + return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()) +} + +func TestFullAppSimulation(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application simulation") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewFarmingApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), EmptyAppOptions{}, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), + simtypes.RandomAccounts, // replace with own random account function if using keys other than secp256k1 + simapp.SimulationOperations(app, app.AppCodec(), config), + app.ModuleAccountAddrs(), + config, + app.AppCodec(), + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } +} + +func TestAppImportExport(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application import/export simulation") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewFarmingApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), EmptyAppOptions{}, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), + simtypes.RandomAccounts, // replace with own random account function if using keys other than secp256k1 + simapp.SimulationOperations(app, app.AppCodec(), config), + app.ModuleAccountAddrs(), + config, + app.AppCodec(), + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } + + fmt.Printf("exporting genesis...\n") + + exported, err := app.ExportAppStateAndValidators(false, []string{}) + require.NoError(t, err) + + fmt.Printf("importing genesis...\n") + + _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2") + require.NoError(t, err, "simulation setup failed") + + defer func() { + newDB.Close() + require.NoError(t, os.RemoveAll(newDir)) + }() + + newApp := NewFarmingApp(log.NewNopLogger(), newDB, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), EmptyAppOptions{}, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + var genesisState GenesisState + err = json.Unmarshal(exported.AppState, &genesisState) + require.NoError(t, err) + + ctxA := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) + ctxB := newApp.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) + newApp.mm.InitGenesis(ctxB, app.AppCodec(), genesisState) + newApp.StoreConsensusParams(ctxB, exported.ConsensusParams) + + fmt.Printf("comparing stores...\n") + + storeKeysPrefixes := []StoreKeysPrefixes{ + {app.keys[authtypes.StoreKey], newApp.keys[authtypes.StoreKey], [][]byte{}}, + {app.keys[stakingtypes.StoreKey], newApp.keys[stakingtypes.StoreKey], + [][]byte{ + stakingtypes.UnbondingQueueKey, stakingtypes.RedelegationQueueKey, stakingtypes.ValidatorQueueKey, + stakingtypes.HistoricalInfoKey, + }}, // ordering may change but it doesn't matter + {app.keys[slashingtypes.StoreKey], newApp.keys[slashingtypes.StoreKey], [][]byte{}}, + {app.keys[minttypes.StoreKey], newApp.keys[minttypes.StoreKey], [][]byte{}}, + {app.keys[distrtypes.StoreKey], newApp.keys[distrtypes.StoreKey], [][]byte{}}, + {app.keys[banktypes.StoreKey], newApp.keys[banktypes.StoreKey], [][]byte{banktypes.BalancesPrefix}}, + {app.keys[paramtypes.StoreKey], newApp.keys[paramtypes.StoreKey], [][]byte{}}, + {app.keys[govtypes.StoreKey], newApp.keys[govtypes.StoreKey], [][]byte{}}, + {app.keys[evidencetypes.StoreKey], newApp.keys[evidencetypes.StoreKey], [][]byte{}}, + {app.keys[capabilitytypes.StoreKey], newApp.keys[capabilitytypes.StoreKey], [][]byte{}}, + {app.keys[authzkeeper.StoreKey], newApp.keys[authzkeeper.StoreKey], [][]byte{}}, + {app.keys[farmingtypes.StoreKey], newApp.keys[farmingtypes.StoreKey], [][]byte{}}, + } + + for _, skp := range storeKeysPrefixes { + storeA := ctxA.KVStore(skp.A) + storeB := ctxB.KVStore(skp.B) + + failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, skp.Prefixes) + require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare") + + fmt.Printf("compared %d different key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B) + require.Equal(t, len(failedKVAs), 0, simapp.GetSimulationLog(skp.A.Name(), app.SimulationManager().StoreDecoders, failedKVAs, failedKVBs)) + } +} + +func TestAppSimulationAfterImport(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application simulation after import") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewFarmingApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), EmptyAppOptions{}, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // run randomized simulation + stopEarly, simParams, simErr := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), + simtypes.RandomAccounts, // replace with own random account function if using keys other than secp256k1 + simapp.SimulationOperations(app, app.AppCodec(), config), + app.ModuleAccountAddrs(), + config, + app.AppCodec(), + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } + + if stopEarly { + fmt.Println("can't export or import a zero-validator genesis, exiting test...") + return + } + + fmt.Printf("exporting genesis...\n") + + exported, err := app.ExportAppStateAndValidators(true, []string{}) + require.NoError(t, err) + + fmt.Printf("importing genesis...\n") + + _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2") + require.NoError(t, err, "simulation setup failed") + + defer func() { + newDB.Close() + require.NoError(t, os.RemoveAll(newDir)) + }() + + newApp := NewFarmingApp(logger, newDB, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), EmptyAppOptions{}, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + newApp.InitChain(abci.RequestInitChain{ + AppStateBytes: exported.AppState, + }) + + _, _, err = simulation.SimulateFromSeed( + t, + os.Stdout, + newApp.BaseApp, + simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simapp.SimulationOperations(newApp, newApp.AppCodec(), config), + app.ModuleAccountAddrs(), + config, + app.AppCodec(), + ) + require.NoError(t, err) +} + +func TestAppStateDeterminism(t *testing.T) { + if !simapp.FlagEnabledValue { + t.Skip("skipping application simulation") + } + + config := simapp.NewConfigFromFlags() + config.InitialBlockHeight = 1 + config.ExportParamsPath = "" + config.OnOperation = false + config.AllInvariants = false + config.ChainID = helpers.SimAppChainID + + numSeeds := 1 + numTimesToRunPerSeed := 1 + appHashList := make([]json.RawMessage, numTimesToRunPerSeed) + + for i := 0; i < numSeeds; i++ { + config.Seed = rand.Int63() + + for j := 0; j < numTimesToRunPerSeed; j++ { + var logger log.Logger + if simapp.FlagVerboseValue { + logger = log.TestingLogger() + } else { + logger = log.NewNopLogger() + } + + db := dbm.NewMemDB() + app := NewFarmingApp(logger, db, nil, true, map[int64]bool{}, DefaultNodeHome, simapp.FlagPeriodValue, MakeEncodingConfig(), EmptyAppOptions{}, fauxMerkleModeOpt) + + fmt.Printf( + "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", + config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + + _, _, err := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simapp.AppStateFn(app.AppCodec(), app.SimulationManager()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simapp.SimulationOperations(app, app.AppCodec(), config), + app.ModuleAccountAddrs(), + config, + app.AppCodec(), + ) + require.NoError(t, err) + + if config.Commit { + simapp.PrintStats(db) + } + + appHash := app.LastCommitID().Hash + appHashList[j] = appHash + + if j != 0 { + require.Equal( + t, string(appHashList[0]), string(appHashList[j]), + "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + } + } + } +} diff --git a/docs/How-To/client.md b/docs/How-To/client.md index 992ef2c2..8e6a0c6d 100644 --- a/docs/How-To/client.md +++ b/docs/How-To/client.md @@ -110,7 +110,6 @@ $BINARY start } ] } -``` # Send to create a private fixed amount plan farmingd tx farming create-private-fixed-plan private-fixed-plan.json \ @@ -119,6 +118,7 @@ farmingd tx farming create-private-fixed-plan private-fixed-plan.json \ --keyring-backend test \ --broadcast-mode block \ --yes +``` ```json { diff --git a/go.mod b/go.mod index 9262d86e..ca987141 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/cosmos/cosmos-sdk v0.44.0 github.com/gogo/protobuf v1.3.3 + github.com/golang/mock v1.6.0 github.com/golang/protobuf v1.5.2 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 diff --git a/x/farming/client/cli/cli_test.go b/x/farming/client/cli/cli_test.go index e584ae16..fe940a7b 100644 --- a/x/farming/client/cli/cli_test.go +++ b/x/farming/client/cli/cli_test.go @@ -82,8 +82,8 @@ func (s *IntegrationTestSuite) TestNewCreateFixedAmountPlanCmd() { case1 := cli.PrivateFixedPlanRequest{ Name: name, StakingCoinWeights: coinWeights, - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochAmount: sdk.NewCoins(sdk.NewInt64Coin("uatom", 100_000_000)), } @@ -94,8 +94,8 @@ func (s *IntegrationTestSuite) TestNewCreateFixedAmountPlanCmd() { OVERMAXLENGTHOVERMAXLENGTHOVERMAXLENGTHOVERMOVERMAXLENGTHOVERMAXLENGTHOVERMAXLENGTHOVERM OVERMAXLENGTHOVERMAXLENGTHOVERMAXLENGTHOVERMOVERMAXLENGTHOVERMAXLENGTHOVERMAXLENGTHOVERM`, StakingCoinWeights: sdk.NewDecCoins(), - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochAmount: sdk.NewCoins(sdk.NewInt64Coin("uatom", 100_000_000)), } @@ -103,8 +103,8 @@ func (s *IntegrationTestSuite) TestNewCreateFixedAmountPlanCmd() { case3 := cli.PrivateFixedPlanRequest{ Name: name, StakingCoinWeights: sdk.NewDecCoins(), - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochAmount: sdk.NewCoins(sdk.NewInt64Coin("uatom", 100_000_000)), } @@ -112,8 +112,8 @@ func (s *IntegrationTestSuite) TestNewCreateFixedAmountPlanCmd() { case4 := cli.PrivateFixedPlanRequest{ Name: name, StakingCoinWeights: sdk.NewDecCoins(sdk.NewDecCoin("poolD35A0CC16EE598F90B044CE296A405BA9C381E38837599D96F2F70C2F02A23A4", sdk.NewInt(2))), - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochAmount: sdk.NewCoins(sdk.NewInt64Coin("uatom", 100_000_000)), } @@ -130,8 +130,8 @@ func (s *IntegrationTestSuite) TestNewCreateFixedAmountPlanCmd() { case6 := cli.PrivateFixedPlanRequest{ Name: name, StakingCoinWeights: coinWeights, - StartTime: mustParseRFC3339("2021-08-13T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-06T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochAmount: sdk.NewCoins(sdk.NewInt64Coin("uatom", 0)), } @@ -195,7 +195,7 @@ func (s *IntegrationTestSuite) TestNewCreateFixedAmountPlanCmd() { fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), }, - true, &sdk.TxResponse{}, 0, + true, &sdk.TxResponse{}, 1, }, { "invalid epoch amount case #1", @@ -247,8 +247,8 @@ func (s *IntegrationTestSuite) TestNewCreateRatioPlanCmd() { case1 := cli.PrivateRatioPlanRequest{ Name: name, StakingCoinWeights: coinWeights, - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochRatio: sdk.MustNewDecFromStr("0.1"), } @@ -259,8 +259,8 @@ func (s *IntegrationTestSuite) TestNewCreateRatioPlanCmd() { OVERMAXLENGTHOVERMAXLENGTHOVERMAXLENGTHOVERMOVERMAXLENGTHOVERMAXLENGTHOVERMAXLENGTHOVERM OVERMAXLENGTHOVERMAXLENGTHOVERMAXLENGTHOVERMOVERMAXLENGTHOVERMAXLENGTHOVERMAXLENGTHOVERM`, StakingCoinWeights: sdk.NewDecCoins(), - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochRatio: sdk.MustNewDecFromStr("0.1"), } @@ -268,8 +268,8 @@ func (s *IntegrationTestSuite) TestNewCreateRatioPlanCmd() { case3 := cli.PrivateRatioPlanRequest{ Name: name, StakingCoinWeights: sdk.NewDecCoins(), - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochRatio: sdk.MustNewDecFromStr("0.1"), } @@ -277,8 +277,8 @@ func (s *IntegrationTestSuite) TestNewCreateRatioPlanCmd() { case4 := cli.PrivateRatioPlanRequest{ Name: name, StakingCoinWeights: sdk.NewDecCoins(sdk.NewDecCoin("poolD35A0CC16EE598F90B044CE296A405BA9C381E38837599D96F2F70C2F02A23A4", sdk.NewInt(2))), - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochRatio: sdk.MustNewDecFromStr("0.1"), } @@ -295,8 +295,8 @@ func (s *IntegrationTestSuite) TestNewCreateRatioPlanCmd() { case6 := cli.PrivateRatioPlanRequest{ Name: name, StakingCoinWeights: coinWeights, - StartTime: mustParseRFC3339("2021-08-13T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-06T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochRatio: sdk.MustNewDecFromStr("1.1"), } @@ -510,6 +510,8 @@ func (s *IntegrationTestSuite) TestNewUnstakeCmd() { s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String()) txResp := tc.respType.(*sdk.TxResponse) + fmt.Println(txResp) + fmt.Println(out.String()) s.Require().Equal(tc.expectedCode, txResp.Code, out.String()) } }) @@ -523,8 +525,8 @@ func (s *IntegrationTestSuite) TestNewHarvestCmd() { req := cli.PrivateFixedPlanRequest{ Name: "test", StakingCoinWeights: sdk.NewDecCoins(sdk.NewDecCoin("stake", sdk.NewInt(1))), - StartTime: mustParseRFC3339("2021-08-06T00:00:00Z"), - EndTime: mustParseRFC3339("2021-08-13T00:00:00Z"), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), EpochAmount: sdk.NewCoins(sdk.NewInt64Coin("node0token", 100_000_000)), } @@ -563,7 +565,7 @@ func (s *IntegrationTestSuite) TestNewHarvestCmd() { fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), }, - false, &sdk.TxResponse{}, 6, + false, &sdk.TxResponse{}, 1, }, { "invalid staking coin denoms case #1", diff --git a/x/farming/client/cli/tx.go b/x/farming/client/cli/tx.go index b7f33163..5c2fa2fa 100644 --- a/x/farming/client/cli/tx.go +++ b/x/farming/client/cli/tx.go @@ -376,11 +376,13 @@ Where proposal.json contains: return err } - content, err := types.NewPublicPlanProposal(proposal.Title, proposal.Description, - proposal.AddRequestProposals, proposal.UpdateRequestProposals, proposal.DeleteRequestProposals) - if err != nil { - return err - } + content := types.NewPublicPlanProposal( + proposal.Title, + proposal.Description, + proposal.AddRequestProposals, + proposal.UpdateRequestProposals, + proposal.DeleteRequestProposals, + ) from := clientCtx.GetFromAddress() diff --git a/x/farming/keeper/genesis.go b/x/farming/keeper/genesis.go index 8287d3b7..6888bd24 100644 --- a/x/farming/keeper/genesis.go +++ b/x/farming/keeper/genesis.go @@ -9,6 +9,7 @@ import ( // InitGenesis initializes the farming module's state from a given genesis state. func (k Keeper) InitGenesis(ctx sdk.Context, genState types.GenesisState) { ctx, applyCache := ctx.CacheContext() + k.SetParams(ctx, genState.Params) moduleAcc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleName) k.accountKeeper.SetModuleAccount(ctx, moduleAcc) @@ -34,9 +35,11 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState types.GenesisState) { if err := k.ValidateRemainingRewardsAmount(ctx); err != nil { panic(err) } + if err := k.ValidateStakingReservedAmount(ctx); err != nil { panic(err) } + applyCache() } diff --git a/x/farming/keeper/genesis_test.go b/x/farming/keeper/genesis_test.go index 1b547058..2a15f2fa 100644 --- a/x/farming/keeper/genesis_test.go +++ b/x/farming/keeper/genesis_test.go @@ -1,6 +1,8 @@ package keeper_test import ( + _ "github.com/stretchr/testify/suite" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/farming/x/farming/types" @@ -11,7 +13,7 @@ func (suite *KeeperTestSuite) TestInitGenesis() { types.NewFixedAmountPlan( types.NewBasePlan( 1, - "", + "name1", types.PlanTypePrivate, suite.addrs[0].String(), suite.addrs[0].String(), @@ -25,7 +27,7 @@ func (suite *KeeperTestSuite) TestInitGenesis() { types.NewRatioPlan( types.NewBasePlan( 2, - "", + "name2", types.PlanTypePublic, suite.addrs[0].String(), suite.addrs[0].String(), diff --git a/x/farming/keeper/keeper_test.go b/x/farming/keeper/keeper_test.go index fd9d3d86..6e057fb4 100644 --- a/x/farming/keeper/keeper_test.go +++ b/x/farming/keeper/keeper_test.go @@ -133,24 +133,6 @@ func (suite *KeeperTestSuite) Stake(farmerAcc sdk.AccAddress, amt sdk.Coins) { suite.Require().NoError(err) } -func (suite *KeeperTestSuite) StakedCoins(farmerAcc sdk.AccAddress) sdk.Coins { - stakedCoins := sdk.NewCoins() - suite.keeper.IterateStakingsByFarmer(suite.ctx, farmerAcc, func(stakingCoinDenom string, staking types.Staking) (stop bool) { - stakedCoins = stakedCoins.Add(sdk.NewCoin(stakingCoinDenom, staking.Amount)) - return false - }) - return stakedCoins -} - -func (suite *KeeperTestSuite) QueuedCoins(farmerAcc sdk.AccAddress) sdk.Coins { - queuedCoins := sdk.NewCoins() - suite.keeper.IterateQueuedStakingsByFarmer(suite.ctx, farmerAcc, func(stakingCoinDenom string, queuedStaking types.QueuedStaking) (stop bool) { - queuedCoins = queuedCoins.Add(sdk.NewCoin(stakingCoinDenom, queuedStaking.Amount)) - return false - }) - return queuedCoins -} - func (suite *KeeperTestSuite) Rewards(farmerAcc sdk.AccAddress) sdk.Coins { cacheCtx, _ := suite.ctx.CacheContext() rewards, err := suite.keeper.WithdrawAllRewards(cacheCtx, farmerAcc) diff --git a/x/farming/keeper/msg_server.go b/x/farming/keeper/msg_server.go index ba5e57ba..61a1fd2e 100644 --- a/x/farming/keeper/msg_server.go +++ b/x/farming/keeper/msg_server.go @@ -32,10 +32,16 @@ func (k msgServer) CreateFixedAmountPlan(goCtx context.Context, msg *types.MsgCr if err != nil { return nil, err } + if _, err := k.Keeper.CreateFixedAmountPlan(ctx, msg, poolAcc, msg.GetCreator(), types.PlanTypePrivate); err != nil { return nil, err } + plans := k.GetAllPlans(ctx) + if err := types.ValidateName(plans); err != nil { + return nil, err + } + return &types.MsgCreateFixedAmountPlanResponse{}, nil } @@ -46,10 +52,20 @@ func (k msgServer) CreateRatioPlan(goCtx context.Context, msg *types.MsgCreateRa if err != nil { return nil, err } + if _, err := k.Keeper.CreateRatioPlan(ctx, msg, poolAcc, msg.GetCreator(), types.PlanTypePrivate); err != nil { return nil, err } + plans := k.GetAllPlans(ctx) + if err := types.ValidateName(plans); err != nil { + return nil, err + } + + if err := types.ValidateTotalEpochRatio(plans); err != nil { + return nil, err + } + return &types.MsgCreateRatioPlanResponse{}, nil } diff --git a/x/farming/keeper/proposal_handler.go b/x/farming/keeper/proposal_handler.go index 972883fd..6b1234a4 100644 --- a/x/farming/keeper/proposal_handler.go +++ b/x/farming/keeper/proposal_handler.go @@ -28,7 +28,10 @@ func HandlePublicPlanProposal(ctx sdk.Context, k Keeper, proposal *types.PublicP } plans := k.GetAllPlans(ctx) - if err := types.ValidateRatioPlans(plans); err != nil { + if err := types.ValidateName(plans); err != nil { + return err + } + if err := types.ValidateTotalEpochRatio(plans); err != nil { return err } diff --git a/x/farming/keeper/proposal_handler_test.go b/x/farming/keeper/proposal_handler_test.go index 165e7e5d..67766257 100644 --- a/x/farming/keeper/proposal_handler_test.go +++ b/x/farming/keeper/proposal_handler_test.go @@ -1,14 +1,14 @@ package keeper_test import ( - _ "github.com/stretchr/testify/suite" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/tendermint/farming/app" "github.com/tendermint/farming/x/farming/keeper" "github.com/tendermint/farming/x/farming/types" + + _ "github.com/stretchr/testify/suite" ) func (suite *KeeperTestSuite) TestAddPublicPlanProposal() { diff --git a/x/farming/keeper/reward_test.go b/x/farming/keeper/reward_test.go index f2440faf..18eeda14 100644 --- a/x/farming/keeper/reward_test.go +++ b/x/farming/keeper/reward_test.go @@ -3,6 +3,8 @@ package keeper_test import ( "time" + _ "github.com/stretchr/testify/suite" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/farming/x/farming/types" @@ -179,7 +181,6 @@ func (suite *KeeperTestSuite) TestHarvest() { suite.keeper.ProcessQueuedCoins(suite.ctx) balancesBefore := suite.app.BankKeeper.GetAllBalances(suite.ctx, suite.addrs[0]) - suite.ctx = suite.ctx.WithBlockTime(mustParseRFC3339("2021-08-05T00:00:00Z")) err := suite.keeper.AllocateRewards(suite.ctx) suite.Require().NoError(err) diff --git a/x/farming/keeper/staking.go b/x/farming/keeper/staking.go index 4d4100f8..8d097af2 100644 --- a/x/farming/keeper/staking.go +++ b/x/farming/keeper/staking.go @@ -60,6 +60,15 @@ func (k Keeper) IterateStakingsByFarmer(ctx sdk.Context, farmerAcc sdk.AccAddres } } +func (k Keeper) GetAllStakedCoinsByFarmer(ctx sdk.Context, farmerAcc sdk.AccAddress) sdk.Coins { + stakedCoins := sdk.NewCoins() + k.IterateStakingsByFarmer(ctx, farmerAcc, func(stakingCoinDenom string, staking types.Staking) (stop bool) { + stakedCoins = stakedCoins.Add(sdk.NewCoin(stakingCoinDenom, staking.Amount)) + return false + }) + return stakedCoins +} + func (k Keeper) GetQueuedStaking(ctx sdk.Context, stakingCoinDenom string, farmerAcc sdk.AccAddress) (queuedStaking types.QueuedStaking, found bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.GetQueuedStakingKey(stakingCoinDenom, farmerAcc)) @@ -71,6 +80,15 @@ func (k Keeper) GetQueuedStaking(ctx sdk.Context, stakingCoinDenom string, farme return } +func (k Keeper) GetAllQueuedStakedCoinsByFarmer(ctx sdk.Context, farmerAcc sdk.AccAddress) sdk.Coins { + stakedCoins := sdk.NewCoins() + k.IterateQueuedStakingsByFarmer(ctx, farmerAcc, func(stakingCoinDenom string, queuedStaking types.QueuedStaking) (stop bool) { + stakedCoins = stakedCoins.Add(sdk.NewCoin(stakingCoinDenom, queuedStaking.Amount)) + return false + }) + return stakedCoins +} + func (k Keeper) SetQueuedStaking(ctx sdk.Context, stakingCoinDenom string, farmerAcc sdk.AccAddress, queuedStaking types.QueuedStaking) { store := ctx.KVStore(k.storeKey) bz := k.cdc.MustMarshal(&queuedStaking) @@ -191,8 +209,10 @@ func (k Keeper) Unstake(ctx sdk.Context, farmerAcc sdk.AccAddress, amount sdk.Co sdkerrors.ErrInsufficientFunds, "%s%s is smaller than %s%s", availableAmt, coin.Denom, coin.Amount, coin.Denom) } - if _, err := k.WithdrawRewards(ctx, farmerAcc, coin.Denom); err != nil { - return err + if staking.Amount.IsPositive() { + if _, err := k.WithdrawRewards(ctx, farmerAcc, coin.Denom); err != nil { + return err + } } removedFromStaking := sdk.ZeroInt() @@ -202,14 +222,13 @@ func (k Keeper) Unstake(ctx sdk.Context, farmerAcc sdk.AccAddress, amount sdk.Co staking.Amount = staking.Amount.Add(queuedStaking.Amount) removedFromStaking = queuedStaking.Amount.Neg() queuedStaking.Amount = sdk.ZeroInt() - } - - if staking.Amount.IsPositive() { - currentEpoch := k.GetCurrentEpoch(ctx, coin.Denom) - staking.StartingEpoch = currentEpoch - k.SetStaking(ctx, coin.Denom, farmerAcc, staking) - } else { - k.DeleteStaking(ctx, coin.Denom, farmerAcc) + if staking.Amount.IsPositive() { + currentEpoch := k.GetCurrentEpoch(ctx, coin.Denom) + staking.StartingEpoch = currentEpoch + k.SetStaking(ctx, coin.Denom, farmerAcc, staking) + } else { + k.DeleteStaking(ctx, coin.Denom, farmerAcc) + } } if queuedStaking.Amount.IsPositive() { @@ -218,7 +237,10 @@ func (k Keeper) Unstake(ctx sdk.Context, farmerAcc sdk.AccAddress, amount sdk.Co k.DeleteQueuedStaking(ctx, coin.Denom, farmerAcc) } - totalStaking, _ := k.GetTotalStaking(ctx, coin.Denom) + totalStaking, found := k.GetTotalStaking(ctx, coin.Denom) + if !found { + totalStaking.Amount = sdk.ZeroInt() + } totalStaking.Amount = totalStaking.Amount.Sub(removedFromStaking) k.SetTotalStaking(ctx, coin.Denom, totalStaking) } diff --git a/x/farming/keeper/staking_test.go b/x/farming/keeper/staking_test.go index ea2be63c..fe07ee40 100644 --- a/x/farming/keeper/staking_test.go +++ b/x/farming/keeper/staking_test.go @@ -120,8 +120,8 @@ func (suite *KeeperTestSuite) TestUnstake() { suite.Error(err) } else { if suite.NoError(err) { - suite.True(coinsEq(tc.remainingStaked, suite.StakedCoins(suite.addrs[tc.addrIdx]))) - suite.True(coinsEq(tc.remainingQueued, suite.QueuedCoins(suite.addrs[tc.addrIdx]))) + suite.True(coinsEq(tc.remainingStaked, suite.keeper.GetAllStakedCoinsByFarmer(suite.ctx, suite.addrs[tc.addrIdx]))) + suite.True(coinsEq(tc.remainingQueued, suite.keeper.GetAllQueuedStakedCoinsByFarmer(suite.ctx, suite.addrs[tc.addrIdx]))) } } }) @@ -154,15 +154,15 @@ func (suite *KeeperTestSuite) TestProcessQueuedCoins() { } } - suite.Require().True(coinsEq(queuedCoins, suite.QueuedCoins(suite.addrs[0]))) - suite.Require().True(coinsEq(stakedCoins, suite.StakedCoins(suite.addrs[0]))) + suite.Require().True(coinsEq(queuedCoins, suite.keeper.GetAllQueuedStakedCoinsByFarmer(suite.ctx, suite.addrs[0]))) + suite.Require().True(coinsEq(stakedCoins, suite.keeper.GetAllStakedCoinsByFarmer(suite.ctx, suite.addrs[0]))) suite.keeper.ProcessQueuedCoins(suite.ctx) stakedCoins = stakedCoins.Add(queuedCoins...) queuedCoins = sdk.NewCoins() - suite.Require().True(coinsEq(queuedCoins, suite.QueuedCoins(suite.addrs[0]))) - suite.Require().True(coinsEq(stakedCoins, suite.StakedCoins(suite.addrs[0]))) + suite.Require().True(coinsEq(queuedCoins, suite.keeper.GetAllQueuedStakedCoinsByFarmer(suite.ctx, suite.addrs[0]))) + suite.Require().True(coinsEq(stakedCoins, suite.keeper.GetAllStakedCoinsByFarmer(suite.ctx, suite.addrs[0]))) } } } diff --git a/x/farming/module.go b/x/farming/module.go index 7119f492..f2b0c627 100644 --- a/x/farming/module.go +++ b/x/farming/module.go @@ -22,8 +22,7 @@ import ( //"github.com/tendermint/farming/x/farming/client/rest" "github.com/tendermint/farming/x/farming/client/cli" "github.com/tendermint/farming/x/farming/keeper" - - //"github.com/tendermint/farming/x/farming/simulation" + "github.com/tendermint/farming/x/farming/simulation" "github.com/tendermint/farming/x/farming/types" ) @@ -45,7 +44,7 @@ func (AppModuleBasic) Name() string { // RegisterLegacyAminoCodec registers the farming module's types for the given codec. func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { - //types.RegisterLegacyAminoCodec(cdc) + // types.RegisterLegacyAminoCodec(cdc) } // DefaultGenesis returns default genesis state as raw bytes for the farming @@ -184,36 +183,28 @@ func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.Val // GenerateGenesisState creates a randomized GenState of the farming module. func (AppModule) GenerateGenesisState(simState *module.SimulationState) { - // TODO: implement - //simulation.RandomizedGenState(simState) + simulation.RandomizedGenState(simState) } // ProposalContents returns all the farming content functions used to // simulate governance proposals. func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { - // TODO: implement - return nil - //return simulation.ProposalContents(am.keeper) + return simulation.ProposalContents(am.accountKeeper, am.bankKeeper, am.keeper) } // RandomizedParams creates randomized farming param changes for the simulator. func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange { - return nil - // TODO: implement - //return simulation.ParamChanges(r) + return simulation.ParamChanges(r) } -// RegisterStoreDecoder registers a decoder for farming module's types +// RegisterStoreDecoder registers a decoder for farming module's types. func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { - // TODO: implement - //sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) + sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc) } // WeightedOperations returns the all the gov module operations with their respective weights. func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { - // TODO: implement - return nil - //return simulation.WeightedOperations( - // simState.AppParams, simState.Cdc, am.accountKeeper, am.bankKeeper, am.keeper, am.stakingKeeper, - //) + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, am.accountKeeper, am.bankKeeper, am.keeper, + ) } diff --git a/x/farming/simulation/decoder.go b/x/farming/simulation/decoder.go index c2ec0ba1..813fd93f 100644 --- a/x/farming/simulation/decoder.go +++ b/x/farming/simulation/decoder.go @@ -15,8 +15,7 @@ import ( func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { return func(kvA, kvB kv.Pair) string { switch { - case bytes.Equal(kvA.Key[:1], types.PlanKeyPrefix), - bytes.Equal(kvA.Key[:1], types.PlansByFarmerIndexKeyPrefix): + case bytes.Equal(kvA.Key[:1], types.PlanKeyPrefix): var pA, pB types.BasePlan cdc.MustUnmarshal(kvA.Value, &pA) cdc.MustUnmarshal(kvA.Value, &pB) @@ -28,6 +27,13 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { cdc.MustUnmarshal(kvA.Value, &sB) return fmt.Sprintf("%v\n%v", sA, sB) + case bytes.Equal(kvA.Key[:1], types.QueuedStakingKeyPrefix): + var sA, sB types.QueuedStaking + cdc.MustUnmarshal(kvA.Value, &sA) + cdc.MustUnmarshal(kvA.Value, &sB) + return fmt.Sprintf("%v\n%v", sA, sB) + + //TODO: add f1 struct default: panic(fmt.Sprintf("invalid farming key prefix %X", kvA.Key[:1])) } diff --git a/x/farming/simulation/decoder_test.go b/x/farming/simulation/decoder_test.go index d0eae7c7..c58ae880 100644 --- a/x/farming/simulation/decoder_test.go +++ b/x/farming/simulation/decoder_test.go @@ -1,49 +1,54 @@ package simulation_test import ( + "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/types/kv" "github.com/tendermint/farming/x/farming/simulation" + "github.com/tendermint/farming/x/farming/types" ) func TestDecodeFarmingStore(t *testing.T) { - - cdc := simapp.MakeTestEncodingConfig() - _ = simulation.NewDecodeStore(cdc.Marshaler) - - // TODO: not implemented yet - - // liquidityPool := types.Pool{} - // liquidityPool.Id = 1 - // liquidityPoolBatch := types.NewPoolBatch(1, 1) - - // kvPairs := kv.Pairs{ - // Pairs: []kv.Pair{ - // {Key: types.PoolKeyPrefix, Value: cdc.MustMarshalBinaryBare(&liquidityPool)}, - // {Key: types.PoolBatchKeyPrefix, Value: cdc.MustMarshalBinaryBare(&liquidityPoolBatch)}, - // {Key: []byte{0x99}, Value: []byte{0x99}}, - // }, - // } - - // tests := []struct { - // name string - // expectedLog string - // }{ - // {"Pool", fmt.Sprintf("%v\n%v", liquidityPool, liquidityPool)}, - // {"PoolBatch", fmt.Sprintf("%v\n%v", liquidityPoolBatch, liquidityPoolBatch)}, - // {"other", ""}, - // } - // for i, tt := range tests { - // i, tt := i, tt - // t.Run(tt.name, func(t *testing.T) { - // switch i { - // case len(tests) - 1: - // require.Panics(t, func() { dec(kvPairs.Pairs[i], kvPairs.Pairs[i]) }, tt.name) - // default: - // require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i]), tt.name) - // } - // }) - // } + cdc := simapp.MakeTestEncodingConfig().Marshaler + dec := simulation.NewDecodeStore(cdc) + + basePlan := types.BasePlan{} + staking := types.Staking{} + queuedStaking := types.QueuedStaking{} + + kvPairs := kv.Pairs{ + Pairs: []kv.Pair{ + {Key: types.PlanKeyPrefix, Value: cdc.MustMarshal(&basePlan)}, + {Key: types.StakingKeyPrefix, Value: cdc.MustMarshal(&staking)}, + {Key: types.QueuedStakingKeyPrefix, Value: cdc.MustMarshal(&queuedStaking)}, + // TODO: f1 structs, indexes + {Key: []byte{0x99}, Value: []byte{0x99}}, + }, + } + + tests := []struct { + name string + expectedLog string + }{ + {"Plan", fmt.Sprintf("%v\n%v", basePlan, basePlan)}, + {"Staking", fmt.Sprintf("%v\n%v", staking, staking)}, + {"QueuedStaking", fmt.Sprintf("%v\n%v", queuedStaking, queuedStaking)}, + {"other", ""}, + } + for i, tt := range tests { + i, tt := i, tt + t.Run(tt.name, func(t *testing.T) { + switch i { + case len(tests) - 1: + require.Panics(t, func() { dec(kvPairs.Pairs[i], kvPairs.Pairs[i]) }, tt.name) + default: + require.Equal(t, tt.expectedLog, dec(kvPairs.Pairs[i], kvPairs.Pairs[i]), tt.name) + } + }) + } } diff --git a/x/farming/simulation/genesis.go b/x/farming/simulation/genesis.go index 59fc905b..ac5b9fdf 100644 --- a/x/farming/simulation/genesis.go +++ b/x/farming/simulation/genesis.go @@ -9,22 +9,34 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/tendermint/farming/x/farming/types" ) -// Simulation parameter constants +// Simulation parameter constants. const ( PrivatePlanCreationFee = "private_plan_creation_fee" + EpochDays = "epoch_days" + FarmingFeeCollector = "farming_fee_collector" ) -// GenPrivatePlanCreationFee return default PrivatePlanCreationFee +// GenPrivatePlanCreationFee return randomized private plan creation fee. func GenPrivatePlanCreationFee(r *rand.Rand) sdk.Coins { - // TODO: randomize private plan creation fee - return types.DefaultPrivatePlanCreationFee + return sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, int64(simulation.RandIntBetween(r, 0, 100_000_000)))) } -// RandomizedGenState generates a random GenesisState for farming +// GenEpochDays return default epoch days. +func GenEpochDays(r *rand.Rand) uint32 { + return uint32(simulation.RandIntBetween(r, int(types.DefaultEpochDays), 10)) +} + +// GenFarmingFeeCollector returns default farming fee collector. +func GenFarmingFeeCollector(r *rand.Rand) string { + return types.DefaultFarmingFeeCollector +} + +// RandomizedGenState generates a random GenesisState for farming. func RandomizedGenState(simState *module.SimulationState) { var privatePlanCreationFee sdk.Coins simState.AppParams.GetOrGenerate( @@ -32,9 +44,23 @@ func RandomizedGenState(simState *module.SimulationState) { func(r *rand.Rand) { privatePlanCreationFee = GenPrivatePlanCreationFee(r) }, ) + var epochDays uint32 + simState.AppParams.GetOrGenerate( + simState.Cdc, EpochDays, &epochDays, simState.Rand, + func(r *rand.Rand) { epochDays = GenEpochDays(r) }, + ) + + var feeCollector string + simState.AppParams.GetOrGenerate( + simState.Cdc, FarmingFeeCollector, &feeCollector, simState.Rand, + func(r *rand.Rand) { feeCollector = GenFarmingFeeCollector(r) }, + ) + farmingGenesis := types.GenesisState{ Params: types.Params{ PrivatePlanCreationFee: privatePlanCreationFee, + EpochDays: epochDays, + FarmingFeeCollector: feeCollector, }, } diff --git a/x/farming/simulation/genesis_test.go b/x/farming/simulation/genesis_test.go new file mode 100644 index 00000000..727d7993 --- /dev/null +++ b/x/farming/simulation/genesis_test.go @@ -0,0 +1,78 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + + "github.com/tendermint/farming/x/farming/simulation" + "github.com/tendermint/farming/x/farming/types" +) + +// TestRandomizedGenState tests the normal scenario of applying RandomizedGenState. +// Abnormal scenarios are not tested here. +func TestRandomizedGenState(t *testing.T) { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + s := rand.NewSource(1) + r := rand.New(s) + + simState := module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + NumBonded: 3, + Accounts: simtypes.RandomAccounts(r, 3), + InitialStake: 1000, + GenState: make(map[string]json.RawMessage), + } + + simulation.RandomizedGenState(&simState) + + var genState types.GenesisState + simState.Cdc.MustUnmarshalJSON(simState.GenState[types.ModuleName], &genState) + + dec1 := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(36122540))) + dec3 := uint32(5) + dec4 := "cosmos1h292smhhttwy0rl3qr4p6xsvpvxc4v05s6rxtczwq3cs6qc462mqejwy8x" + + require.Equal(t, dec1, genState.Params.PrivatePlanCreationFee) + require.Equal(t, dec3, genState.Params.EpochDays) + require.Equal(t, dec4, genState.Params.FarmingFeeCollector) +} + +// TestRandomizedGenState tests abnormal scenarios of applying RandomizedGenState. +func TestRandomizedGenState1(t *testing.T) { + interfaceRegistry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(interfaceRegistry) + + s := rand.NewSource(1) + r := rand.New(s) + + // all these tests will panic + tests := []struct { + simState module.SimulationState + panicMsg string + }{ + { // panic => reason: incomplete initialization of the simState + module.SimulationState{}, "invalid memory address or nil pointer dereference"}, + { // panic => reason: incomplete initialization of the simState + module.SimulationState{ + AppParams: make(simtypes.AppParams), + Cdc: cdc, + Rand: r, + }, "assignment to entry in nil map"}, + } + + for _, tt := range tests { + require.Panicsf(t, func() { simulation.RandomizedGenState(&tt.simState) }, tt.panicMsg) + } +} diff --git a/x/farming/simulation/operations.go b/x/farming/simulation/operations.go index a2203bd2..2d7ff762 100644 --- a/x/farming/simulation/operations.go +++ b/x/farming/simulation/operations.go @@ -5,6 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" + simappparams "github.com/cosmos/cosmos-sdk/simapp/params" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" @@ -14,16 +15,16 @@ import ( "github.com/tendermint/farming/x/farming/types" ) -// Simulation operation weights constants +// Simulation operation weights constants. const ( OpWeightMsgCreateFixedAmountPlan = "op_weight_msg_create_fixed_amount_plan" OpWeightMsgCreateRatioPlan = "op_weight_msg_create_ratio_plan" OpWeightMsgStake = "op_weight_msg_stake" OpWeightMsgUnstake = "op_weight_msg_unstake" - OpWeightMsgClaim = "op_weight_msg_claim" + OpWeightMsgHarvest = "op_weight_msg_harvest" ) -// WeightedOperations returns all the operations from the module with their respective weights +// WeightedOperations returns all the operations from the module with their respective weights. func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper, @@ -57,10 +58,10 @@ func WeightedOperations( }, ) - var weightMsgClaim int - appParams.GetOrGenerate(cdc, OpWeightMsgClaim, &weightMsgClaim, nil, + var weightMsgHarvest int + appParams.GetOrGenerate(cdc, OpWeightMsgHarvest, &weightMsgHarvest, nil, func(_ *rand.Rand) { - weightMsgClaim = params.DefaultWeightMsgHarvest + weightMsgHarvest = params.DefaultWeightMsgHarvest }, ) @@ -82,8 +83,8 @@ func WeightedOperations( SimulateMsgUnstake(ak, bk, k), ), simulation.NewWeightedOperation( - weightMsgClaim, - SimulateMsgClaim(ak, bk, k), + weightMsgHarvest, + SimulateMsgHarvest(ak, bk, k), ), } } @@ -94,8 +95,57 @@ func SimulateMsgCreateFixedAmountPlan(ak types.AccountKeeper, bk types.BankKeepe return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - // TODO: not implemented yet - return simtypes.OperationMsg{}, nil, nil + simAccount, _ := simtypes.RandomAcc(r, accs) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + params := k.GetParams(ctx) + _, hasNeg := spendable.SafeSub(params.PrivatePlanCreationFee) + if hasNeg { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateFixedAmountPlan, "insufficient balance for plan creation fee"), nil, nil + } + + // mint pool coins to simulate the real-world cases + poolCoins, err := mintPoolCoins(ctx, r, bk, simAccount) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateFixedAmountPlan, "unable to mint pool coins"), nil, nil + } + + name := "simulation-test-" + simtypes.RandStringOfLength(r, 5) // name must be unique + creatorAcc := account.GetAddress() + stakingCoinWeights := sdk.NewDecCoins(sdk.NewInt64DecCoin(sdk.DefaultBondDenom, 1)) + startTime := ctx.BlockTime() + endTime := startTime.AddDate(0, 1, 0) + epochAmount := sdk.NewCoins( + sdk.NewInt64Coin(poolCoins[r.Intn(3)].Denom, int64(simtypes.RandIntBetween(r, 10_000_000, 1_000_000_000))), + ) + + msg := types.NewMsgCreateFixedAmountPlan( + name, + creatorAcc, + stakingCoinWeights, + startTime, + endTime, + epochAmount, + ) + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) } } @@ -105,19 +155,94 @@ func SimulateMsgCreateRatioPlan(ak types.AccountKeeper, bk types.BankKeeper, k k return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - // TODO: not implemented yet - return simtypes.OperationMsg{}, nil, nil + simAccount, _ := simtypes.RandomAcc(r, accs) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + params := k.GetParams(ctx) + _, hasNeg := spendable.SafeSub(params.PrivatePlanCreationFee) + if hasNeg { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateRatioPlan, "insufficient balance for plan creation fee"), nil, nil + } + + // mint pool coins to simulate the real-world cases + _, err := mintPoolCoins(ctx, r, bk, simAccount) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgCreateFixedAmountPlan, "unable to mint pool coins"), nil, nil + } + + name := "simulation-test-" + simtypes.RandStringOfLength(r, 5) // name must be unique + creatorAcc := account.GetAddress() + stakingCoinWeights := sdk.NewDecCoins(sdk.NewInt64DecCoin(sdk.DefaultBondDenom, 1)) + startTime := ctx.BlockTime() + endTime := startTime.AddDate(0, 1, 0) + epochRatio := sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 1, 10)), 1) + + msg := types.NewMsgCreateRatioPlan( + name, + creatorAcc, + stakingCoinWeights, + startTime, + endTime, + epochRatio, + ) + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) } } -// SimulateMsgStake generates a MsgCreateFixedAmountPlan with random values +// SimulateMsgStake generates a MsgStake with random values // nolint: interfacer func SimulateMsgStake(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - // TODO: not implemented yet - return simtypes.OperationMsg{}, nil, nil + simAccount, _ := simtypes.RandomAcc(r, accs) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + farmer := account.GetAddress() + stakingCoins := sdk.NewCoins( + sdk.NewInt64Coin(sdk.DefaultBondDenom, int64(simtypes.RandIntBetween(r, 1_000_000, 1_000_000_000))), + ) + if !spendable.IsAllGTE(stakingCoins) { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUnstake, "insufficient funds"), nil, nil + } + + msg := types.NewMsgStake(farmer, stakingCoins) + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) } } @@ -127,18 +252,147 @@ func SimulateMsgUnstake(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Ke return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - // TODO: not implemented yet - return simtypes.OperationMsg{}, nil, nil + simAccount, _ := simtypes.RandomAcc(r, accs) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + farmer := account.GetAddress() + unstakingCoins := sdk.NewCoins( + sdk.NewInt64Coin(sdk.DefaultBondDenom, int64(simtypes.RandIntBetween(r, 1_000_000, 100_000_000))), + ) + + // staking must exist in order to unstake + staking, sf := k.GetStaking(ctx, sdk.DefaultBondDenom, farmer) + if !sf { + staking = types.Staking{ + Amount: sdk.ZeroInt(), + } + } + queuedStaking, qsf := k.GetQueuedStaking(ctx, sdk.DefaultBondDenom, farmer) + if !qsf { + if !qsf { + queuedStaking = types.QueuedStaking{ + Amount: sdk.ZeroInt(), + } + } + } + if !sf && !qsf { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUnstake, "unable to find staking and queued staking"), nil, nil + } + // sum of staked and queued coins must be greater than unstaking coins + if !staking.Amount.Add(queuedStaking.Amount).GTE(unstakingCoins[0].Amount) { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUnstake, "insufficient funds"), nil, nil + } + + // spendable must be greater than unstaking coins + if !spendable.IsAllGT(unstakingCoins) { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUnstake, "insufficient funds"), nil, nil + } + + msg := types.NewMsgUnstake(farmer, unstakingCoins) + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) } } -// SimulateMsgClaim generates a MsgClaim with random values +// SimulateMsgHarvest generates a MsgHarvest with random values // nolint: interfacer -func SimulateMsgClaim(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation { +func SimulateMsgHarvest(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - // TODO: not implemented yet - return simtypes.OperationMsg{}, nil, nil + // + // TODO: not fully implemented yet. It needs debugging why + // there are no rewards although it processes queued coins and distribute rewards + // + + var simAccount simtypes.Account + + // find staking from the simulated accounts + var ranStaking sdk.Coins + for _, acc := range accs { + staking := k.GetAllStakedCoinsByFarmer(ctx, acc.Address) + if !staking.IsZero() { + simAccount = acc + ranStaking = staking + break + } + } + + var stakingCoinDenoms []string + for _, coin := range ranStaking { + stakingCoinDenoms = append(stakingCoinDenoms, coin.Denom) + } + + var totalRewards sdk.Coins + for _, denom := range stakingCoinDenoms { + rewards, err := k.WithdrawRewards(ctx, simAccount.Address, denom) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgHarvest, "unable to withdraw rewards"), nil, nil + } + totalRewards = totalRewards.Add(rewards...) + } + + if totalRewards.IsZero() { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgHarvest, "no rewards to harvest"), nil, nil + } + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + msg := types.NewMsgHarvest(simAccount.Address, stakingCoinDenoms) + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +// mintPoolCoins mints random amount of coins with the provided pool coin denoms and +// send them to the simulated account. +func mintPoolCoins(ctx sdk.Context, r *rand.Rand, bk types.BankKeeper, acc simtypes.Account) (mintCoins sdk.Coins, err error) { + for _, denom := range []string{ + "pool93E069B333B5ECEBFE24C6E1437E814003248E0DD7FF8B9F82119F4587449BA5", + "pool3036F43CB8131A1A63D2B3D3B11E9CF6FA2A2B6FEC17D5AD283C25C939614A8C", + "poolE4D2617BFE03E1146F6BBA1D9893F2B3D77BA29E7ED532BB721A39FF1ECC1B07", + } { + mintCoins = mintCoins.Add(sdk.NewInt64Coin(denom, int64(simtypes.RandIntBetween(r, 1e14, 1e15)))) } + + if err := bk.MintCoins(ctx, types.ModuleName, mintCoins); err != nil { + return nil, err + } + + if err := bk.SendCoinsFromModuleToAccount(ctx, types.ModuleName, acc.Address, mintCoins); err != nil { + return nil, err + } + + return mintCoins, nil } diff --git a/x/farming/simulation/operations_test.go b/x/farming/simulation/operations_test.go new file mode 100644 index 00000000..54e2015e --- /dev/null +++ b/x/farming/simulation/operations_test.go @@ -0,0 +1,327 @@ +package simulation_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + abci "github.com/tendermint/tendermint/abci/types" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + farmingapp "github.com/tendermint/farming/app" + "github.com/tendermint/farming/app/params" + "github.com/tendermint/farming/x/farming/simulation" + "github.com/tendermint/farming/x/farming/types" +) + +// TestWeightedOperations tests the weights of the operations. +func TestWeightedOperations(t *testing.T) { + app, ctx := createTestApp(false) + + ctx.WithChainID("test-chain") + + cdc := app.AppCodec() + appParams := make(simtypes.AppParams) + + weightedOps := simulation.WeightedOperations(appParams, cdc, app.AccountKeeper, app.BankKeeper, app.FarmingKeeper) + + s := rand.NewSource(1) + r := rand.New(s) + accs := getTestingAccounts(t, r, app, ctx, 1) + + expected := []struct { + weight int + opMsgRoute string + opMsgName string + }{ + {params.DefaultWeightMsgCreateFixedAmountPlan, types.ModuleName, types.TypeMsgCreateFixedAmountPlan}, + {params.DefaultWeightMsgCreateRatioPlan, types.ModuleName, types.TypeMsgCreateRatioPlan}, + {params.DefaultWeightMsgStake, types.ModuleName, types.TypeMsgStake}, + {params.DefaultWeightMsgUnstake, types.ModuleName, types.TypeMsgUnstake}, + {params.DefaultWeightMsgHarvest, types.ModuleName, types.TypeMsgHarvest}, + } + + for i, w := range weightedOps { + operationMsg, _, _ := w.Op()(r, app.BaseApp, ctx, accs, ctx.ChainID()) + // the following checks are very much dependent from the ordering of the output given + // by WeightedOperations. if the ordering in WeightedOperations changes some tests + // will fail + require.Equal(t, expected[i].weight, w.Weight(), "weight should be the same") + require.Equal(t, expected[i].opMsgRoute, operationMsg.Route, "route should be the same") + require.Equal(t, expected[i].opMsgName, operationMsg.Name, "operation Msg name should be the same") + } +} + +// TestSimulateMsgCreateFixedAmountPlan tests the normal scenario of a valid message of type TypeMsgCreateFixedAmountPlan. +// Abnormal scenarios, where the message are created by an errors are not tested here. +func TestSimulateMsgCreateFixedAmountPlan(t *testing.T) { + app, ctx := createTestApp(false) + + // setup a single account + s := rand.NewSource(1) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 1) + + // setup randomly generated private plan creation fees + feeCoins := simulation.GenPrivatePlanCreationFee(r) + params := app.FarmingKeeper.GetParams(ctx) + params.PrivatePlanCreationFee = feeCoins + app.FarmingKeeper.SetParams(ctx, params) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + + // execute operation + op := simulation.SimulateMsgCreateFixedAmountPlan(app.AccountKeeper, app.BankKeeper, app.FarmingKeeper) + operationMsg, futureOperations, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg types.MsgCreateFixedAmountPlan + err = app.AppCodec().UnmarshalJSON(operationMsg.Msg, &msg) + require.NoError(t, err) + + require.True(t, operationMsg.OK) + require.Equal(t, types.TypeMsgCreateFixedAmountPlan, msg.Type()) + require.Equal(t, "simulation-test-GkqEG", msg.Name) + require.Equal(t, "cosmos1tnh2q55v8wyygtt9srz5safamzdengsnqeycj3", msg.Creator) + require.Equal(t, "1.000000000000000000stake", msg.StakingCoinWeights.String()) + require.Equal(t, "126410694pool3036F43CB8131A1A63D2B3D3B11E9CF6FA2A2B6FEC17D5AD283C25C939614A8C", msg.EpochAmount.String()) + require.Len(t, futureOperations, 0) +} + +// TestSimulateMsgCreateRatioPlan tests the normal scenario of a valid message of type TypeMsgCreateRatioPlan. +// Abnormal scenarios, where the message are created by an errors are not tested here. +func TestSimulateMsgCreateRatioPlan(t *testing.T) { + app, ctx := createTestApp(false) + + // setup a single account + s := rand.NewSource(1) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 1) + + // setup randomly generated private plan creation fees + feeCoins := simulation.GenPrivatePlanCreationFee(r) + params := app.FarmingKeeper.GetParams(ctx) + params.PrivatePlanCreationFee = feeCoins + app.FarmingKeeper.SetParams(ctx, params) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + + // execute operation + op := simulation.SimulateMsgCreateRatioPlan(app.AccountKeeper, app.BankKeeper, app.FarmingKeeper) + operationMsg, futureOperations, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg types.MsgCreateRatioPlan + err = app.AppCodec().UnmarshalJSON(operationMsg.Msg, &msg) + require.NoError(t, err) + + require.True(t, operationMsg.OK) + require.Equal(t, types.TypeMsgCreateRatioPlan, msg.Type()) + require.Equal(t, "simulation-test-GkqEG", msg.Name) + require.Equal(t, "cosmos1tnh2q55v8wyygtt9srz5safamzdengsnqeycj3", msg.Creator) + require.Equal(t, "1.000000000000000000stake", msg.StakingCoinWeights.String()) + require.Equal(t, "0.700000000000000000", msg.EpochRatio.String()) + require.Len(t, futureOperations, 0) +} + +// TestSimulateMsgStake tests the normal scenario of a valid message of type TypeMsgStake. +// Abnormal scenarios, where the message are created by an errors are not tested here. +func TestSimulateMsgStake(t *testing.T) { + app, ctx := createTestApp(false) + + // setup a single account + s := rand.NewSource(1) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 1) + + // setup randomly generated staking creation fees + params := app.FarmingKeeper.GetParams(ctx) + app.FarmingKeeper.SetParams(ctx, params) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + + // execute operation + op := simulation.SimulateMsgStake(app.AccountKeeper, app.BankKeeper, app.FarmingKeeper) + operationMsg, futureOperations, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg types.MsgStake + err = app.AppCodec().UnmarshalJSON(operationMsg.Msg, &msg) + require.NoError(t, err) + + require.True(t, operationMsg.OK) + require.Equal(t, types.TypeMsgStake, msg.Type()) + require.Equal(t, "cosmos1tnh2q55v8wyygtt9srz5safamzdengsnqeycj3", msg.Farmer) + require.Equal(t, "912902081stake", msg.StakingCoins.String()) + require.Len(t, futureOperations, 0) +} + +// TestSimulateMsgUnstake tests the normal scenario of a valid message of type TypeMsgUnstake. +// Abnormal scenarios, where the message are created by an errors are not tested here. +func TestSimulateMsgUnstake(t *testing.T) { + app, ctx := createTestApp(false) + + // setup a single account + s := rand.NewSource(1) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 1) + + // setup randomly generated staking creation fees + params := app.FarmingKeeper.GetParams(ctx) + app.FarmingKeeper.SetParams(ctx, params) + + // staking must exist in order to simulate unstake + stakingCoins := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 100_000_000)) + err := app.FarmingKeeper.Stake(ctx, accounts[0].Address, stakingCoins) + require.NoError(t, err) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + err = app.FarmingKeeper.AdvanceEpoch(ctx) + require.NoError(t, err) + + // execute operation + op := simulation.SimulateMsgUnstake(app.AccountKeeper, app.BankKeeper, app.FarmingKeeper) + operationMsg, futureOperations, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg types.MsgUnstake + err = app.AppCodec().UnmarshalJSON(operationMsg.Msg, &msg) + require.NoError(t, err) + + require.True(t, operationMsg.OK) + require.Equal(t, types.TypeMsgUnstake, msg.Type()) + require.Equal(t, "cosmos1tnh2q55v8wyygtt9srz5safamzdengsnqeycj3", msg.Farmer) + require.Equal(t, "21902081stake", msg.UnstakingCoins.String()) + require.Len(t, futureOperations, 0) +} + +// TestSimulateMsgHarvest tests the normal scenario of a valid message of type TypeMsgHarvest. +// Abnormal scenarios, where the message are created by an errors are not tested here. +func TestSimulateMsgHarvest(t *testing.T) { + app, ctx := createTestApp(false) + + // setup a single account + s := rand.NewSource(1) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 1) + + // setup epoch days to 1 to ease the test + params := app.FarmingKeeper.GetParams(ctx) + params.EpochDays = 1 + app.FarmingKeeper.SetParams(ctx, params) + + // setup a fixed amount plan + msgPlan := &types.MsgCreateFixedAmountPlan{ + Name: "simulation", + Creator: accounts[0].Address.String(), + StakingCoinWeights: sdk.NewDecCoins( + sdk.NewDecCoinFromDec(sdk.DefaultBondDenom, sdk.NewDecWithPrec(10, 1)), // 100% + ), + StartTime: mustParseRFC3339("0001-01-01T00:00:00Z"), + EndTime: mustParseRFC3339("9999-01-01T00:00:00Z"), + EpochAmount: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 200_000_000)), + } + + _, err := app.FarmingKeeper.CreateFixedAmountPlan( + ctx, + msgPlan, + accounts[0].Address, + accounts[0].Address, + types.PlanTypePrivate, + ) + require.NoError(t, err) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + + // set staking + stakingCoins := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 100_000_000)) + err = app.FarmingKeeper.Stake(ctx, accounts[0].Address, stakingCoins) + require.NoError(t, err) + + queuedStaking, found := app.FarmingKeeper.GetQueuedStaking(ctx, sdk.DefaultBondDenom, accounts[0].Address) + require.Equal(t, true, found) + require.Equal(t, true, queuedStaking.Amount.IsPositive()) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + err = app.FarmingKeeper.AdvanceEpoch(ctx) + require.NoError(t, err) + + // check that queue coins are moved to staked coins + staking, found := app.FarmingKeeper.GetStaking(ctx, sdk.DefaultBondDenom, accounts[0].Address) + require.Equal(t, true, found) + require.Equal(t, true, staking.Amount.IsPositive()) + queuedStaking, found = app.FarmingKeeper.GetQueuedStaking(ctx, sdk.DefaultBondDenom, accounts[0].Address) + require.Equal(t, false, found) + + // begin a new block + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1, AppHash: app.LastCommitID().Hash}}) + err = app.FarmingKeeper.AdvanceEpoch(ctx) + require.NoError(t, err) + + // execute operation + op := simulation.SimulateMsgHarvest(app.AccountKeeper, app.BankKeeper, app.FarmingKeeper) + operationMsg, futureOperations, err := op(r, app.BaseApp, ctx, accounts, "") + require.NoError(t, err) + + var msg types.MsgHarvest + err = app.AppCodec().UnmarshalJSON(operationMsg.Msg, &msg) + require.NoError(t, err) + + require.True(t, operationMsg.OK) + require.Equal(t, types.TypeMsgHarvest, msg.Type()) + require.Equal(t, "cosmos1tnh2q55v8wyygtt9srz5safamzdengsnqeycj3", msg.Farmer) + require.Equal(t, []string{"stake"}, msg.StakingCoinDenoms) + require.Len(t, futureOperations, 0) +} + +func createTestApp(isCheckTx bool) (*farmingapp.FarmingApp, sdk.Context) { + app := farmingapp.Setup(isCheckTx) + + ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{}) + app.MintKeeper.SetParams(ctx, minttypes.DefaultParams()) + app.MintKeeper.SetMinter(ctx, minttypes.DefaultInitialMinter()) + + return app, ctx +} + +func getTestingAccounts(t *testing.T, r *rand.Rand, app *farmingapp.FarmingApp, ctx sdk.Context, n int) []simtypes.Account { + accounts := simtypes.RandomAccounts(r, n) + + initAmt := app.StakingKeeper.TokensFromConsensusPower(ctx, 100_000_000_000) + initCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, initAmt)) + + // add coins to the accounts + for _, account := range accounts { + acc := app.AccountKeeper.NewAccountWithAddress(ctx, account.Address) + app.AccountKeeper.SetAccount(ctx, acc) + err := simapp.FundAccount(app.BankKeeper, ctx, account.Address, initCoins) + require.NoError(t, err) + } + + return accounts +} + +func mustParseRFC3339(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(err) + } + return t +} diff --git a/x/farming/simulation/params.go b/x/farming/simulation/params.go index 6eb610ab..e10a36a6 100644 --- a/x/farming/simulation/params.go +++ b/x/farming/simulation/params.go @@ -13,12 +13,26 @@ import ( ) // ParamChanges defines the parameters that can be modified by param change proposals -// on the simulation +// on the simulation. func ParamChanges(r *rand.Rand) []simtypes.ParamChange { return []simtypes.ParamChange{ simulation.NewSimParamChange(types.ModuleName, string(types.KeyPrivatePlanCreationFee), func(r *rand.Rand) string { - return fmt.Sprintf("\"%s\"", GenPrivatePlanCreationFee(r)) + bz, err := GenPrivatePlanCreationFee(r).MarshalJSON() + if err != nil { + panic(err) + } + return string(bz) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.KeyEpochDays), + func(r *rand.Rand) string { + return fmt.Sprintf("%d", GenEpochDays(r)) + }, + ), + simulation.NewSimParamChange(types.ModuleName, string(types.KeyFarmingFeeCollector), + func(r *rand.Rand) string { + return fmt.Sprintf("\"%s\"", GenFarmingFeeCollector(r)) }, ), } diff --git a/x/farming/simulation/params_test.go b/x/farming/simulation/params_test.go index c3d11341..fd93cd39 100644 --- a/x/farming/simulation/params_test.go +++ b/x/farming/simulation/params_test.go @@ -19,12 +19,13 @@ func TestParamChanges(t *testing.T) { simValue string subspace string }{ - {"farming/PrivatePlanCreationFee", "PrivatePlanCreationFee", "\"100000000stake\"", "farming"}, + {"farming/PrivatePlanCreationFee", "PrivatePlanCreationFee", "[{\"denom\":\"stake\",\"amount\":\"98498081\"}]", "farming"}, + {"farming/EpochDays", "EpochDays", "7", "farming"}, + {"farming/FarmingFeeCollector", "FarmingFeeCollector", "\"cosmos1h292smhhttwy0rl3qr4p6xsvpvxc4v05s6rxtczwq3cs6qc462mqejwy8x\"", "farming"}, } paramChanges := simulation.ParamChanges(r) - - require.Len(t, paramChanges, 1) + require.Len(t, paramChanges, 3) for i, p := range paramChanges { require.Equal(t, expected[i].composedKey, p.ComposedKey()) diff --git a/x/farming/simulation/proposals.go b/x/farming/simulation/proposals.go new file mode 100644 index 00000000..ca8f4d78 --- /dev/null +++ b/x/farming/simulation/proposals.go @@ -0,0 +1,201 @@ +package simulation + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/tendermint/farming/app/params" + "github.com/tendermint/farming/x/farming/keeper" + "github.com/tendermint/farming/x/farming/types" +) + +/* +[TODO]: + We need to come up with better ways to simulate public plan proposals. + Currently, the details are igored and only basic logics are written to simulate. + + These are some of the following considerations that i think need to be discussed and addressed: + 1. Randomize staking coin weights (single or multiple denoms) + 2. Simulate multiple proposals (add new weighted proposal content for multiple plans?) +*/ + +// Simulation operation weights constants. +const ( + OpWeightSimulateAddPublicPlanProposal = "op_weight_add_public_plan_proposal" + OpWeightSimulateUpdatePublicPlanProposal = "op_weight_update_public_plan_proposal" + OpWeightSimulateDeletePublicPlanProposal = "op_weight_delete_public_plan_proposal" +) + +// ProposalContents defines the module weighted proposals' contents +func ProposalContents(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) []simtypes.WeightedProposalContent { + return []simtypes.WeightedProposalContent{ + simulation.NewWeightedProposalContent( + OpWeightSimulateAddPublicPlanProposal, + params.DefaultWeightAddPublicPlanProposal, + SimulateAddPublicPlanProposal(ak, bk, k), + ), + simulation.NewWeightedProposalContent( + OpWeightSimulateUpdatePublicPlanProposal, + params.DefaultWeightUpdatePublicPlanProposal, + SimulateUpdatePublicPlanProposal(ak, bk, k), + ), + simulation.NewWeightedProposalContent( + OpWeightSimulateDeletePublicPlanProposal, + params.DefaultWeightDeletePublicPlanProposal, + SimulateDeletePublicPlanProposal(ak, bk, k), + ), + } +} + +// SimulateAddPublicPlanProposal generates random public plan proposal content +func SimulateAddPublicPlanProposal(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + simAccount, _ := simtypes.RandomAcc(r, accs) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + params := k.GetParams(ctx) + _, hasNeg := spendable.SafeSub(params.PrivatePlanCreationFee) + if hasNeg { + return nil + } + + poolCoins, err := mintPoolCoins(ctx, r, bk, simAccount) + if err != nil { + return nil + } + + // add request proposal + req := &types.AddRequestProposal{ + Name: "simulation-test-" + simtypes.RandStringOfLength(r, 5), + FarmingPoolAddress: simAccount.Address.String(), + TerminationAddress: simAccount.Address.String(), + StakingCoinWeights: sdk.NewDecCoins(sdk.NewInt64DecCoin(sdk.DefaultBondDenom, 1)), + StartTime: ctx.BlockTime(), + EndTime: ctx.BlockTime().AddDate(0, 1, 0), + EpochAmount: sdk.NewCoins(sdk.NewInt64Coin(poolCoins[r.Intn(3)].Denom, int64(simtypes.RandIntBetween(r, 10_000_000, 1_000_000_000)))), + EpochRatio: sdk.ZeroDec(), + } + addRequests := []*types.AddRequestProposal{req} + + return types.NewPublicPlanProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 100), + addRequests, + []*types.UpdateRequestProposal{}, + []*types.DeleteRequestProposal{}, + ) + } +} + +// SimulateUpdatePublicPlanProposal generates random public plan proposal content +func SimulateUpdatePublicPlanProposal(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + simAccount, _ := simtypes.RandomAcc(r, accs) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + params := k.GetParams(ctx) + _, hasNeg := spendable.SafeSub(params.PrivatePlanCreationFee) + if hasNeg { + return nil + } + + poolCoins, err := mintPoolCoins(ctx, r, bk, simAccount) + if err != nil { + return nil + } + + req := &types.UpdateRequestProposal{} + + // TODO: decide which values of fields to randomize + plans := k.GetAllPlans(ctx) + for _, p := range plans { + if p.GetType() == types.PlanTypePublic { + startTime := ctx.BlockTime() + endTime := startTime.AddDate(0, 1, 0) + + switch plan := p.(type) { + case *types.FixedAmountPlan: + req.PlanId = plan.GetId() + req.Name = plan.GetName() + req.FarmingPoolAddress = plan.GetFarmingPoolAddress().String() + req.TerminationAddress = plan.GetTerminationAddress().String() + req.StakingCoinWeights = plan.GetStakingCoinWeights() + req.StartTime = &startTime + req.EndTime = &endTime + req.EpochAmount = sdk.NewCoins(sdk.NewInt64Coin(poolCoins[r.Intn(3)].Denom, int64(simtypes.RandIntBetween(r, 10_000_000, 1_000_000_000)))) + case *types.RatioPlan: + req.PlanId = plan.GetId() + req.Name = plan.GetName() + req.FarmingPoolAddress = plan.GetFarmingPoolAddress().String() + req.TerminationAddress = plan.GetTerminationAddress().String() + req.StakingCoinWeights = plan.GetStakingCoinWeights() + req.StartTime = &startTime + req.EndTime = &endTime + req.EpochRatio = sdk.NewDecWithPrec(int64(simtypes.RandIntBetween(r, 1, 10)), 1) + } + break + } + } + + if req.PlanId == 0 { + return nil + } + + updateRequests := []*types.UpdateRequestProposal{req} + + return types.NewPublicPlanProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 100), + []*types.AddRequestProposal{}, + updateRequests, + []*types.DeleteRequestProposal{}, + ) + } +} + +// SimulateDeletePublicPlanProposal generates random public plan proposal content +func SimulateDeletePublicPlanProposal(ak types.AccountKeeper, bk types.BankKeeper, k keeper.Keeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + simAccount, _ := simtypes.RandomAcc(r, accs) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + params := k.GetParams(ctx) + _, hasNeg := spendable.SafeSub(params.PrivatePlanCreationFee) + if hasNeg { + return nil + } + + req := &types.DeleteRequestProposal{} + + plans := k.GetAllPlans(ctx) + for _, p := range plans { + if p.GetType() == types.PlanTypePublic { + req.PlanId = p.GetId() + break + } + } + + if req.PlanId == 0 { + return nil + } + + deleteRequest := []*types.DeleteRequestProposal{req} + + return types.NewPublicPlanProposal( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 100), + []*types.AddRequestProposal{}, + []*types.UpdateRequestProposal{}, + deleteRequest, + ) + } +} diff --git a/x/farming/simulation/proposals_test.go b/x/farming/simulation/proposals_test.go new file mode 100644 index 00000000..24e81bd5 --- /dev/null +++ b/x/farming/simulation/proposals_test.go @@ -0,0 +1,84 @@ +package simulation_test + +import ( + "math/rand" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/tendermint/farming/app/params" + "github.com/tendermint/farming/x/farming/simulation" + "github.com/tendermint/farming/x/farming/types" +) + +func TestProposalContents(t *testing.T) { + app, ctx := createTestApp(false) + + // initialize parameters + s := rand.NewSource(1) + r := rand.New(s) + + accounts := getTestingAccounts(t, r, app, ctx, 1) + + // execute ProposalContents function + weightedProposalContent := simulation.ProposalContents(app.AccountKeeper, app.BankKeeper, app.FarmingKeeper) + require.Len(t, weightedProposalContent, 3) + + w0 := weightedProposalContent[0] + w1 := weightedProposalContent[1] + w2 := weightedProposalContent[2] + + // tests w0 interface: + require.Equal(t, simulation.OpWeightSimulateAddPublicPlanProposal, w0.AppParamsKey()) + require.Equal(t, params.DefaultWeightAddPublicPlanProposal, w0.DefaultWeight()) + + // tests w1 interface: + require.Equal(t, simulation.OpWeightSimulateUpdatePublicPlanProposal, w1.AppParamsKey()) + require.Equal(t, params.DefaultWeightUpdatePublicPlanProposal, w1.DefaultWeight()) + + // tests w2 interface: + require.Equal(t, simulation.OpWeightSimulateDeletePublicPlanProposal, w2.AppParamsKey()) + require.Equal(t, params.DefaultWeightDeletePublicPlanProposal, w2.DefaultWeight()) + + content0 := w0.ContentSimulatorFn()(r, ctx, accounts) + + require.Equal(t, "yNhYFmBZHe", content0.GetTitle()) + require.Equal(t, "weXhSUkMhPjMaxKlMIJMOXcnQfyzeOcbWwNbeHVIkPZBSpYuLyYggwexjxusrBqDOTtGTOWeLrQKjLxzIivHSlcxgdXhhuTSkuxK", content0.GetDescription()) + require.Equal(t, "farming", content0.ProposalRoute()) + require.Equal(t, "PublicPlan", content0.ProposalType()) + + // setup public fixed amount plan + msgPlan := &types.MsgCreateFixedAmountPlan{ + Name: "simulation", + Creator: accounts[0].Address.String(), + StakingCoinWeights: sdk.NewDecCoins( + sdk.NewDecCoinFromDec(sdk.DefaultBondDenom, sdk.NewDecWithPrec(10, 1)), // 100% + ), + StartTime: mustParseRFC3339("2021-08-01T00:00:00Z"), + EndTime: mustParseRFC3339("2021-08-31T00:00:00Z"), + EpochAmount: sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 200_000_000)), + } + + _, err := app.FarmingKeeper.CreateFixedAmountPlan( + ctx, + msgPlan, + accounts[0].Address, + accounts[0].Address, + types.PlanTypePublic, + ) + require.NoError(t, err) + + content1 := w1.ContentSimulatorFn()(r, ctx, accounts) + require.Equal(t, "GNoFBIHxvi", content1.GetTitle()) + require.Equal(t, "TVpQoottZyPFfNOoMioXHRuFwMRYUiKvcWPkrayyTLOCFJlAyslDameIuqVAuxErqFPEWIScKpBORIuZqoXlZuTvAjEdlEWDODFR", content1.GetDescription()) + require.Equal(t, "farming", content1.ProposalRoute()) + require.Equal(t, "PublicPlan", content1.ProposalType()) + + content2 := w2.ContentSimulatorFn()(r, ctx, accounts) + require.Equal(t, "MhptXaxIxg", content2.GetTitle()) + require.Equal(t, "MBcObErwgTDNGWnwQMUgFFSKtPDMEoEQCTKVREqrXZSGLqwTMcxHfWotDllNkIJPMbXzjDVjPOOjCFuIvTyhXKLyhUScOXvYthRX", content2.GetDescription()) + require.Equal(t, "farming", content2.ProposalRoute()) + require.Equal(t, "PublicPlan", content2.ProposalType()) + +} diff --git a/x/farming/spec/03_state_transitions.md b/x/farming/spec/03_state_transitions.md index 24cff93d..9eaf837b 100644 --- a/x/farming/spec/03_state_transitions.md +++ b/x/farming/spec/03_state_transitions.md @@ -42,7 +42,6 @@ const ( ## Staking - New `Staking` object is created when a farmer creates a staking, and when the farmer does not have existing `Staking`. -- When a farmer creates new staking, the farmer should pay `StakingCreationFee` to prevent spamming. - When a farmer add/remove stakings to/from existing `Staking`, `StakedCoins` and `QueuedCoins` are updated in the corresponding `Staking`. - `QueuedCoins` : newly staked coins are in this status until end of current epoch, and then migrated to `StakedCoins` at the end of current epoch. - When a farmer unstakes, `QueuedCoins` are unstaked first, and then `StakedCoins`. diff --git a/x/farming/spec/08_params.md b/x/farming/spec/08_params.md index fb50590a..e6fcf4e1 100644 --- a/x/farming/spec/08_params.md +++ b/x/farming/spec/08_params.md @@ -7,7 +7,6 @@ The farming module contains the following parameters: | Key | Type | Example | | -------------------------- | --------- | ------------------------------------------------------------------- | | PrivatePlanCreationFee | sdk.Coins | [{"denom":"stake","amount":"100000000"}] | -| StakingCreationFee | sdk.Coins | [{"denom":"stake","amount":"100000"}] | | EpochDays | uint32 | 1 | | FarmingFeeCollector | string | "cosmos1h292smhhttwy0rl3qr4p6xsvpvxc4v05s6rxtczwq3cs6qc462mqejwy8x" | @@ -15,10 +14,6 @@ The farming module contains the following parameters: Fee paid for to create a Private type Farming plan. This fee prevents spamming and is collected in in the community pool of the distribution module. -## StakingCreationFee - -When a farmer creates new `Staking`, the farmer needs to pay `StakingCreationFee` to prevent spam on the `Staking` struct. - ## EpochDays The universal epoch length in number of days. Every process for staking and reward distribution is executed with this `EpochDays` frequency. diff --git a/x/farming/types/codec.go b/x/farming/types/codec.go index debe0c11..58439c78 100644 --- a/x/farming/types/codec.go +++ b/x/farming/types/codec.go @@ -7,15 +7,15 @@ import ( govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" ) -//// RegisterLegacyAminoCodec registers the necessary x/farming interfaces and concrete types -//// on the provided LegacyAmino codec. These types are used for Amino JSON serialization. -//func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { -// cdc.RegisterConcrete(&MsgCreateFixedAmountPlan{}, "farming/MsgCreateFixedAmountPlan", nil) -// cdc.RegisterConcrete(&MsgCreateRatioPlan{}, "farming/MsgCreateRatioPlan", nil) -// cdc.RegisterConcrete(&MsgStake{}, "farming/MsgStake", nil) -// cdc.RegisterConcrete(&MsgUnstake{}, "farming/MsgUnstake", nil) -// cdc.RegisterConcrete(&MsgHarvest{}, "farming/MsgHarvest", nil) -//} +// // RegisterLegacyAminoCodec registers the necessary x/farming interfaces and concrete types +// // on the provided LegacyAmino codec. These types are used for Amino JSON serialization. +// func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { +// cdc.RegisterConcrete(&MsgCreateFixedAmountPlan{}, "farming/MsgCreateFixedAmountPlan", nil) +// cdc.RegisterConcrete(&MsgCreateRatioPlan{}, "farming/MsgCreateRatioPlan", nil) +// cdc.RegisterConcrete(&MsgStake{}, "farming/MsgStake", nil) +// cdc.RegisterConcrete(&MsgUnstake{}, "farming/MsgUnstake", nil) +// cdc.RegisterConcrete(&MsgHarvest{}, "farming/MsgHarvest", nil) +// } // RegisterInterfaces registers the x/farming interfaces types with the interface registry func RegisterInterfaces(registry types.InterfaceRegistry) { @@ -43,20 +43,20 @@ func RegisterInterfaces(registry types.InterfaceRegistry) { msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } -//var ( -// amino = codec.NewLegacyAmino() -// -// // ModuleCdc references the global x/farming module codec. Note, the codec -// // should ONLY be used in certain instances of tests and for JSON encoding as Amino -// // is still used for that purpose. -// // -// // The actual codec used for serialization should be provided to x/farming and -// // defined at the application level. -// ModuleCdc = codec.NewAminoCodec(amino) -//) - -func init() { - //RegisterLegacyAminoCodec(amino) - //cryptocodec.RegisterCrypto(amino) - //amino.Seal() -} +// var ( +// amino = codec.NewLegacyAmino() + +// // ModuleCdc references the global x/farming module codec. Note, the codec +// // should ONLY be used in certain instances of tests and for JSON encoding as Amino +// // is still used for that purpose. +// // +// // The actual codec used for serialization should be provided to x/farming and +// // defined at the application level. +// ModuleCdc = codec.NewAminoCodec(amino) +// ) + +// func init() { +// RegisterLegacyAminoCodec(amino) +// cryptocodec.RegisterCrypto(amino) +// amino.Seal() +// } diff --git a/x/farming/types/expected_keepers.go b/x/farming/types/expected_keepers.go index 30cfbd12..5984cb5b 100644 --- a/x/farming/types/expected_keepers.go +++ b/x/farming/types/expected_keepers.go @@ -9,10 +9,16 @@ import ( type BankKeeper interface { SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + //GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error } // AccountKeeper defines the expected account keeper type AccountKeeper interface { + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI GetModuleAddress(name string) sdk.AccAddress GetModuleAccount(ctx sdk.Context, moduleName string) authtypes.ModuleAccountI SetModuleAccount(sdk.Context, authtypes.ModuleAccountI) diff --git a/x/farming/types/genesis.go b/x/farming/types/genesis.go index 97bc7859..0c988ab1 100644 --- a/x/farming/types/genesis.go +++ b/x/farming/types/genesis.go @@ -45,7 +45,9 @@ func ValidateGenesis(data GenesisState) error { if err := data.Params.Validate(); err != nil { return err } + id := uint64(0) + var plans []PlanI for _, record := range data.PlanRecords { if err := record.Validate(); err != nil { @@ -61,8 +63,12 @@ func ValidateGenesis(data GenesisState) error { plans = append(plans, plan) id = plan.GetId() + 1 } - err := ValidateRatioPlans(plans) - if err != nil { + + if err := ValidateName(plans); err != nil { + return err + } + + if err := ValidateTotalEpochRatio(plans); err != nil { return err } @@ -70,9 +76,11 @@ func ValidateGenesis(data GenesisState) error { if err := data.RewardPoolCoins.Validate(); err != nil { return err } + if err := data.StakingReserveCoins.Validate(); err != nil { return err } + return nil } diff --git a/x/farming/types/plan.go b/x/farming/types/plan.go index 63198b17..08a72a94 100644 --- a/x/farming/types/plan.go +++ b/x/farming/types/plan.go @@ -249,16 +249,33 @@ type PlanI interface { Validate() error } -// ValidateRatioPlans validates a farmer's total epoch ratio and plan name. -// A total epoch ratio cannot be higher than 1 and plan name must not be duplicate. -func ValidateRatioPlans(i interface{}) error { +// ValidateName validates duplicate plan name value. +func ValidateName(i interface{}) error { + plans, ok := i.([]PlanI) + if !ok { + return sdkerrors.Wrapf(ErrInvalidPlanType, "invalid plan type %T", i) + } + + names := map[string]struct{}{} + + for _, plan := range plans { + if _, ok := names[plan.GetName()]; ok { + return sdkerrors.Wrap(ErrDuplicatePlanName, plan.GetName()) + } + names[plan.GetName()] = struct{}{} + } + + return nil +} + +// ValidateTotalEpochRatio validates a farmer's total epoch ratio that must be equal to 1. +func ValidateTotalEpochRatio(i interface{}) error { plans, ok := i.([]PlanI) if !ok { return sdkerrors.Wrapf(ErrInvalidPlanType, "invalid plan type %T", i) } totalEpochRatio := make(map[string]sdk.Dec) - names := make(map[string]bool) for _, plan := range plans { farmingPoolAddr := plan.GetFarmingPoolAddress().String() @@ -273,11 +290,6 @@ func ValidateRatioPlans(i interface{}) error { } else { totalEpochRatio[farmingPoolAddr] = plan.EpochRatio } - - if _, ok := names[plan.Name]; ok { - return sdkerrors.Wrap(ErrDuplicatePlanName, plan.Name) - } - names[plan.Name] = true } } diff --git a/x/farming/types/plan_test.go b/x/farming/types/plan_test.go index ea19c256..e6681c16 100644 --- a/x/farming/types/plan_test.go +++ b/x/farming/types/plan_test.go @@ -13,9 +13,8 @@ import ( "github.com/tendermint/farming/x/farming/types" ) -func TestRatioPlans(t *testing.T) { - name1 := "testPlan1" - name2 := "testPlan2" +func TestPlanName(t *testing.T) { + name := "testPlan1" farmingPoolAddr1 := sdk.AccAddress("farmingPoolAddr1") terminationAddr1 := sdk.AccAddress("terminationAddr1") stakingCoinWeights := sdk.NewDecCoins( @@ -31,7 +30,7 @@ func TestRatioPlans(t *testing.T) { { []types.PlanI{ types.NewRatioPlan( - types.NewBasePlan(1, name1, 1, farmingPoolAddr1.String(), terminationAddr1.String(), stakingCoinWeights, startTime, endTime), + types.NewBasePlan(1, name, 1, farmingPoolAddr1.String(), terminationAddr1.String(), stakingCoinWeights, startTime, endTime), sdk.NewDec(1), ), }, @@ -40,15 +39,52 @@ func TestRatioPlans(t *testing.T) { { []types.PlanI{ types.NewRatioPlan( - types.NewBasePlan(1, name1, 1, farmingPoolAddr1.String(), terminationAddr1.String(), stakingCoinWeights, startTime, endTime), + types.NewBasePlan(1, name, 1, farmingPoolAddr1.String(), terminationAddr1.String(), stakingCoinWeights, startTime, endTime), sdk.NewDec(1), ), + types.NewRatioPlan( + types.NewBasePlan(1, name, 1, farmingPoolAddr1.String(), terminationAddr1.String(), stakingCoinWeights, startTime, endTime), + sdk.NewDec(1), + ), + }, + sdkerrors.Wrap(types.ErrDuplicatePlanName, name), + }, + } + + for _, tc := range testCases { + err := types.ValidateName(tc.plans) + if tc.expectedErr == nil { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Equal(t, tc.expectedErr.Error(), err.Error()) + } + } +} + +func TestTotalEpochRatio(t *testing.T) { + name1 := "testPlan1" + name2 := "testPlan2" + farmingPoolAddr1 := sdk.AccAddress("farmingPoolAddr1") + terminationAddr1 := sdk.AccAddress("terminationAddr1") + stakingCoinWeights := sdk.NewDecCoins( + sdk.DecCoin{Denom: "testFarmStakingCoinDenom", Amount: sdk.MustNewDecFromStr("1.0")}, + ) + startTime := time.Now().UTC() + endTime := startTime.AddDate(1, 0, 0) + + testCases := []struct { + plans []types.PlanI + expectedErr error + }{ + { + []types.PlanI{ types.NewRatioPlan( types.NewBasePlan(1, name1, 1, farmingPoolAddr1.String(), terminationAddr1.String(), stakingCoinWeights, startTime, endTime), sdk.NewDec(1), ), }, - sdkerrors.Wrap(types.ErrDuplicatePlanName, name1), + nil, }, { []types.PlanI{ @@ -66,7 +102,7 @@ func TestRatioPlans(t *testing.T) { } for _, tc := range testCases { - err := types.ValidateRatioPlans(tc.plans) + err := types.ValidateTotalEpochRatio(tc.plans) if tc.expectedErr == nil { require.NoError(t, err) } else { @@ -85,3 +121,53 @@ func TestPrivatePlanFarmingPoolAddress(t *testing.T) { require.Equal(t, testAcc2, sdk.AccAddress(address.Module(types.ModuleName, []byte("PrivatePlan|1|test2")))) require.Equal(t, "cosmos172yhzhxwgwul3s8m6qpgw2ww3auedq4k3dt224543d0sd44fgx4spcjthr", testAcc2.String()) } + +// TODO: needs to cover more cases +// https://github.com/tendermint/farming/issues/90 +func TestUnpackPlan(t *testing.T) { + plan := []types.PlanI{ + types.NewRatioPlan( + types.NewBasePlan( + 1, + "testPlan1", + types.PlanTypePrivate, + types.PrivatePlanFarmingPoolAddress("farmingPoolAddr1", 1).String(), + sdk.AccAddress("terminationAddr1").String(), + sdk.NewDecCoins(sdk.DecCoin{Denom: "testFarmStakingCoinDenom", Amount: sdk.MustNewDecFromStr("1.0")}), + mustParseRFC3339("2021-08-03T00:00:00Z"), + mustParseRFC3339("2021-08-07T00:00:00Z"), + ), + sdk.NewDec(1), + ), + } + + any, err := types.PackPlan(plan[0]) + require.NoError(t, err) + + marshaled, err := any.Marshal() + require.NoError(t, err) + + any.Value = []byte{} + err = any.Unmarshal(marshaled) + require.NoError(t, err) + + reMarshal, err := any.Marshal() + require.NoError(t, err) + require.Equal(t, marshaled, reMarshal) + + planRecord := types.PlanRecord{ + Plan: *any, + FarmingPoolCoins: sdk.NewCoins(), + } + + _, err = types.UnpackPlan(&planRecord.Plan) + require.NoError(t, err) +} + +func mustParseRFC3339(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(err) + } + return t +} diff --git a/x/farming/types/proposal.go b/x/farming/types/proposal.go index f3d4523c..63f4fa36 100644 --- a/x/farming/types/proposal.go +++ b/x/farming/types/proposal.go @@ -20,15 +20,20 @@ func init() { gov.RegisterProposalTypeCodec(&PublicPlanProposal{}, "cosmos-sdk/PublicPlanProposal") } -func NewPublicPlanProposal(title, description string, addReq []*AddRequestProposal, - updateReq []*UpdateRequestProposal, deleteReq []*DeleteRequestProposal) (gov.Content, error) { +func NewPublicPlanProposal( + title string, + description string, + addReq []*AddRequestProposal, + updateReq []*UpdateRequestProposal, + deleteReq []*DeleteRequestProposal, +) *PublicPlanProposal { return &PublicPlanProposal{ Title: title, Description: description, AddRequestProposals: addReq, UpdateRequestProposals: updateReq, DeleteRequestProposals: deleteReq, - }, nil + } } func (p *PublicPlanProposal) GetTitle() string { return p.Title }