diff --git a/app/app.go b/app/app.go index 0f097d74..5edba683 100644 --- a/app/app.go +++ b/app/app.go @@ -30,6 +30,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/cosmos/cosmos-sdk/x/upgrade" + upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client" "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/bep3" @@ -61,10 +63,14 @@ var ( staking.AppModuleBasic{}, mint.AppModuleBasic{}, distr.AppModuleBasic{}, - gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler, committee.ProposalHandler), + gov.NewAppModuleBasic( + paramsclient.ProposalHandler, distr.ProposalHandler, committee.ProposalHandler, + upgradeclient.ProposalHandler, + ), params.AppModuleBasic{}, crisis.AppModuleBasic{}, slashing.AppModuleBasic{}, + upgrade.AppModuleBasic{}, supply.AppModuleBasic{}, evidence.AppModuleBasic{}, auction.AppModuleBasic{}, @@ -89,7 +95,7 @@ var ( cdp.ModuleName: {supply.Minter, supply.Burner}, cdp.LiquidatorMacc: {supply.Minter, supply.Burner}, cdp.SavingsRateMacc: {supply.Minter}, - bep3.ModuleName: {supply.Minter, supply.Burner}, + bep3.ModuleName: nil, kavadist.ModuleName: {supply.Minter}, } ) @@ -118,6 +124,7 @@ type App struct { distrKeeper distr.Keeper govKeeper gov.Keeper crisisKeeper crisis.Keeper + upgradeKeeper upgrade.Keeper paramsKeeper params.Keeper evidenceKeeper evidence.Keeper vvKeeper validatorvesting.Keeper @@ -138,7 +145,7 @@ type App struct { // NewApp returns a reference to an initialized App. func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, - invCheckPeriod uint, + skipUpgradeHeights map[int64]bool, invCheckPeriod uint, baseAppOptions ...func(*bam.BaseApp)) *App { cdc := MakeCodec() @@ -150,9 +157,9 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, keys := sdk.NewKVStoreKeys( bam.MainStoreKey, auth.StoreKey, staking.StoreKey, supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey, - gov.StoreKey, params.StoreKey, evidence.StoreKey, validatorvesting.StoreKey, - auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey, - kavadist.StoreKey, incentive.StoreKey, committee.StoreKey, + gov.StoreKey, params.StoreKey, upgrade.StoreKey, evidence.StoreKey, + validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, + bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, committee.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(params.TStoreKey) @@ -236,6 +243,11 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.supplyKeeper, auth.FeeCollectorName, ) + app.upgradeKeeper = upgrade.NewKeeper( + skipUpgradeHeights, + keys[upgrade.StoreKey], + app.cdc, + ) // create evidence keeper with router evidenceKeeper := evidence.NewKeeper( @@ -269,6 +281,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, AddRoute(gov.RouterKey, gov.ProposalHandler). AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)). + AddRoute(upgrade.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.upgradeKeeper)). AddRoute(committee.RouterKey, committee.NewProposalHandler(app.committeeKeeper)) app.govKeeper = gov.NewKeeper( app.cdc, @@ -311,6 +324,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.cdc, keys[bep3.StoreKey], app.supplyKeeper, + app.accountKeeper, bep3Subspace, ) app.kavadistKeeper = kavadist.NewKeeper( @@ -346,6 +360,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, slashing.NewAppModule(app.slashingKeeper, app.accountKeeper, app.stakingKeeper), distr.NewAppModule(app.distrKeeper, app.accountKeeper, app.supplyKeeper, app.stakingKeeper), staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), + upgrade.NewAppModule(app.upgradeKeeper), evidence.NewAppModule(app.evidenceKeeper), validatorvesting.NewAppModule(app.vvKeeper, app.accountKeeper), auction.NewAppModule(app.auctionKeeper, app.accountKeeper, app.supplyKeeper), @@ -362,7 +377,11 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // CanWithdrawInvariant invariant. // Auction.BeginBlocker will close out expired auctions and pay debt back to cdp. // So it should be run before cdp.BeginBlocker which cancels out debt with stable and starts more auctions. - app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName, bep3.ModuleName, incentive.ModuleName, committee.ModuleName) + app.mm.SetOrderBeginBlockers( + upgrade.ModuleName, mint.ModuleName, distr.ModuleName, slashing.ModuleName, + validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName, + bep3.ModuleName, incentive.ModuleName, committee.ModuleName, + ) app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName) diff --git a/app/app_test.go b/app/app_test.go index 263c16b7..0ca09394 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -16,11 +16,11 @@ import ( func TestExport(t *testing.T) { db := db.NewMemDB() - app := NewApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, 0) + app := NewApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, 0) setGenesis(app) // Making a new app object with the db, so that initchain hasn't been called - newApp := NewApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, 0) + newApp := NewApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, 0) _, _, err := newApp.ExportAppStateAndValidators(false, []string{}) require.NoError(t, err, "ExportAppStateAndValidators should not have an error") } @@ -28,7 +28,7 @@ func TestExport(t *testing.T) { // ensure that black listed addresses are properly set in bank keeper func TestBlackListedAddrs(t *testing.T) { db := db.NewMemDB() - app := NewApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, 0) + app := NewApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, 0) for acc := range mAccPerms { require.True(t, app.bankKeeper.BlacklistedAddr(app.supplyKeeper.GetModuleAddress(acc))) diff --git a/app/params/params.go b/app/params/params.go index 64cf7471..7853bc39 100644 --- a/app/params/params.go +++ b/app/params/params.go @@ -8,9 +8,10 @@ const ( // Default simulation operation weights for messages and gov proposals const ( - DefaultWeightMsgPlaceBid int = 75 - DefaultWeightMsgCreateAtomicSwap int = 50 - DefaultWeightMsgUpdatePrices int = 50 - DefaultWeightMsgCdp int = 100 - DefaultWeightMsgClaimReward int = 50 + DefaultWeightMsgPlaceBid int = 75 + DefaultWeightMsgCreateAtomicSwap int = 50 + DefaultWeightMsgUpdatePrices int = 50 + DefaultWeightMsgCdp int = 100 + DefaultWeightMsgClaimReward int = 50 + OpWeightSubmitCommitteeChangeProposal int = 50 ) diff --git a/app/sim_test.go b/app/sim_test.go index 18d6e4ec..8a42b28a 100644 --- a/app/sim_test.go +++ b/app/sim_test.go @@ -8,6 +8,10 @@ import ( "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/simapp" "github.com/cosmos/cosmos-sdk/simapp/helpers" @@ -23,13 +27,10 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/supply" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" - dbm "github.com/tendermint/tm-db" - "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/bep3" "github.com/kava-labs/kava/x/cdp" + "github.com/kava-labs/kava/x/committee" "github.com/kava-labs/kava/x/incentive" "github.com/kava-labs/kava/x/kavadist" "github.com/kava-labs/kava/x/pricefeed" @@ -79,7 +80,7 @@ func TestFullAppSimulation(t *testing.T) { require.NoError(t, os.RemoveAll(dir)) }() - app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + app := NewApp(logger, db, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) require.Equal(t, appName, app.Name()) // run randomized simulation @@ -111,7 +112,7 @@ func TestAppImportExport(t *testing.T) { require.NoError(t, os.RemoveAll(dir)) }() - app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + app := NewApp(logger, db, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) require.Equal(t, appName, app.Name()) // Run randomized simulation @@ -145,7 +146,7 @@ func TestAppImportExport(t *testing.T) { require.NoError(t, os.RemoveAll(newDir)) }() - newApp := NewApp(log.NewNopLogger(), newDB, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + newApp := NewApp(log.NewNopLogger(), newDB, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) require.Equal(t, appName, newApp.Name()) var genesisState GenesisState @@ -178,6 +179,7 @@ func TestAppImportExport(t *testing.T) { {app.keys[kavadist.StoreKey], newApp.keys[kavadist.StoreKey], [][]byte{}}, {app.keys[pricefeed.StoreKey], newApp.keys[pricefeed.StoreKey], [][]byte{}}, {app.keys[validatorvesting.StoreKey], newApp.keys[validatorvesting.StoreKey], [][]byte{}}, + {app.keys[committee.StoreKey], newApp.keys[committee.StoreKey], [][]byte{}}, } for _, skp := range storeKeysPrefixes { @@ -205,7 +207,7 @@ func TestAppSimulationAfterImport(t *testing.T) { require.NoError(t, os.RemoveAll(dir)) }() - app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + app := NewApp(logger, db, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) require.Equal(t, appName, app.Name()) // Run randomized simulation @@ -244,7 +246,7 @@ func TestAppSimulationAfterImport(t *testing.T) { require.NoError(t, os.RemoveAll(newDir)) }() - newApp := NewApp(log.NewNopLogger(), newDB, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + newApp := NewApp(log.NewNopLogger(), newDB, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) require.Equal(t, appName, newApp.Name()) newApp.InitChain(abci.RequestInitChain{ @@ -277,7 +279,7 @@ func TestAppStateDeterminism(t *testing.T) { for j := 0; j < numTimesToRunPerSeed; j++ { logger := log.NewNopLogger() db := dbm.NewMemDB() - app := NewApp(logger, db, nil, true, simapp.FlagPeriodValue, interBlockCacheOpt()) + app := NewApp(logger, db, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, interBlockCacheOpt()) fmt.Printf( "running non-determinism simulation; seed %d: attempt: %d/%d\n", diff --git a/app/test_common.go b/app/test_common.go index 78b80486..5303d630 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -27,6 +27,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/staking" "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/cosmos/cosmos-sdk/x/upgrade" "github.com/kava-labs/kava/x/auction" "github.com/kava-labs/kava/x/bep3" @@ -58,10 +59,11 @@ func NewTestApp() TestApp { SetBip44CoinType(config) db := tmdb.NewMemDB() - app := NewApp(log.NewNopLogger(), db, nil, true, 0) + app := NewApp(log.NewNopLogger(), db, nil, true, map[int64]bool{}, 0) return TestApp{App: *app} } +// nolint func (tApp TestApp) GetAccountKeeper() auth.AccountKeeper { return tApp.accountKeeper } func (tApp TestApp) GetBankKeeper() bank.Keeper { return tApp.bankKeeper } func (tApp TestApp) GetSupplyKeeper() supply.Keeper { return tApp.supplyKeeper } @@ -71,6 +73,7 @@ func (tApp TestApp) GetMintKeeper() mint.Keeper { return tApp.mintKeep func (tApp TestApp) GetDistrKeeper() distribution.Keeper { return tApp.distrKeeper } func (tApp TestApp) GetGovKeeper() gov.Keeper { return tApp.govKeeper } func (tApp TestApp) GetCrisisKeeper() crisis.Keeper { return tApp.crisisKeeper } +func (tApp TestApp) GetUpgradeKeeper() upgrade.Keeper { return tApp.upgradeKeeper } func (tApp TestApp) GetParamsKeeper() params.Keeper { return tApp.paramsKeeper } func (tApp TestApp) GetVVKeeper() validatorvesting.Keeper { return tApp.vvKeeper } func (tApp TestApp) GetAuctionKeeper() auction.Keeper { return tApp.auctionKeeper } diff --git a/cli_test/cli_test.go b/cli_test/cli_test.go index 40e9acc2..e60723ab 100644 --- a/cli_test/cli_test.go +++ b/cli_test/cli_test.go @@ -92,6 +92,18 @@ func TestKavaCLIKeysAddRecoverHDPath(t *testing.T) { f.KeysAddRecoverHDPath("test-recoverH5", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 2, 17, "--legacy-hd-path") require.Equal(t, "kava1v9plmhvyhgxk3th9ydacm7j4z357s3nhhmy0tv", f.KeyAddress("test-recoverH5").String()) + exitSuccess, _, _ := f.KeysAddRecover("test-recover-fail", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", "--legacy-hd-path --hd-path 44'/459'/0'/0/0") + require.False(t, exitSuccess) + + // test -hd-path flag + exitSuccess, _, _ = f.KeysAddRecover("test-recoverH6", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", "--hd-path 44'/459'/0'/0/0") + require.True(t, exitSuccess) + require.Equal(t, "kava1rsjxn2e4dfl3a2qzuzzjvvgjmmate383g9q4cz", f.KeyAddress("test-recoverH6").String()) + + exitSuccess, _, _ = f.KeysAddRecover("test-recoverH7", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", "--hd-path 44'/459'/2'/0/17") + require.True(t, exitSuccess) + require.Equal(t, "kava1xvsfnksmhr887skcfrm4pe3va54tkmrtw7wyer", f.KeyAddress("test-recoverH7").String()) + // Cleanup testing directories f.Cleanup() } diff --git a/cmd/kvcli/cmd_keys.go b/cmd/kvcli/cmd_keys.go index c17d8927..add085b3 100644 --- a/cmd/kvcli/cmd_keys.go +++ b/cmd/kvcli/cmd_keys.go @@ -17,18 +17,28 @@ import ( NOTE TO FUTURE IMPLEMENTERS This monkey patches the sdk `keys` command, therefore needs to be reviewed on any sdk updates. -Where a bip44 coin type is used (cosmos-sdk 18de630d): -- adding local keys - - global variable `sdk.Config.CoinType` is used to derive the key from a mnemonic (supplied by user or generated), but only the private key is stored -- adding ledger keys - - global variable `sdk.Config.CoinType` is used to reference a key on a ledger device, bip44 path (not private key) is stored locally -- signing txs with local keys +The patch adds support for using kava's legacy bip44 coin type to the cli. +Coin types are used to create a bip44 derivation path, which is used as a mapping from mnemonic to private key. + +In cosmos-sdk v0.38.3, all private keys are stored without reference to the mnemonic or bip44 derivation path, except ledger keys. +Ledger keys are just references to a private key on a ledger device. They contain the bip44 derivation path. +To patch the cli, we only need to modify: +- when new ledger references are created +- anything to do with converting a mnemonic to a private key. + +These only happen in `kvcli keys add` cmd. +For private key generation, use a --legacy-hd-path flag to enable old coin type. +The current cosmos ledger app (v1.5.3) only supports the legacy coin type. So we only need to ensure ledger reference creation doesn't use the new coin type. + +Signing txs: +- with local keys - the stored the priv key is used to sign, mnemonics or bip44 paths not involved -- signing txs with ledger +- with ledger - the stored bip44 path is used to instruct the ledger which key to sign with */ const flagLegacyHDPath = "legacy-hd-path" +const flagHDPath = "hd-path" // this is copied from keys add cmd because it's not exported // getModifiedKeysCmd returns the standard cosmos-sdk/client/keys cmd but modified to support new and old bip44 coin types supported by kava. func getModifiedKeysCmd() *cobra.Command { @@ -70,18 +80,20 @@ func monkeyPatchCmdKeysAdd(keysAddCmd *cobra.Command) { // replace the run function with a wrapped version that sets the old coin type in the global config oldRun := keysAddCmd.RunE keysAddCmd.RunE = func(cmd *cobra.Command, args []string) error { - preExistingCoinType := sdk.GetConfig().GetCoinType() + if !viper.GetBool(flagLegacyHDPath) && viper.GetBool(flags.FlagUseLedger) { + return fmt.Errorf("cosmos ledger app only supports legacy bip44 coin type, must use --%s flag when adding ledger key", flagLegacyHDPath) + } + if viper.GetBool(flagLegacyHDPath) && viper.IsSet(flagHDPath) { + return fmt.Errorf("cannot use a custom hd path (--%s) and legacy bip44 coin type (--%s) at the same time", flagHDPath, flagLegacyHDPath) + } if viper.GetBool(flagLegacyHDPath) { + preExistingCoinType := sdk.GetConfig().GetCoinType() sdk.GetConfig().SetCoinType(sdk.CoinType) // set old coin type err := oldRun(cmd, args) - sdk.GetConfig().SetCoinType(preExistingCoinType) // revert to preexisting coin type + sdk.GetConfig().SetCoinType(preExistingCoinType) // revert to pre-existing coin type return err - } else { - if viper.GetBool(flags.FlagUseLedger) { - return fmt.Errorf("cosmos ledger app only supports legacy bip44 coin type, must use --%s flag when adding ledger key", flagLegacyHDPath) - } - return oldRun(cmd, args) } + return oldRun(cmd, args) } } diff --git a/cmd/kvd/add_account.go b/cmd/kvd/add_account.go index 4ef2307b..1b352c1e 100644 --- a/cmd/kvd/add_account.go +++ b/cmd/kvd/add_account.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "errors" "fmt" "io/ioutil" @@ -12,8 +13,8 @@ import ( "github.com/tendermint/tendermint/libs/cli" "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/cosmos/cosmos-sdk/server" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/version" @@ -21,6 +22,7 @@ import ( authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" "github.com/cosmos/cosmos-sdk/x/auth/vesting" "github.com/cosmos/cosmos-sdk/x/genutil" + validatorvesting "github.com/kava-labs/kava/x/validator-vesting" ) @@ -46,21 +48,27 @@ func AddGenesisAccountCmd( the account address or key name and a list of initial coins. If a key name is given, the address will be looked up in the local Keybase. The list of initial tokens must contain valid denominations. Accounts may optionally be supplied with vesting parameters. - If the account is a periodic or validator vesting account, vesting periods must be suppleid + If the account is a periodic or validator vesting account, vesting periods must be supplied via a JSON file using the 'vesting-periods-file' flag or 'validator-vesting-file' flag, respectively. Example: %s add-genesis-account --vesting-amount --vesting-end-time --vesting-start-time --vesting-periods `, version.ClientName), ), Args: cobra.ExactArgs(2), - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { config := ctx.Config config.SetRoot(viper.GetString(cli.HomeFlag)) addr, err := sdk.AccAddressFromBech32(args[0]) + inBuf := bufio.NewReader(cmd.InOrStdin()) if err != nil { // attempt to lookup address from Keybase if no address was provided - kb, err := keys.NewKeyBaseFromDir(viper.GetString(flagClientHome)) + kb, err := keys.NewKeyring( + sdk.KeyringServiceName(), + viper.GetString(flags.FlagKeyringBackend), + viper.GetString(flagClientHome), + inBuf, + ) if err != nil { return err } diff --git a/cmd/kvd/main.go b/cmd/kvd/main.go index d551ab96..6f049696 100644 --- a/cmd/kvd/main.go +++ b/cmd/kvd/main.go @@ -47,20 +47,22 @@ func main() { PersistentPreRunE: server.PersistentPreRunEFn(ctx), } - rootCmd.AddCommand(genutilcli.InitCmd(ctx, cdc, app.ModuleBasics, app.DefaultNodeHome)) - rootCmd.AddCommand(genutilcli.CollectGenTxsCmd(ctx, cdc, auth.GenesisAccountIterator{}, app.DefaultNodeHome)) - rootCmd.AddCommand(genutilcli.MigrateGenesisCmd(ctx, cdc)) - rootCmd.AddCommand(genutilcli.GenTxCmd( - ctx, - cdc, - app.ModuleBasics, - staking.AppModuleBasic{}, - auth.GenesisAccountIterator{}, - app.DefaultNodeHome, - app.DefaultCLIHome)) - rootCmd.AddCommand(genutilcli.ValidateGenesisCmd(ctx, cdc, app.ModuleBasics)) - rootCmd.AddCommand(AddGenesisAccountCmd(ctx, cdc, app.DefaultNodeHome, app.DefaultCLIHome)) - rootCmd.AddCommand(flags.NewCompletionCmd(rootCmd, true)) + rootCmd.AddCommand( + genutilcli.InitCmd(ctx, cdc, app.ModuleBasics, app.DefaultNodeHome), + genutilcli.CollectGenTxsCmd(ctx, cdc, auth.GenesisAccountIterator{}, app.DefaultNodeHome), + genutilcli.MigrateGenesisCmd(ctx, cdc), + genutilcli.GenTxCmd( + ctx, + cdc, + app.ModuleBasics, + staking.AppModuleBasic{}, + auth.GenesisAccountIterator{}, + app.DefaultNodeHome, + app.DefaultCLIHome), + genutilcli.ValidateGenesisCmd(ctx, cdc, app.ModuleBasics), + AddGenesisAccountCmd(ctx, cdc, app.DefaultNodeHome, app.DefaultCLIHome), + flags.NewCompletionCmd(rootCmd, true), + ) server.AddCommands(ctx, cdc, rootCmd, newApp, exportAppStateAndTMValidators) @@ -81,8 +83,13 @@ func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application cache = store.NewCommitKVStoreCacheManager() } + skipUpgradeHeights := make(map[int64]bool) + for _, h := range viper.GetIntSlice(server.FlagUnsafeSkipUpgrades) { + skipUpgradeHeights[int64(h)] = true + } + return app.NewApp( - logger, db, traceStore, true, invCheckPeriod, + logger, db, traceStore, true, skipUpgradeHeights, invCheckPeriod, baseapp.SetPruning(store.NewPruningOptionsFromString(viper.GetString("pruning"))), baseapp.SetMinGasPrices(viper.GetString(server.FlagMinGasPrices)), baseapp.SetHaltHeight(viper.GetUint64(server.FlagHaltHeight)), @@ -96,13 +103,13 @@ func exportAppStateAndTMValidators( ) (json.RawMessage, []tmtypes.GenesisValidator, error) { if height != -1 { - tempApp := app.NewApp(logger, db, traceStore, false, uint(1)) + tempApp := app.NewApp(logger, db, traceStore, false, map[int64]bool{}, uint(1)) err := tempApp.LoadHeight(height) if err != nil { return nil, nil, err } return tempApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) } - tempApp := app.NewApp(logger, db, traceStore, true, uint(1)) + tempApp := app.NewApp(logger, db, traceStore, true, map[int64]bool{}, uint(1)) return tempApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) } diff --git a/contrib/testnet-6000/genesis_examples/wip_genesis.json b/contrib/testnet-6000/genesis_examples/wip_genesis.json index c921ef73..a9a9fae7 100644 --- a/contrib/testnet-6000/genesis_examples/wip_genesis.json +++ b/contrib/testnet-6000/genesis_examples/wip_genesis.json @@ -71,23 +71,20 @@ { "auction_size": "50000000000", "conversion_factor": "8", - "debt_limit": [ - { + "debt_limit": { "amount": "2000000000000", "denom": "usdx" - } - ], + }, "denom": "bnb", "liquidation_penalty": "0.05", - "liquidation_ratio": "2.0", + "liquidation_ratio": "1.5", "market_id": "bnb:usd", "prefix": 1, "stability_fee": "1.0000000007829977" } ], "debt_auction_threshold": "10000000000", - "debt_params": [ - { + "debt_param": { "conversion_factor": "6", "debt_floor": "10000000", "debt_limit": [ @@ -99,14 +96,11 @@ "denom": "usdx", "reference_asset": "usd", "savings_rate": "0.95" - } - ], - "global_debt_limit": [ - { + }, + "global_debt_limit": { "amount": "2000000000000", "denom": "usdx" - } - ], + }, "savings_distribution_frequency": "120000000000", "surplus_auction_threshold": "1000000000" }, @@ -114,6 +108,12 @@ "previous_distribution_time": "1970-01-01T00:00:00Z", "starting_cdp_id": "1" }, + "committee": { + "next_proposal_id": "1", + "committees": [], + "proposals": [], + "votes": [] + }, "crisis": { "constant_fee": { "amount": "1333000000", @@ -171,6 +171,26 @@ "voting_period": "600000000000" } }, + "incentive": { + "params": { + "active": true, + "rewards": [ + { + "active": true, + "denom": "bnb", + "available_rewards": + { + "amount": "50000000000", + "denom": "ukava" + }, + "duration": "36288000000000000", + "time_lock": "1892160000000000000", + "claim_duration": "36288000000000000" + } + ] + }, + "previous_block_time": "1970-01-01T00:00:00Z" + }, "kavadist": { "params": { "active": true, diff --git a/rest_test/run_all_tests_from_make.sh b/rest_test/run_all_tests_from_make.sh index d1e01f50..9dbac3a9 100755 --- a/rest_test/run_all_tests_from_make.sh +++ b/rest_test/run_all_tests_from_make.sh @@ -1,11 +1,10 @@ #! /bin/bash # kill kava if it is already running -pgrep kvd | xargs kill -pgrep kvcli | xargs kill +pkill kvd && pkill kvcli # TODO import from development environment in envkey -password="password" +password="" validatorMnemonic="equip town gesture square tomorrow volume nephew minute witness beef rich gadget actress egg sing secret pole winter alarm law today check violin uncover" # address: kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c # address: kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0 @@ -24,9 +23,10 @@ rm -rf $kvcliHome mkdir /tmp/kvdHome mkdir /tmp/kvcliHome # create validator key -printf "$password\n$validatorMnemonic\n" | kvcli keys add vlad --recover --home $kvcliHome +printf "$validatorMnemonic\n" | kvcli keys add vlad --recover --home $kvcliHome --keyring-backend test # create faucet key -printf "$password\n$faucet\n" | kvcli --home $kvcliHome keys add faucet --recover --home $kvcliHome +printf "$faucet\n" | kvcli --home $kvcliHome keys add faucet --recover --home $kvcliHome --keyring-backend test + # function used to show that it is still loading showLoading() { mypid=$! @@ -51,11 +51,11 @@ kvd --home $kvdHome init --chain-id=testing vlad # doesn't need to be the same a } > /dev/null 2>&1 kvcli --home $kvcliHome config chain-id testing # or set trust-node true # add validator account to genesis -kvd --home $kvdHome add-genesis-account $(kvcli --home $kvcliHome keys show vlad -a) 10000000000000stake,1000000000000xrp,100000000000btc +kvd --home $kvdHome add-genesis-account $(kvcli keys show vlad -a --keyring-backend test --home $kvcliHome) 10000000000000stake,1000000000000xrp,100000000000btc # add faucet account to genesis -kvd --home $kvdHome add-genesis-account $(kvcli --home $kvcliHome keys show faucet -a) 10000000000000stake,1000000000000xrp,100000000000btc +kvd --home $kvdHome add-genesis-account $(kvcli keys show faucet -a --keyring-backend test --home $kvcliHome) 10000000000000stake,1000000000000xrp,100000000000btc # Create a delegation tx for the validator and add to genesis -printf "$password\n" | kvd --home $kvdHome gentx --name vlad --home-client $kvcliHome +kvd --home $kvdHome gentx --name vlad --home-client $kvcliHome --keyring-backend test { kvd --home $kvdHome collect-gentxs } > /dev/null 2>&1 @@ -65,7 +65,7 @@ jq '.app_state.mint.params.blocks_per_year = "31540000"' $genesis > $genesis.tmp jq '.app_state.pricefeed.params.markets += [{"active": true, "base_asset": "xrp", "market_id": "xrp:usd", "oracles": ["kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c"], "quote_asset": "usd"}, {"active": true, "base_asset": "btc", "market_id": "btc:usd", "oracles": ["kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c"], "quote_asset": "usd"}]' $genesis > $genesis.tmp && mv $genesis.tmp $genesis jq '.app_state.pricefeed.posted_prices += [{"expiry": "2050-01-01T00:00:00Z", "market_id": "btc:usd", "oracle_address": "kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c", "price": "8700.0"}, {"expiry": "2050-01-01T00:00:00Z", "market_id": "xrp:usd", "oracle_address": "kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c", "price": "0.25"}]' $genesis > $genesis.tmp && mv $genesis.tmp $genesis # now update cdp params -jq '.app_state.cdp.params = { "circuit_breaker": false, "collateral_params": [ { "auction_size": "10000000000", "conversion_factor": "8", "debt_limit": [ { "amount": "1000000000", "denom": "usdx" } ], "denom": "btc", "liquidation_penalty": "0.05", "liquidation_ratio": "1.5", "market_id": "btc:usd", "prefix": 0, "stability_fee": "1.0000000007829977" }, { "auction_size": "100000000", "conversion_factor": "6", "debt_limit": [ { "amount": "10000000", "denom": "usdx" } ], "denom": "xrp", "liquidation_penalty": "0.1", "liquidation_ratio": "2.0", "market_id": "xrp:usd", "prefix": 1, "stability_fee": "1.0000000007829977"} ], "debt_auction_threshold": "9000000", "debt_params": [ { "conversion_factor": "6", "debt_floor": "10000000", "debt_limit": [ { "amount": "2000000000000", "denom": "usdx" } ], "denom": "usdx", "reference_asset": "usd", "savings_rate": "0.95" } ], "global_debt_limit": [ { "amount": "2000000000000", "denom": "usdx" } ], "surplus_auction_threshold": "9000000", "savings_distribution_frequency": "120000000000" }' $genesis > $genesis.tmp && mv $genesis.tmp $genesis +jq '.app_state.cdp.params = { "circuit_breaker": false, "collateral_params": [ { "auction_size": "10000000000", "conversion_factor": "8", "debt_limit": { "amount": "1000000000000", "denom": "usdx" }, "denom": "btc", "liquidation_penalty": "0.05", "liquidation_ratio": "1.5", "market_id": "btc:usd", "prefix": 0, "stability_fee": "1.0000000007829977" }, { "auction_size": "100000000", "conversion_factor": "6", "debt_limit": { "amount": "1000000000000", "denom": "usdx" }, "denom": "xrp", "liquidation_penalty": "0.1", "liquidation_ratio": "2.0", "market_id": "xrp:usd", "prefix": 1, "stability_fee": "1.0000000007829977"} ], "debt_auction_threshold": "10000000000", "debt_param": { "conversion_factor": "6", "debt_floor": "10000000", "denom": "usdx", "reference_asset": "usd", "savings_rate": "0.95" }, "global_debt_limit": { "amount": "2000000000000", "denom": "usdx" }, "surplus_auction_threshold": "10000000000", "savings_distribution_frequency": "43200000000000" }' $genesis > $genesis.tmp && mv $genesis.tmp $genesis # start the blockchain in the background, wait until it starts making blocks { kvd start --home $kvdHome & kvdPid="$!" @@ -83,7 +83,7 @@ printf "\n" rm -f rest_test/setuptest go build rest_test/setup/setuptest.go & showLoading "Building go test file, please wait" # run the go code to send transactions to the chain and set it up correctly -./setuptest $kvcliHome & showLoading "Sending messages to blockchain" +./setuptest printf "\n" printf "Blockchain setup completed" printf "\n\n" diff --git a/rest_test/setup/setuptest.go b/rest_test/setup/setuptest.go index c6d384a3..2e7db657 100644 --- a/rest_test/setup/setuptest.go +++ b/rest_test/setup/setuptest.go @@ -1,19 +1,18 @@ package main import ( - "bufio" "bytes" "fmt" "io/ioutil" "net/http" - "os" + "strings" "time" - "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/codec" crkeys "github.com/cosmos/cosmos-sdk/crypto/keys" sdk "github.com/cosmos/cosmos-sdk/types" sdkrest "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/version" "github.com/cosmos/cosmos-sdk/x/auth" authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" authclient "github.com/cosmos/cosmos-sdk/x/auth/client/utils" @@ -31,19 +30,19 @@ import ( ) func init() { + version.Name = "kava" config := sdk.GetConfig() app.SetBech32AddressPrefixes(config) app.SetBip44CoinType(config) config.Seal() + keybase = getKeybase() } +var ( + keybase crkeys.Keybase +) + func main() { - if len(os.Args) < 2 { - fmt.Printf("Please include the kvcli home directory as a command line argument\n") - fmt.Printf("For example: ./setuptest /tmp/kvcliHome\n") - fmt.Printf("Exiting...goodbye!\n") - return - } // setup messages send to blockchain so it is in the correct state for testing sendProposal() @@ -53,14 +52,6 @@ func main() { sendUndelegation() sendCoins() - sendProposal() - sendDeposit() - sendVote() - sendDelegation() - sendUndelegation() - - sendCoins() - // create an XRP cdp and send to blockchain sendXrpCdp() @@ -234,7 +225,7 @@ func sendDeposit() { // create a deposit transaction to send to the proposal amount := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 10000000)) - deposit := gov.NewMsgDeposit(addr, 2, amount) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml + deposit := gov.NewMsgDeposit(addr, 1, amount) // Note: '1' must match 'x-example' in swagger.yaml depositToSend := []sdk.Msg{deposit} sendMsgToBlockchain(cdc, address, keyname, password, depositToSend, keybase) @@ -261,7 +252,7 @@ func sendVote() { // NOW SEND THE VOTE // create a vote on a proposal to send to the blockchain - vote := gov.NewMsgVote(addr, uint64(2), types.OptionYes) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml + vote := gov.NewMsgVote(addr, uint64(1), types.OptionYes) // Note: '1' must match 'x-example' in swagger.yaml // send a vote to the blockchain voteToSend := []sdk.Msg{vote} @@ -281,7 +272,7 @@ func sendCoins() { panic(err) } - addrTo, err := sdk.AccAddressFromBech32("kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz") // TODO IMPORTANT this is the faucet address + addrTo, err := sdk.AccAddressFromBech32("kava1ls82zzghsx0exkpr52m8vht5jqs3un0ceysshz") // Note: must match the faucet address if err != nil { panic(err) } @@ -295,7 +286,7 @@ func sendCoins() { // create coins amount := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 2000000)) - coins := bank.NewMsgSend(addrFrom, addrTo, amount) // TODO IMPORTANT '2' must match 'x-example' in swagger.yaml + coins := bank.NewMsgSend(addrFrom, addrTo, amount) // Note: '1' must match 'x-example' in swagger.yaml coinsToSend := []sdk.Msg{coins} // NOW SEND THE COINS @@ -306,14 +297,14 @@ func sendCoins() { } func getTestAddress() (address string) { - // the test address - TODO IMPORTANT make sure this lines up with startchain.sh + // the test address - Note: this must match with startchain.sh address = "kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c" return address } func getKeynameAndPassword() (keyname string, password string) { - keyname = "vlad" // TODO - IMPORTANT this must match the keys in the startchain.sh script - password = "password" // TODO - IMPORTANT this must match the keys in the startchain.sh script + keyname = "vlad" // Note: this must match the keys in the startchain.sh script + password = "" // Note: this must match the keys in the startchain.sh script return keyname, password } @@ -385,13 +376,22 @@ func sendUndelegation() { } func getKeybase() crkeys.Keybase { + + if keybase != nil { + return keybase + } + // create a keybase // IMPORTANT - TAKE THIS FROM COMMAND LINE PARAMETER and does NOT work with tilde i.e. ~/ does NOT work - keybase, err := keys.NewKeyBaseFromDir(os.Args[1]) + // myKeybase, err := keys.NewKeyBaseFromDir("/tmp/kvcliHome") + + inBuf := strings.NewReader("") + keybase, err := crkeys.NewKeyring(sdk.KeyringServiceName(), + "test", "/tmp/kvcliHome", inBuf) + if err != nil { panic(err) } - return keybase } @@ -401,16 +401,17 @@ func sendMsgToBlockchain(cdc *codec.Codec, address string, keyname string, // get the account number and sequence number accountNumber, sequenceNumber := getAccountNumberAndSequenceNumber(cdc, address) - inBuf := bufio.NewReader(os.Stdin) - txBldr := auth.NewTxBuilderFromCLI(inBuf). - WithTxEncoder(authclient.GetTxEncoder(cdc)).WithChainID("testing"). + + txBldr := auth.NewTxBuilder( + authclient.GetTxEncoder(cdc), accountNumber, sequenceNumber, 500000, 0, + true, "testing", "memo", sdk.NewCoins(), sdk.NewDecCoins(), + ).WithTxEncoder(authclient.GetTxEncoder(cdc)).WithChainID("testing"). WithKeybase(keybase).WithAccountNumber(accountNumber). WithSequence(sequenceNumber).WithGas(500000) // build and sign the transaction // this is the *Amino* encoded version of the transaction - // fmt.Printf("%+v", txBldr.Keybase()) - txBytes, err := txBldr.BuildAndSign("vlad", "password", msg) + txBytes, err := txBldr.BuildAndSign("vlad", "", msg) if err != nil { panic(err) } @@ -433,7 +434,9 @@ func sendMsgToBlockchain(cdc *codec.Codec, address string, keyname string, if err != nil { panic(err) } - // fmt.Println("post body: ", string(jsonBytes)) + fmt.Println() + fmt.Println("post body: ", string(jsonBytes)) + fmt.Println() resp, err := http.Post("http://localhost:1317/txs", "application/json", bytes.NewBuffer(jsonBytes)) if err != nil { diff --git a/swagger-ui/testnet-4000/swagger-testnet-4000.yaml b/swagger-ui/testnet-4000/swagger-testnet-4000.yaml index bf369a13..f55cad99 100644 --- a/swagger-ui/testnet-4000/swagger-testnet-4000.yaml +++ b/swagger-ui/testnet-4000/swagger-testnet-4000.yaml @@ -237,7 +237,7 @@ description: Tx hash required: true type: string - x-example: 4B2C3449FF2647BD51B54C32761FE4EBE7AE024BBE2A9898972A69BEB82D97C2 + x-example: FFC8D0E5D52D330AD315E950E5C18A9D65FC640156B9A95B5AEF07DDAE32E61D responses: 200: description: Tx with the provided hash @@ -400,13 +400,9 @@ owner: $ref: '#/definitions/Address' collateral: - type: array - items: - $ref: '#/definitions/CoinCollateral' + $ref: '#/definitions/CoinCollateral' principal: - type: array - items: - $ref: '#/definitions/CoinPrincipal' + $ref: '#/definitions/CoinPrincipal' responses: 200: description: Tx was successfully generated @@ -450,9 +446,7 @@ depositor: $ref: '#/definitions/Address' collateral: - type: array - items: - $ref: '#/definitions/CoinCollateral' + $ref: '#/definitions/CoinCollateral' responses: 200: description: Tx was successfully generated @@ -496,9 +490,7 @@ depositor: $ref: '#/definitions/Address' collateral: - type: array - items: - $ref: '#/definitions/CoinCollateral' + $ref: '#/definitions/CoinCollateral' responses: 200: description: Tx was successfully generated @@ -540,9 +532,7 @@ owner: $ref: '#/definitions/Address' principal: - type: array - items: - $ref: '#/definitions/CoinPrincipal' + $ref: '#/definitions/CoinPrincipal' responses: 200: description: Tx was successfully generated @@ -584,9 +574,7 @@ owner: $ref: '#/definitions/Address' payment: - type: array - items: - $ref: '#/definitions/CoinPrincipal' + $ref: '#/definitions/CoinPrincipal' responses: 200: description: Tx was successfully generated @@ -725,7 +713,7 @@ description: Server internal error /cdp/cdps/cdp/deposits/{owner}/{denom}: get: - summary: Get the deposits associated with the cdp owned by the input owner address and with colalteral type equal to the input collateral denom + summary: Get the deposits associated with the cdp owned by the input owner address and with collateral type equal to the input collateral denom tags: - CDP produces: @@ -1888,7 +1876,7 @@ name: proposalId required: true in: path - x-example: '2' + x-example: '1' responses: 200: description: OK @@ -1911,7 +1899,7 @@ name: proposalId required: true in: path - x-example: '2' + x-example: '1' responses: 200: description: OK @@ -1934,7 +1922,7 @@ name: proposalId required: true in: path - x-example: '2' + x-example: '1' responses: 200: description: OK @@ -1966,7 +1954,7 @@ name: proposalId required: true in: path - x-example: '2' + x-example: '1' - description: '' name: post_deposit_body in: body @@ -2007,7 +1995,7 @@ name: proposalId required: true in: path - x-example: "2" + x-example: "1" - type: string description: Bech32 depositor address name: depositor @@ -2039,7 +2027,7 @@ name: proposalId required: true in: path - x-example: '2' + x-example: '1' responses: 200: description: OK @@ -2071,7 +2059,7 @@ name: proposalId required: true in: path - x-example: '2' + x-example: '1' - description: valid value of `"option"` field can be `"yes"`, `"no"`, `"no_with_veto"` and `"abstain"` name: post_vote_body in: body @@ -2111,7 +2099,7 @@ name: proposalId required: true in: path - x-example: '2' + x-example: '1' - type: string description: Bech32 voter address name: voter @@ -2143,7 +2131,7 @@ name: proposalId required: true in: path - x-example: '2' + x-example: '1' responses: 200: description: OK @@ -3098,9 +3086,7 @@ type: string example: kava1q53rwutgpzx7szcrgzqguxyccjpzt9j4cyctn9 amount: - type: array - items: - $ref: '#/definitions/CoinCollateral' + $ref: '#/definitions/CoinCollateral' PricefeedParameters: type: object properties: diff --git a/x/bep3/alias.go b/x/bep3/alias.go index 1a315ad9..15babfab 100644 --- a/x/bep3/alias.go +++ b/x/bep3/alias.go @@ -135,7 +135,6 @@ type ( QueryAssetSupply = types.QueryAssetSupply QueryAtomicSwapByID = types.QueryAtomicSwapByID QueryAtomicSwaps = types.QueryAtomicSwaps - SupplyKeeper = types.SupplyKeeper SwapDirection = types.SwapDirection SwapStatus = types.SwapStatus ) diff --git a/x/bep3/genesis.go b/x/bep3/genesis.go index b6d01ad4..cd7836b7 100644 --- a/x/bep3/genesis.go +++ b/x/bep3/genesis.go @@ -4,10 +4,18 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/bep3/types" ) // InitGenesis initializes the store state from a genesis state. -func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper SupplyKeeper, gs GenesisState) { +func InitGenesis(ctx sdk.Context, keeper Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) { + // Check if the module account exists + moduleAcc := supplyKeeper.GetModuleAccount(ctx, ModuleName) + if moduleAcc == nil { + panic(fmt.Sprintf("%s module account has not been set", ModuleName)) + } + if err := gs.Validate(); err != nil { panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err)) } diff --git a/x/bep3/genesis_test.go b/x/bep3/genesis_test.go index e29ef730..a4499f67 100644 --- a/x/bep3/genesis_test.go +++ b/x/bep3/genesis_test.go @@ -278,19 +278,14 @@ func (suite *GenesisTestSuite) TestGenesisState() { } for _, tc := range testCases { - suite.SetupTest() if tc.expectPass { - suite.Run(tc.name, func() { - suite.NotPanics(func() { - suite.app.InitializeFromGenesisStates(tc.genState()) - }) - }) + suite.NotPanics(func() { + suite.app.InitializeFromGenesisStates(tc.genState()) + }, tc.name) } else { - suite.Run(tc.name, func() { - suite.Panics(func() { - suite.app.InitializeFromGenesisStates(tc.genState()) - }) - }) + suite.Panics(func() { + suite.app.InitializeFromGenesisStates(tc.genState()) + }, tc.name) } } } diff --git a/x/bep3/integration_test.go b/x/bep3/integration_test.go index 1abfd8ee..390b13a4 100644 --- a/x/bep3/integration_test.go +++ b/x/bep3/integration_test.go @@ -79,7 +79,7 @@ func loadSwapAndSupply(addr sdk.AccAddress, index int) (bep3.AtomicSwap, bep3.As randomNumberHash := bep3.CalculateRandomHash(randomNumber.Bytes(), timestamp) swap := bep3.NewAtomicSwap(cs(coin), randomNumberHash, expireOffset, timestamp, addr, addr, TestSenderOtherChain, - TestRecipientOtherChain, 0, bep3.Open, true, bep3.Incoming) + TestRecipientOtherChain, 1, bep3.Open, true, bep3.Incoming) supply := bep3.NewAssetSupply(coin.Denom, coin, c(coin.Denom, 0), c(coin.Denom, 0), c(coin.Denom, StandardSupplyLimit.Int64())) diff --git a/x/bep3/keeper/keeper.go b/x/bep3/keeper/keeper.go index 430f658d..4657138b 100644 --- a/x/bep3/keeper/keeper.go +++ b/x/bep3/keeper/keeper.go @@ -19,14 +19,14 @@ type Keeper struct { cdc *codec.Codec paramSubspace subspace.Subspace supplyKeeper types.SupplyKeeper + accountKeeper types.AccountKeeper } // NewKeeper creates a bep3 keeper -func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, sk types.SupplyKeeper, paramstore subspace.Subspace) Keeper { - if addr := sk.GetModuleAddress(types.ModuleName); addr == nil { - panic(fmt.Sprintf("%s module account has not been set", types.ModuleName)) - } - +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, + sk types.SupplyKeeper, ak types.AccountKeeper, + paramstore subspace.Subspace, +) Keeper { if !paramstore.HasKeyTable() { paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) } @@ -36,6 +36,7 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, sk types.SupplyKeeper, params cdc: cdc, paramSubspace: paramstore, supplyKeeper: sk, + accountKeeper: ak, } return keeper } diff --git a/x/bep3/keeper/swap.go b/x/bep3/keeper/swap.go index 5f4fc811..ac58affd 100644 --- a/x/bep3/keeper/swap.go +++ b/x/bep3/keeper/swap.go @@ -55,6 +55,13 @@ func (k Keeper) CreateAtomicSwap(ctx sdk.Context, randomNumberHash []byte, times switch direction { case types.Incoming: + // If recipient's account doesn't exist, register it in state so that the address can send + // a claim swap tx without needing to be registered in state by receiving a coin transfer. + recipientAcc := k.accountKeeper.GetAccount(ctx, recipient) + if recipientAcc == nil { + newAcc := k.accountKeeper.NewAccountWithAddress(ctx, recipient) + k.accountKeeper.SetAccount(ctx, newAcc) + } err = k.IncrementIncomingAssetSupply(ctx, amount[0]) case types.Outgoing: err = k.IncrementOutgoingAssetSupply(ctx, amount[0]) diff --git a/x/bep3/module.go b/x/bep3/module.go index de15fac9..1154bcf7 100644 --- a/x/bep3/module.go +++ b/x/bep3/module.go @@ -11,7 +11,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "github.com/cosmos/cosmos-sdk/x/auth" sim "github.com/cosmos/cosmos-sdk/x/simulation" abci "github.com/tendermint/tendermint/abci/types" @@ -19,6 +18,7 @@ import ( "github.com/kava-labs/kava/x/bep3/client/cli" "github.com/kava-labs/kava/x/bep3/client/rest" "github.com/kava-labs/kava/x/bep3/simulation" + "github.com/kava-labs/kava/x/bep3/types" ) var ( @@ -78,12 +78,12 @@ type AppModule struct { AppModuleBasic keeper Keeper - accountKeeper auth.AccountKeeper - supplyKeeper SupplyKeeper + accountKeeper types.AccountKeeper + supplyKeeper types.SupplyKeeper } // NewAppModule creates a new AppModule object -func NewAppModule(keeper Keeper, accountKeeper auth.AccountKeeper, supplyKeeper SupplyKeeper) AppModule { +func NewAppModule(keeper Keeper, accountKeeper types.AccountKeeper, supplyKeeper types.SupplyKeeper) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, keeper: keeper, diff --git a/x/bep3/simulation/operations.go b/x/bep3/simulation/operations.go index 0f3592f5..3c8bf155 100644 --- a/x/bep3/simulation/operations.go +++ b/x/bep3/simulation/operations.go @@ -9,7 +9,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/simapp/helpers" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/simulation" appparams "github.com/kava-labs/kava/app/params" @@ -28,7 +27,7 @@ const ( // WeightedOperations returns all the operations from the module with their respective weights func WeightedOperations( - appParams simulation.AppParams, cdc *codec.Codec, ak auth.AccountKeeper, k keeper.Keeper, + appParams simulation.AppParams, cdc *codec.Codec, ak types.AccountKeeper, k keeper.Keeper, ) simulation.WeightedOperations { var weightCreateAtomicSwap int @@ -47,19 +46,54 @@ func WeightedOperations( } // SimulateMsgCreateAtomicSwap generates a MsgCreateAtomicSwap with random values -func SimulateMsgCreateAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper) simulation.Operation { +func SimulateMsgCreateAtomicSwap(ak types.AccountKeeper, k keeper.Keeper) simulation.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string, ) (simulation.OperationMsg, []simulation.FutureOperation, error) { - senderAddr := k.GetBnbDeputyAddress(ctx) - - sender, found := simulation.FindAccount(accs, senderAddr) - if !found { + // Set up deputy address as it's required for all atomic swaps + deputyAddr := k.GetBnbDeputyAddress(ctx) + deputyAcc, foundDeputy := simulation.FindAccount(accs, deputyAddr) + if !foundDeputy { return noOpMsg, nil, nil } - recipient, _ := simulation.RandomAcc(r, accs) + // Get asset supplies and shuffle them + supplies := k.GetAllAssetSupplies(ctx) + r.Shuffle(len(supplies), func(i, j int) { + supplies[i], supplies[j] = supplies[j], supplies[i] + }) + + // Search for an account that holds coins received by an atomic swap + senderOut, asset, found := findValidAccountAssetSupplyPair(accs, supplies, func(acc simulation.Account, asset types.AssetSupply) bool { + if asset.CurrentSupply.Amount.IsPositive() { + authAcc := ak.GetAccount(ctx, acc.Address) + if authAcc.SpendableCoins(ctx.BlockTime()).AmountOf(asset.Denom).IsPositive() { + return true + } + } + return false + }) + + // Set sender, recipient, and denom depending on swap direction + var sender simulation.Account + var recipient simulation.Account + var denom string + // If an outgoing swap can be created, it's chosen 50% of the time. + if found && r.Intn(100) < 50 { + sender = senderOut + recipient = deputyAcc + denom = asset.Denom + } else { + sender = deputyAcc + recipient, _ = simulation.RandomAcc(r, accs) + // Randomly select an asset from supported assets + assets, foundAsset := k.GetAssets(ctx) + if !foundAsset { + return noOpMsg, nil, fmt.Errorf("no supported assets found") + } + denom = assets[r.Intn(len(assets))].Denom + } recipientOtherChain := simulation.RandStringOfLength(r, 43) senderOtherChain := simulation.RandStringOfLength(r, 43) @@ -73,36 +107,41 @@ func SimulateMsgCreateAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper) simulat timestamp := ctx.BlockTime().Unix() randomNumberHash := types.CalculateRandomHash(randomNumber.BigInt().Bytes(), timestamp) - // Randomly select an asset from supported assets - assets, found := k.GetAssets(ctx) - if !found { - return noOpMsg, nil, fmt.Errorf("no supported assets found") - } - asset := assets[r.Intn(len(assets))] - - // Check that the sender has coins of this type - senderAcc := ak.GetAccount(ctx, senderAddr) + // Check that the sender has coins for fee + senderAcc := ak.GetAccount(ctx, sender.Address) fees, err := simulation.RandomFees(r, ctx, senderAcc.SpendableCoins(ctx.BlockTime())) if err != nil { return simulation.NoOpMsg(types.ModuleName), nil, err } - availableAmount := senderAcc.SpendableCoins(ctx.BlockTime()).Sub(fees).AmountOf(asset.Denom) - // Get an amount of coins between 0.1 and 2% of total coins - amount := availableAmount.Quo(sdk.NewInt(int64(simulation.RandIntBetween(r, 50, 1000)))) - if amount.IsZero() { - return simulation.NewOperationMsgBasic(types.ModuleName, fmt.Sprintf("no-operation (all funds exhausted for asset %s)", asset.Denom), "", false, nil), nil, nil + + // Get maximum valid amount + maximumAmount := senderAcc.SpendableCoins(ctx.BlockTime()).Sub(fees).AmountOf(denom) + // The maximum amount for outgoing swaps is limited by the asset's current supply + if recipient.Equals(deputyAcc) { + assetSupply, foundAssetSupply := k.GetAssetSupply(ctx, []byte(denom)) + if !foundAssetSupply { + return noOpMsg, nil, fmt.Errorf("no asset supply found") + } + if maximumAmount.GT(assetSupply.CurrentSupply.Amount) { + maximumAmount = assetSupply.CurrentSupply.Amount + } } - coin := sdk.NewCoin(asset.Denom, amount) - coins := sdk.NewCoins(coin) - expectedIncome := coin.String() + + // Get an amount of coins between 0.1 and 2% of total coins + amount := maximumAmount.Quo(sdk.NewInt(int64(simulation.RandIntBetween(r, 50, 1000)))) + if amount.IsZero() { + return simulation.NewOperationMsgBasic(types.ModuleName, fmt.Sprintf("no-operation (all funds exhausted for asset %s)", denom), "", false, nil), nil, nil + } + coins := sdk.NewCoins(sdk.NewCoin(denom, amount)) + expectedIncome := coins.String() // We're assuming that sims are run with -NumBlocks=100 heightSpan := int64(55) crossChain := true msg := types.NewMsgCreateAtomicSwap( - senderAddr, recipient.Address, recipientOtherChain, senderOtherChain, randomNumberHash, - timestamp, coins, expectedIncome, heightSpan, crossChain, + sender.Address, recipient.Address, recipientOtherChain, senderOtherChain, + randomNumberHash, timestamp, coins, expectedIncome, heightSpan, crossChain, ) tx := helpers.GenTx( @@ -120,7 +159,7 @@ func SimulateMsgCreateAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper) simulat return simulation.NoOpMsg(types.ModuleName), nil, err } - // If created, construct a MsgClaimAtomicSwap or MsgRefundAtomicSwap future operation + // Construct a MsgClaimAtomicSwap or MsgRefundAtomicSwap future operation var futureOp simulation.FutureOperation swapID := types.CalculateSwapID(msg.RandomNumberHash, msg.From, msg.SenderOtherChain) if r.Intn(100) < 50 { @@ -143,7 +182,7 @@ func SimulateMsgCreateAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper) simulat } } -func operationClaimAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper, swapID []byte, randomNumber []byte) simulation.Operation { +func operationClaimAtomicSwap(ak types.AccountKeeper, k keeper.Keeper, swapID []byte, randomNumber []byte) simulation.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string, ) (simulation.OperationMsg, []simulation.FutureOperation, error) { @@ -176,7 +215,7 @@ func operationClaimAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper, swapID []b } } -func operationRefundAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper, swapID []byte) simulation.Operation { +func operationRefundAtomicSwap(ak types.AccountKeeper, k keeper.Keeper, swapID []byte) simulation.Operation { return func( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string, ) (simulation.OperationMsg, []simulation.FutureOperation, error) { @@ -208,3 +247,16 @@ func operationRefundAtomicSwap(ak auth.AccountKeeper, k keeper.Keeper, swapID [] return simulation.NewOperationMsg(msg, true, result.Log), nil, nil } } + +// findValidAccountAssetSupplyPair finds an account for which the callback func returns true +func findValidAccountAssetSupplyPair(accounts []simulation.Account, supplies types.AssetSupplies, + cb func(simulation.Account, types.AssetSupply) bool) (simulation.Account, types.AssetSupply, bool) { + for _, supply := range supplies { + for _, acc := range accounts { + if isValid := cb(acc, supply); isValid { + return acc, supply, true + } + } + } + return simulation.Account{}, types.AssetSupply{}, false +} diff --git a/x/bep3/types/asset.go b/x/bep3/types/asset.go index 51ef07e5..f9ab7971 100644 --- a/x/bep3/types/asset.go +++ b/x/bep3/types/asset.go @@ -4,6 +4,7 @@ import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) // AssetSupply contains information about an asset's supply @@ -26,6 +27,23 @@ func NewAssetSupply(denom string, incomingSupply, outgoingSupply, currentSupply, } } +// Validate performs a basic validation of an asset supply fields. +func (a AssetSupply) Validate() error { + if !a.IncomingSupply.IsValid() { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "incoming supply %s", a.IncomingSupply) + } + if !a.OutgoingSupply.IsValid() { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "outgoing supply %s", a.OutgoingSupply) + } + if !a.CurrentSupply.IsValid() { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "current supply %s", a.CurrentSupply) + } + if !a.Limit.IsValid() { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "limit %s", a.Limit) + } + return sdk.ValidateDenom(a.Denom) +} + // String implements stringer func (a AssetSupply) String() string { return fmt.Sprintf("Asset Supply"+ diff --git a/x/bep3/types/asset_test.go b/x/bep3/types/asset_test.go new file mode 100644 index 00000000..186d34b6 --- /dev/null +++ b/x/bep3/types/asset_test.go @@ -0,0 +1,70 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestAssetSupplyValidate(t *testing.T) { + coin := sdk.NewCoin("kava", sdk.OneInt()) + invalidCoin := sdk.Coin{Denom: "Invalid Denom", Amount: sdk.NewInt(-1)} + testCases := []struct { + msg string + asset AssetSupply + expPass bool + }{ + { + msg: "valid asset", + asset: NewAssetSupply("kava", coin, coin, coin, coin), + expPass: true, + }, + { + "invalid incoming supply", + AssetSupply{IncomingSupply: invalidCoin}, + false, + }, + { + "invalid outgoing supply", + AssetSupply{ + IncomingSupply: coin, + OutgoingSupply: invalidCoin, + }, + false, + }, + { + "invalid current supply", + AssetSupply{ + IncomingSupply: coin, + OutgoingSupply: coin, + CurrentSupply: invalidCoin, + }, + false, + }, + { + "invalid limit", + AssetSupply{ + IncomingSupply: coin, + OutgoingSupply: coin, + CurrentSupply: coin, + Limit: invalidCoin, + }, + false, + }, + { + msg: "invalid denom", + asset: NewAssetSupply("Invalid Denom", coin, coin, coin, coin), + expPass: false, + }, + } + + for _, tc := range testCases { + err := tc.asset.Validate() + if tc.expPass { + require.NoError(t, err, tc.msg) + } else { + require.Error(t, err, tc.msg) + } + } +} diff --git a/x/bep3/types/common_test.go b/x/bep3/types/common_test.go index 2dccb4fe..bf481647 100644 --- a/x/bep3/types/common_test.go +++ b/x/bep3/types/common_test.go @@ -31,7 +31,7 @@ func atomicSwap(index int) types.AtomicSwap { randomNumberHash := types.CalculateRandomHash(randomNumber.Bytes(), timestamp) swap := types.NewAtomicSwap(cs(c("bnb", 50000)), randomNumberHash, expireOffset, timestamp, kavaAddrs[0], - kavaAddrs[1], binanceAddrs[0].String(), binanceAddrs[1].String(), 0, types.Open, true, types.Incoming) + kavaAddrs[1], binanceAddrs[0].String(), binanceAddrs[1].String(), 1, types.Open, true, types.Incoming) return swap } diff --git a/x/bep3/types/expected_keepers.go b/x/bep3/types/expected_keepers.go index bb58ef9b..4035557e 100644 --- a/x/bep3/types/expected_keepers.go +++ b/x/bep3/types/expected_keepers.go @@ -2,18 +2,22 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" ) -// SupplyKeeper defines the expected supply Keeper +// SupplyKeeper defines the expected supply keeper (noalias) type SupplyKeeper interface { GetModuleAddress(name string) sdk.AccAddress GetModuleAccount(ctx sdk.Context, moduleName string) supplyexported.ModuleAccountI - SendCoinsFromModuleToModule(ctx sdk.Context, sender, recipient string, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error - - BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) error - MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error +} + +// AccountKeeper defines the expected account keeper (noalias) +type AccountKeeper interface { + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) authexported.Account + SetAccount(ctx sdk.Context, acc authexported.Account) } diff --git a/x/bep3/types/genesis.go b/x/bep3/types/genesis.go index a2159e13..ef3af6d1 100644 --- a/x/bep3/types/genesis.go +++ b/x/bep3/types/genesis.go @@ -48,19 +48,31 @@ func (gs GenesisState) Validate() error { if err := gs.Params.Validate(); err != nil { return err } + denoms := map[string]bool{} - for _, a := range gs.AssetSupplies { - if denoms[a.Denom] { - return fmt.Errorf("found duplicate asset denom %s", a.Denom) + for _, asset := range gs.AssetSupplies { + if denoms[asset.Denom] { + return fmt.Errorf("found duplicate asset denom %s", asset.Denom) } - denoms[a.Denom] = true + + if err := asset.Validate(); err != nil { + return err + } + + denoms[asset.Denom] = true } + ids := map[string]bool{} - for _, a := range gs.AtomicSwaps { - if ids[hex.EncodeToString(a.GetSwapID())] { - return fmt.Errorf("found duplicate atomic swap ID %s", hex.EncodeToString(a.GetSwapID())) + for _, swap := range gs.AtomicSwaps { + if ids[hex.EncodeToString(swap.GetSwapID())] { + return fmt.Errorf("found duplicate atomic swap ID %s", hex.EncodeToString(swap.GetSwapID())) } - ids[hex.EncodeToString(a.GetSwapID())] = true + + if err := swap.Validate(); err != nil { + return err + } + + ids[hex.EncodeToString(swap.GetSwapID())] = true } return nil } diff --git a/x/bep3/types/genesis_test.go b/x/bep3/types/genesis_test.go index 4c2be611..016291f8 100644 --- a/x/bep3/types/genesis_test.go +++ b/x/bep3/types/genesis_test.go @@ -21,12 +21,10 @@ func (suite *GenesisTestSuite) SetupTest() { config := sdk.GetConfig() app.SetBech32AddressPrefixes(config) - count := 10 - suite.swaps = atomicSwaps(count) + coin := sdk.NewCoin("kava", sdk.OneInt()) + suite.swaps = atomicSwaps(10) - incomingSupply := int64(count * 50000) - supply := types.NewAssetSupply("bnb", c("bnb", incomingSupply), - c("bnb", 0), c("bnb", 0), c("bnb", 100000000000)) + supply := types.NewAssetSupply("kava", coin, coin, coin, coin) suite.supplies = types.AssetSupplies{supply} } @@ -72,6 +70,22 @@ func (suite *GenesisTestSuite) TestValidate() { }, false, }, + { + "invalid swap", + args{ + swaps: types.AtomicSwaps{types.AtomicSwap{Amount: sdk.Coins{sdk.Coin{Denom: "Invalid Denom", Amount: sdk.NewInt(-1)}}}}, + supplies: types.AssetSupplies{}, + }, + false, + }, + { + "invalid supply", + args{ + swaps: types.AtomicSwaps{}, + supplies: types.AssetSupplies{types.AssetSupply{Denom: "Invalid Denom"}}, + }, + false, + }, { "duplicate supplies", args{ @@ -82,22 +96,19 @@ func (suite *GenesisTestSuite) TestValidate() { }} for _, tc := range testCases { - suite.SetupTest() - suite.Run(tc.name, func() { - var gs types.GenesisState - if tc.name == "default" { - gs = types.DefaultGenesisState() - } else { - gs = types.NewGenesisState(types.DefaultParams(), tc.args.swaps, tc.args.supplies) - } + var gs types.GenesisState + if tc.name == "default" { + gs = types.DefaultGenesisState() + } else { + gs = types.NewGenesisState(types.DefaultParams(), tc.args.swaps, tc.args.supplies) + } - err := gs.Validate() - if tc.expectPass { - suite.Nil(err) - } else { - suite.Error(err) - } - }) + err := gs.Validate() + if tc.expectPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } } } diff --git a/x/bep3/types/swap.go b/x/bep3/types/swap.go index 52a75f6c..8ac98170 100644 --- a/x/bep3/types/swap.go +++ b/x/bep3/types/swap.go @@ -3,11 +3,14 @@ package types import ( "encoding/hex" "encoding/json" + "errors" "fmt" + "strings" tmbytes "github.com/tendermint/tendermint/libs/bytes" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) // AtomicSwap contains the information for an atomic swap @@ -56,19 +59,50 @@ func (a AtomicSwap) GetCoins() sdk.Coins { return sdk.NewCoins(a.Amount...) } -// Validate verifies that recipient is not empty +// Validate performs a basic validation of an atomic swap fields. func (a AtomicSwap) Validate() error { - if len(a.Sender) != AddrByteCount { - return fmt.Errorf(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Sender))) - } - if len(a.Recipient) != AddrByteCount { - return fmt.Errorf(fmt.Sprintf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Recipient))) - } - if len(a.RandomNumberHash) != RandomNumberHashLength { - return fmt.Errorf(fmt.Sprintf("the length of random number hash should be %d", RandomNumberHashLength)) + if !a.Amount.IsValid() { + return fmt.Errorf("invalid amount: %s", a.Amount) } if !a.Amount.IsAllPositive() { - return fmt.Errorf(fmt.Sprintf("the swapped out coin must be positive")) + return fmt.Errorf("the swapped out coin must be positive: %s", a.Amount) + } + if len(a.RandomNumberHash) != RandomNumberHashLength { + return fmt.Errorf("the length of random number hash should be %d", RandomNumberHashLength) + } + if a.ExpireHeight == 0 { + return errors.New("expire height cannot be 0") + } + if a.Timestamp == 0 { + return errors.New("timestamp cannot be 0") + } + if a.Sender.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender cannot be empty") + } + if a.Recipient.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "recipient cannot be empty") + } + if len(a.Sender) != AddrByteCount { + return fmt.Errorf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Sender)) + } + if len(a.Recipient) != AddrByteCount { + return fmt.Errorf("the expected address length is %d, actual length is %d", AddrByteCount, len(a.Recipient)) + } + // NOTE: These adresses may not have a bech32 prefix. + if strings.TrimSpace(a.SenderOtherChain) == "" { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender other chain cannot be blank") + } + if strings.TrimSpace(a.RecipientOtherChain) == "" { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "recipient other chain cannot be blank") + } + if a.ClosedBlock == 0 { + return errors.New("closed block cannot be 0") + } + if a.Status == NULL || a.Status > 3 { + return errors.New("invalid swap status") + } + if a.Direction == INVALID || a.Direction > 2 { + return errors.New("invalid swap direction") } return nil } @@ -111,6 +145,7 @@ func (swaps AtomicSwaps) String() string { // SwapStatus is the status of an AtomicSwap type SwapStatus byte +// swap statuses const ( NULL SwapStatus = 0x00 Open SwapStatus = 0x01 diff --git a/x/bep3/types/swap_test.go b/x/bep3/types/swap_test.go index c9eae7f5..0f06ee18 100644 --- a/x/bep3/types/swap_test.go +++ b/x/bep3/types/swap_test.go @@ -44,95 +44,205 @@ func (suite *AtomicSwapTestSuite) SetupTest() { } func (suite *AtomicSwapTestSuite) TestNewAtomicSwap() { - type args struct { - amount sdk.Coins - randomNumberHash tmbytes.HexBytes - expireHeight int64 - timestamp int64 - sender sdk.AccAddress - recipient sdk.AccAddress - recipientOtherChain string - senderOtherChain string - closedBlock int64 - status types.SwapStatus - crossChain bool - direction types.SwapDirection - } testCases := []struct { - description string - args args - expectPass bool + msg string + swap types.AtomicSwap + expectPass bool }{ { - "normal", - args{ - amount: cs(c("bnb", 50000)), - randomNumberHash: suite.randomNumberHashes[0], - expireHeight: int64(360), - timestamp: suite.timestamps[0], - sender: suite.addrs[0], - recipient: suite.addrs[5], - recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", - senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", - closedBlock: 0, - status: types.Open, - crossChain: true, - direction: types.Incoming, + "valid Swap", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 360, + Timestamp: suite.timestamps[0], + Sender: suite.addrs[0], + Recipient: suite.addrs[5], + RecipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", + SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", + ClosedBlock: 1, + Status: types.Open, + CrossChain: true, + Direction: types.Incoming, }, true, }, { - "invalid random number hash length", - args{ - amount: cs(c("bnb", 50000)), - randomNumberHash: suite.randomNumberHashes[1][0:20], - expireHeight: int64(360), - timestamp: suite.timestamps[1], - sender: suite.addrs[1], - recipient: suite.addrs[5], - recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", - senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", - closedBlock: 0, - status: types.Open, - crossChain: true, - direction: types.Incoming, + "invalid amount", + types.AtomicSwap{ + Amount: sdk.Coins{sdk.Coin{Denom: "BNB", Amount: sdk.NewInt(10)}}, }, false, }, { - "invalid amount", - args{ - amount: cs(c("bnb", 0)), - randomNumberHash: suite.randomNumberHashes[2], - expireHeight: int64(360), - timestamp: suite.timestamps[2], - sender: suite.addrs[2], - recipient: suite.addrs[5], - recipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", - senderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", - closedBlock: 0, - status: types.Open, - crossChain: true, - direction: types.Incoming, + "amount not positive", + types.AtomicSwap{ + Amount: cs(c("bnb", 0)), + }, + false, + }, + { + "invalid random number hash length", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[1][0:20], + }, + false, + }, + { + "exp height 0", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 0, + }, + false, + }, + { + "timestamp 0", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 0, + }, + false, + }, + { + "empty sender", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: nil, + }, + false, + }, + { + "empty recipient", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: suite.addrs[0], + Recipient: nil, + }, + false, + }, + { + "invalid sender length", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: suite.addrs[0][:10], + Recipient: suite.addrs[5], + }, + false, + }, + { + "invalid recipient length", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: suite.addrs[0], + Recipient: suite.addrs[5][:10], + }, + false, + }, + { + "invalid sender other chain", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: suite.addrs[0], + Recipient: suite.addrs[5], + SenderOtherChain: "", + }, + false, + }, + { + "invalid recipient other chain", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: suite.addrs[0], + Recipient: suite.addrs[5], + SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", + RecipientOtherChain: "", + }, + false, + }, + { + "closed block 0", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: suite.addrs[0], + Recipient: suite.addrs[5], + SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", + RecipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", + ClosedBlock: 0, + }, + false, + }, + { + "invalid status 0", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: suite.addrs[0], + Recipient: suite.addrs[5], + SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", + RecipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", + ClosedBlock: 1, + Status: types.NULL, + }, + false, + }, + { + "invalid direction ", + types.AtomicSwap{ + Amount: cs(c("bnb", 50000)), + RandomNumberHash: suite.randomNumberHashes[0], + ExpireHeight: 10, + Timestamp: 10, + Sender: suite.addrs[0], + Recipient: suite.addrs[5], + SenderOtherChain: "bnb1uky3me9ggqypmrsvxk7ur6hqkzq7zmv4ed4ng7", + RecipientOtherChain: "bnb1urfermcg92dwq36572cx4xg84wpk3lfpksr5g7", + ClosedBlock: 1, + Status: types.Open, + Direction: types.INVALID, }, false, }, } for _, tc := range testCases { - // Create atomic swap - swap := types.NewAtomicSwap(tc.args.amount, tc.args.randomNumberHash, tc.args.expireHeight, - tc.args.timestamp, tc.args.sender, tc.args.recipient, tc.args.senderOtherChain, - tc.args.recipientOtherChain, tc.args.closedBlock, tc.args.status, tc.args.crossChain, - tc.args.direction) + err := tc.swap.Validate() if tc.expectPass { - suite.Nil(swap.Validate()) - suite.Equal(tc.args.amount, swap.GetCoins()) - expectedSwapID := types.CalculateSwapID(tc.args.randomNumberHash, tc.args.sender, tc.args.senderOtherChain) - suite.Equal(tmbytes.HexBytes(expectedSwapID), swap.GetSwapID()) + suite.Require().NoError(err, tc.msg) + suite.Require().Equal(tc.swap.Amount, tc.swap.GetCoins()) + + expectedSwapID := types.CalculateSwapID(tc.swap.RandomNumberHash, tc.swap.Sender, tc.swap.SenderOtherChain) + suite.Require().Equal(tmbytes.HexBytes(expectedSwapID), tc.swap.GetSwapID()) } else { - suite.Error(swap.Validate()) + suite.Require().Error(err) } } diff --git a/x/cdp/alias.go b/x/cdp/alias.go index a390e006..d9f1e924 100644 --- a/x/cdp/alias.go +++ b/x/cdp/alias.go @@ -22,7 +22,7 @@ const ( EventTypeCdpLiquidation = types.EventTypeCdpLiquidation EventTypeBeginBlockerFatal = types.EventTypeBeginBlockerFatal AttributeKeyCdpID = types.AttributeKeyCdpID - AttributeKeyDepositor = types.AttributeKeyDepositor + AttributeKeyDeposit = types.AttributeKeyDeposit AttributeValueCategory = types.AttributeValueCategory AttributeKeyError = types.AttributeKeyError ModuleName = types.ModuleName diff --git a/x/cdp/keeper/seize.go b/x/cdp/keeper/seize.go index b10c25ce..1e64360c 100644 --- a/x/cdp/keeper/seize.go +++ b/x/cdp/keeper/seize.go @@ -40,7 +40,7 @@ func (k Keeper) SeizeCollateral(ctx sdk.Context, cdp types.CDP) error { types.EventTypeCdpLiquidation, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)), - sdk.NewAttribute(types.AttributeKeyDepositor, fmt.Sprintf("%s", dep.Depositor)), + sdk.NewAttribute(types.AttributeKeyDeposit, dep.String()), ), ) err := k.supplyKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.LiquidatorMacc, sdk.NewCoins(dep.Amount)) diff --git a/x/cdp/spec/05_events.md b/x/cdp/spec/05_events.md index 693e91c5..863806ee 100644 --- a/x/cdp/spec/05_events.md +++ b/x/cdp/spec/05_events.md @@ -18,10 +18,12 @@ The cdp module emits the following events: ### MsgWithdraw -| Type | Attribute Key | Attribute Value | -|---------|---------------|------------------| -| message | module | cdp | -| message | sender | {sender address} | +| Type | Attribute Key | Attribute Value | +|---------|--------------- |-----------------------| +| message | cdp_withdrawal | {collateral amount} | +| message | cdp_id | {cdp_id} | +| message | module | cdp | +| message | sender | {sender address} | ### MsgDeposit @@ -43,10 +45,13 @@ The cdp module emits the following events: ### MsgRepayDebt -| Type | Attribute Key | Attribute Value | -|---------|---------------|------------------| -| message | module | cdp | -| message | sender | {sender address} | +| Type | Attribute Key | Attribute Value | +|---------------|---------------|--------------------| +| cdp_repayment | amount | {repayment amount} | +| cdp_repayment | cdp_id | {cdp id} | +| cdp_close | cdp_id | {cdp id} | +| message | module | cdp | +| message | sender | {sender address} | ## BeginBlock @@ -54,6 +59,6 @@ The cdp module emits the following events: |-------------------------|---------------|---------------------| | cdp_liquidation | module | cdp | | cdp_liquidation | cdp_id | {cdp id} | -| cdp_liquidation | depositor | {depositor address} | +| cdp_liquidation | deposit | {deposit} | | cdp_begin_blocker_error | module | cdp | | cdp_begin_blocker_error | error_message | {error} | diff --git a/x/cdp/types/events.go b/x/cdp/types/events.go index 1884084b..793fe55a 100644 --- a/x/cdp/types/events.go +++ b/x/cdp/types/events.go @@ -12,7 +12,7 @@ const ( EventTypeBeginBlockerFatal = "cdp_begin_block_error" AttributeKeyCdpID = "cdp_id" - AttributeKeyDepositor = "depositor" + AttributeKeyDeposit = "deposit" AttributeValueCategory = "cdp" AttributeKeyError = "error_message" ) diff --git a/x/committee/alias.go b/x/committee/alias.go index 5bc4c1ea..524448dc 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -12,6 +12,7 @@ const ( AttributeKeyCommitteeID = types.AttributeKeyCommitteeID AttributeKeyProposalCloseStatus = types.AttributeKeyProposalCloseStatus AttributeKeyProposalID = types.AttributeKeyProposalID + AttributeKeyVoter = types.AttributeKeyVoter AttributeValueCategory = types.AttributeValueCategory AttributeValueProposalFailed = types.AttributeValueProposalFailed AttributeValueProposalPassed = types.AttributeValueProposalPassed @@ -43,6 +44,10 @@ var ( // function aliases NewKeeper = keeper.NewKeeper NewQuerier = keeper.NewQuerier + RegisterInvariants = keeper.RegisterInvariants + ValidCommitteesInvariant = keeper.ValidCommitteesInvariant + ValidProposalsInvariant = keeper.ValidProposalsInvariant + ValidVotesInvariant = keeper.ValidVotesInvariant DefaultGenesisState = types.DefaultGenesisState GetKeyFromID = types.GetKeyFromID GetVoteKey = types.GetVoteKey diff --git a/x/committee/keeper/invariants.go b/x/committee/keeper/invariants.go new file mode 100644 index 00000000..d5202ae2 --- /dev/null +++ b/x/committee/keeper/invariants.go @@ -0,0 +1,145 @@ +package keeper + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/committee/types" +) + +// RegisterInvariants registers all committee invariants +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + + ir.RegisterRoute(types.ModuleName, "valid-committees", + ValidCommitteesInvariant(k)) + ir.RegisterRoute(types.ModuleName, "valid-proposals", + ValidProposalsInvariant(k)) + ir.RegisterRoute(types.ModuleName, "valid-votes", + ValidVotesInvariant(k)) +} + +// ValidCommitteesInvariant verifies that all committees in the store are independently valid +func ValidCommitteesInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + + var validationErr error + var invalidCommittee types.Committee + k.IterateCommittees(ctx, func(com types.Committee) bool { + + if err := com.Validate(); err != nil { + validationErr = err + invalidCommittee = com + return true + } + return false + }) + + broken := validationErr != nil + invariantMessage := sdk.FormatInvariant( + types.ModuleName, + "valid committees", + fmt.Sprintf( + "\tfound invalid committee, reason: %s\n"+ + "\tcommittee:\n\t%+v\n", + validationErr, invalidCommittee), + ) + return invariantMessage, broken + } +} + +// ValidProposalsInvariant verifies that all proposals in the store are valid +func ValidProposalsInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + + var validationErr error + var invalidProposal types.Proposal + k.IterateProposals(ctx, func(proposal types.Proposal) bool { + invalidProposal = proposal + + if err := proposal.PubProposal.ValidateBasic(); err != nil { + validationErr = err + return true + } + + currentTime := ctx.BlockTime() + if !currentTime.Equal(time.Time{}) { // this avoids a simulator bug where app.InitGenesis is called with blockTime=0 instead of the correct time + if proposal.Deadline.Before(currentTime) { + validationErr = fmt.Errorf("deadline after current block time %s", currentTime) + return true + } + } + + com, found := k.GetCommittee(ctx, proposal.CommitteeID) + if !found { + validationErr = fmt.Errorf("proposal has no committee %d", proposal.CommitteeID) + return true + } + + if !com.HasPermissionsFor(proposal.PubProposal) { + validationErr = fmt.Errorf("proposal not permitted for committee %+v", com) + return true + } + + return false + }) + + broken := validationErr != nil + invariantMessage := sdk.FormatInvariant( + types.ModuleName, + "valid proposals", + fmt.Sprintf( + "\tfound invalid proposal, reason: %s\n"+ + "\tproposal:\n\t%s\n", + validationErr, invalidProposal), + ) + return invariantMessage, broken + } +} + +// ValidVotesInvariant verifies that all votes in the store are valid +func ValidVotesInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + + var validationErr error + var invalidVote types.Vote + k.IterateVotes(ctx, func(vote types.Vote) bool { + invalidVote = vote + + if vote.Voter.Empty() { + validationErr = fmt.Errorf("empty voter address") + return true + } + + proposal, found := k.GetProposal(ctx, vote.ProposalID) + if !found { + validationErr = fmt.Errorf("vote has no proposal %d", vote.ProposalID) + return true + } + + com, found := k.GetCommittee(ctx, proposal.CommitteeID) + if !found { + validationErr = fmt.Errorf("vote's proposal has no committee %d", proposal.CommitteeID) + return true + } + if !com.HasMember(vote.Voter) { + validationErr = fmt.Errorf("voter is not a member of committee %+v", com) + return true + } + + return false + }) + + broken := validationErr != nil + invariantMessage := sdk.FormatInvariant( + types.ModuleName, + "valid votes", + fmt.Sprintf( + "\tfound invalid vote, reason: %s\n"+ + "\tvote:\n\t%+v\n", + validationErr, invalidVote), + ) + return invariantMessage, broken + } +} diff --git a/x/committee/keeper/proposal.go b/x/committee/keeper/proposal.go index 47fbd8b4..46ac5b9e 100644 --- a/x/committee/keeper/proposal.go +++ b/x/committee/keeper/proposal.go @@ -74,6 +74,7 @@ func (k Keeper) AddVote(ctx sdk.Context, proposalID uint64, voter sdk.AccAddress types.EventTypeProposalVote, sdk.NewAttribute(types.AttributeKeyCommitteeID, fmt.Sprintf("%d", com.ID)), sdk.NewAttribute(types.AttributeKeyProposalID, fmt.Sprintf("%d", pr.ID)), + sdk.NewAttribute(types.AttributeKeyVoter, voter.String()), ), ) return nil diff --git a/x/committee/module.go b/x/committee/module.go index 857d8a1e..cdb55138 100644 --- a/x/committee/module.go +++ b/x/committee/module.go @@ -95,7 +95,9 @@ func (AppModule) Name() string { } // RegisterInvariants register module invariants -func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + RegisterInvariants(ir, am.keeper) +} // Route module message route name func (AppModule) Route() string { @@ -149,12 +151,12 @@ func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) { simulation.RandomizedGenState(simState) } -// TODO -func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent { - return nil +// ProposalContents returns functions that generate gov proposals for the module +func (am AppModule) ProposalContents(simState module.SimulationState) []sim.WeightedProposalContent { + return simulation.ProposalContents(am.keeper, simState.ParamChanges) } -// RandomizedParams returns functions that generate params for the module. +// RandomizedParams returns functions that generate params for the module func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange { return nil } @@ -164,7 +166,7 @@ func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { sdr[StoreKey] = simulation.DecodeStore } -// WeightedOperations returns the all the auction module operations with their respective weights. +// WeightedOperations returns the module operations for use in simulations func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation { - return nil // TODO simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper) + return simulation.WeightedOperations(simState.AppParams, simState.Cdc, am.accountKeeper, am.keeper, simState.Contents) } diff --git a/x/committee/simulation/common.go b/x/committee/simulation/common.go new file mode 100644 index 00000000..80383b8f --- /dev/null +++ b/x/committee/simulation/common.go @@ -0,0 +1,71 @@ +package simulation + +import ( + "fmt" + "math/big" + "math/rand" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +func RandomAddresses(r *rand.Rand, accs []simulation.Account) []sdk.AccAddress { + r.Shuffle(len(accs), func(i, j int) { + accs[i], accs[j] = accs[j], accs[i] + }) + + var addresses []sdk.AccAddress + numAddresses := r.Intn(len(accs) + 1) + for i := 0; i < numAddresses; i++ { + addresses = append(addresses, accs[i].Address) + } + return addresses +} + +func RandomPositiveDuration(r *rand.Rand, inclusiveMin, exclusiveMax time.Duration) (time.Duration, error) { + min := int64(inclusiveMin) + max := int64(exclusiveMax) + if min < 0 || max < 0 { + return 0, fmt.Errorf("min and max must be positive") + } + if min >= max { + return 0, fmt.Errorf("max must be < min") + } + randPositiveInt64 := r.Int63n(max-min) + min + return time.Duration(randPositiveInt64), nil +} + +func RandomTime(r *rand.Rand, inclusiveMin, exclusiveMax time.Time) (time.Time, error) { + if exclusiveMax.Before(inclusiveMin) { + return time.Time{}, fmt.Errorf("max must be > min") + } + period := exclusiveMax.Sub(inclusiveMin) + subPeriod, err := RandomPositiveDuration(r, 0, period) + if err != nil { + return time.Time{}, err + } + return inclusiveMin.Add(subPeriod), nil +} + +// RandInt randomly generates an sdk.Int in the range [inclusiveMin, inclusiveMax]. It works for negative and positive integers. +func RandIntInclusive(r *rand.Rand, inclusiveMin, inclusiveMax sdk.Int) (sdk.Int, error) { + if inclusiveMin.GT(inclusiveMax) { + return sdk.Int{}, fmt.Errorf("min larger than max") + } + return RandInt(r, inclusiveMin, inclusiveMax.Add(sdk.OneInt())) +} + +// RandInt randomly generates an sdk.Int in the range [inclusiveMin, exclusiveMax). It works for negative and positive integers. +func RandInt(r *rand.Rand, inclusiveMin, exclusiveMax sdk.Int) (sdk.Int, error) { + // validate input + if inclusiveMin.GTE(exclusiveMax) { + return sdk.Int{}, fmt.Errorf("min larger or equal to max") + } + // shift the range to start at 0 + shiftedRange := exclusiveMax.Sub(inclusiveMin) // should always be positive given the check above + // randomly pick from the shifted range + shiftedRandInt := sdk.NewIntFromBigInt(new(big.Int).Rand(r, shiftedRange.BigInt())) + // shift back to the original range + return shiftedRandInt.Add(inclusiveMin), nil +} diff --git a/x/committee/simulation/decoder.go b/x/committee/simulation/decoder.go index 954c1f3a..8b2946a8 100644 --- a/x/committee/simulation/decoder.go +++ b/x/committee/simulation/decoder.go @@ -1,13 +1,43 @@ package simulation import ( + "bytes" + "fmt" + "github.com/cosmos/cosmos-sdk/codec" "github.com/tendermint/tendermint/libs/kv" + + "github.com/kava-labs/kava/x/committee/types" ) // DecodeStore unmarshals the KVPair's Value to the corresponding module type func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string { - // TODO implement this - return "" + switch { + case bytes.Equal(kvA.Key[:1], types.CommitteeKeyPrefix): + var committeeA, committeeB types.Committee + cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &committeeA) + cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &committeeB) + return fmt.Sprintf("%v\n%v", committeeA, committeeB) + + case bytes.Equal(kvA.Key[:1], types.ProposalKeyPrefix): + var proposalA, proposalB types.Proposal + cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &proposalA) + cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &proposalB) + return fmt.Sprintf("%v\n%v", proposalA, proposalB) + + case bytes.Equal(kvA.Key[:1], types.VoteKeyPrefix): + var voteA, voteB types.Vote + cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &voteA) + cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &voteB) + return fmt.Sprintf("%v\n%v", voteA, voteB) + + case bytes.Equal(kvA.Key[:1], types.NextProposalIDKey): + proposalIDA := types.Uint64FromBytes(kvA.Value) + proposalIDB := types.Uint64FromBytes(kvB.Value) + return fmt.Sprintf("%d\n%d", proposalIDA, proposalIDB) + + default: + panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1])) + } } diff --git a/x/committee/simulation/decoder_test.go b/x/committee/simulation/decoder_test.go new file mode 100644 index 00000000..3e361ac7 --- /dev/null +++ b/x/committee/simulation/decoder_test.go @@ -0,0 +1,77 @@ +package simulation + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/kv" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/kava-labs/kava/x/committee/types" +) + +func makeTestCodec() (cdc *codec.Codec) { + cdc = codec.New() + sdk.RegisterCodec(cdc) + govtypes.RegisterCodec(cdc) + types.RegisterCodec(cdc) + return cdc +} + +func TestDecodeStore(t *testing.T) { + cdc := makeTestCodec() + + committee := types.NewCommittee( + 12, + "This committee is for testing.", + nil, + []types.Permission{types.TextPermission{}}, + sdk.MustNewDecFromStr("0.667"), + time.Hour*24*7, + ) + proposal := types.Proposal{ + ID: 34, + CommitteeID: 12, + Deadline: time.Date(1998, time.January, 1, 1, 0, 0, 0, time.UTC), + PubProposal: govtypes.NewTextProposal("A Title", "A description of this proposal."), + } + vote := types.Vote{ + ProposalID: 9, + Voter: nil, + } + + kvPairs := kv.Pairs{ + kv.Pair{Key: types.CommitteeKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&committee)}, + kv.Pair{Key: types.ProposalKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&proposal)}, + kv.Pair{Key: types.VoteKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&vote)}, + kv.Pair{Key: types.NextProposalIDKey, Value: sdk.Uint64ToBigEndian(10)}, + kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}}, + } + + tests := []struct { + name string + expectedLog string + }{ + {"Committee", fmt.Sprintf("%v\n%v", committee, committee)}, + {"Proposal", fmt.Sprintf("%v\n%v", proposal, proposal)}, + {"Vote", fmt.Sprintf("%v\n%v", vote, vote)}, + {"NextProposalID", "10\n10"}, + {"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() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name) + default: + require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name) + } + }) + } +} diff --git a/x/committee/simulation/genesis.go b/x/committee/simulation/genesis.go index 22a25439..8945a4f5 100644 --- a/x/committee/simulation/genesis.go +++ b/x/committee/simulation/genesis.go @@ -2,21 +2,122 @@ package simulation import ( "fmt" + "math/rand" + "time" - "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/simulation" "github.com/kava-labs/kava/x/committee/types" ) +const ( + // Block time params are un-exported constants in cosmos-sdk/x/simulation. + // Copy them here in lieu of importing them. + minTimePerBlock time.Duration = (10000 / 2) * time.Second + maxTimePerBlock time.Duration = 10000 * time.Second + // Calculate the average block time + AverageBlockTime time.Duration = (maxTimePerBlock - minTimePerBlock) / 2 + + FallbackCommitteeID uint64 = 0 +) + // RandomizedGenState generates a random GenesisState for the module func RandomizedGenState(simState *module.SimulationState) { + r := simState.Rand - // TODO implement this fully - // - randomly generating the genesis params - // - overwriting with genesis provided to simulation - genesisState := types.DefaultGenesisState() + // Create an always present committee with god permissions to ensure any randomly generated proposal can always be submitted. + // Without this, proposals can often not be submitted as there aren't any committees with the right set of permissions available. + // It provides more control over how often different proposal types happen during simulation. + // It also makes the code simpler--proposals can just be randomly generated and submitted without having to comply to permissions that happen to be available at the time. + fallbackCommittee := types.NewCommittee( + FallbackCommitteeID, + "A committee with god permissions that will always be in state and not deleted. It ensures any generated proposal can always be submitted and passed.", + RandomAddresses(r, simState.Accounts), + []types.Permission{types.GodPermission{}}, + sdk.MustNewDecFromStr("0.5"), + AverageBlockTime*10, + ) - fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesisState)) + // Create other committees + numCommittees := r.Intn(100) + committees := []types.Committee{fallbackCommittee} + for i := 0; i < numCommittees; i++ { + com, err := RandomCommittee(r, firstNAccounts(25, simState.Accounts), paramChangeToAllowedParams(simState.ParamChanges)) + if err != nil { + panic(err) + } + committees = append(committees, com) + } + + // Add genesis state to simState + genesisState := types.NewGenesisState( + types.DefaultNextProposalID, + committees, + []types.Proposal{}, + []types.Vote{}, + ) + fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, []byte{}) simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesisState) } + +func RandomCommittee(r *rand.Rand, availableAccs []simulation.Account, allowedParams []types.AllowedParam) (types.Committee, error) { + // pick committee members + if len(availableAccs) < 1 { + return types.Committee{}, fmt.Errorf("must be ≥ 1 addresses") + } + var members []sdk.AccAddress + for len(members) < 1 { + members = RandomAddresses(r, availableAccs) + } + + // pick proposal duration + dur, err := RandomPositiveDuration(r, 0, AverageBlockTime*10) + if err != nil { + return types.Committee{}, err + } + + // pick committee vote threshold, must be in interval (0,1] + threshold := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec()) + + return types.NewCommittee( + r.Uint64(), // could collide with other committees, but unlikely + simulation.RandStringOfLength(r, r.Intn(types.MaxCommitteeDescriptionLength+1)), + members, + RandomPermissions(r, allowedParams), + threshold, + dur, + ), nil +} + +func RandomPermissions(r *rand.Rand, allowedParams []types.AllowedParam) []types.Permission { + var permissions []types.Permission + if r.Intn(100) < 50 { + permissions = append(permissions, types.TextPermission{}) + } + if r.Intn(100) < 50 { + r.Shuffle(len(allowedParams), func(i, j int) { + allowedParams[i], allowedParams[j] = allowedParams[j], allowedParams[i] + }) + permissions = append(permissions, + types.ParamChangePermission{ + AllowedParams: allowedParams[:r.Intn(len(allowedParams)+1)], + }) + } + return permissions +} + +func paramChangeToAllowedParams(paramChanges []simulation.ParamChange) []types.AllowedParam { + var allowedParams []types.AllowedParam + for _, pc := range paramChanges { + allowedParams = append( + allowedParams, + types.AllowedParam{ + Subspace: pc.Subspace, + Key: pc.Key, + }, + ) + } + return allowedParams +} diff --git a/x/committee/simulation/operations.go b/x/committee/simulation/operations.go new file mode 100644 index 00000000..71617aab --- /dev/null +++ b/x/committee/simulation/operations.go @@ -0,0 +1,199 @@ +package simulation + +import ( + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/simapp/helpers" + sdk "github.com/cosmos/cosmos-sdk/types" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/kava-labs/kava/x/committee/keeper" + "github.com/kava-labs/kava/x/committee/types" +) + +var ( + proposalPassPercentage = 0.9 +) + +type AccountKeeper interface { + GetAccount(sdk.Context, sdk.AccAddress) authexported.Account +} + +// WeightedOperations creates an operation (with weight) for each type of proposal generator. +// Custom proposal generators can be added for more control over types of proposal submitted, eg to increase likelyhood of particular cdp param changes. +func WeightedOperations(appParams simulation.AppParams, cdc *codec.Codec, ak AccountKeeper, + k keeper.Keeper, wContents []simulation.WeightedProposalContent) simulation.WeightedOperations { + + var wops simulation.WeightedOperations + + for _, wContent := range wContents { + wContent := wContent // pin variable + if wContent.AppParamsKey == OpWeightSubmitCommitteeChangeProposal { + // don't include committee change/delete proposals as they're not enabled for submission to committees + continue + } + var weight int + // TODO this doesn't allow weights to be different from what they are in the gov module + appParams.GetOrGenerate(cdc, wContent.AppParamsKey, &weight, nil, + func(_ *rand.Rand) { weight = wContent.DefaultWeight }) + + wops = append( + wops, + simulation.NewWeightedOperation( + weight, + SimulateMsgSubmitProposal(ak, k, wContent.ContentSimulatorFn), + ), + ) + } + return wops +} + +// SimulateMsgSubmitProposal creates a proposal using the passed contentSimulatorFn and tries to find a committee that has permissions for it. If it can't then it uses the fallback committee. +// If the fallback committee isn't there (eg when using an non-generated genesis) and no committee can be found this emits a no-op msg and doesn't do anything. +// For each submit proposal msg, future ops for the vote messages are generated. Sometimes it doesn't run enough votes to allow the proposal to timeout - the likelihood of this happening is controlled by a parameter. +func SimulateMsgSubmitProposal(ak AccountKeeper, k keeper.Keeper, contentSim simulation.ContentSimulatorFn) simulation.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string) (simulation.OperationMsg, []simulation.FutureOperation, error) { + + // 1) Send a submit proposal msg + + committees := k.GetCommittees(ctx) + // shuffle committees to ensure proposals are distributed across them evenly + r.Shuffle(len(committees), func(i, j int) { + committees[i], committees[j] = committees[j], committees[i] + }) + // move fallback committee to the end of slice + for i, c := range committees { + if c.ID == FallbackCommitteeID { + // switch places with last element + committees[i], committees[len(committees)-1] = committees[len(committees)-1], committees[i] + } + } + // pick a committee that has permissions for proposal + pp := types.PubProposal(contentSim(r, ctx, accs)) + var selectedCommittee types.Committee + var found bool + for _, c := range committees { + if c.HasPermissionsFor(pp) { + selectedCommittee = c + found = true + break + } + } + if !found { + // fallback committee was not present, this should only happen if not using the generated genesis state + return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation (no committee has permissions for proposal)", "", false, nil), nil, nil + } + + // create the msg and tx + proposer := selectedCommittee.Members[r.Intn(len(selectedCommittee.Members))] // won't panic as committees must have ≥ 1 members + msg := types.NewMsgSubmitProposal( + pp, + proposer, + selectedCommittee.ID, + ) + account := ak.GetAccount(ctx, proposer) + fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime())) + if err != nil { + return simulation.NoOpMsg(types.ModuleName), nil, err + } + proposerAcc, found := simulation.FindAccount(accs, proposer) + if !found { + return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("address not in account list") + } + tx := helpers.GenTx( + []sdk.Msg{msg}, + fees, + helpers.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + proposerAcc.PrivKey, + ) + // submit tx + _, result, err := app.Deliver(tx) + if err != nil { + // to aid debugging, add the stack trace to the comment field of the returned opMsg + return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err + } + // to aid debugging, add the result log to the comment field + submitOpMsg := simulation.NewOperationMsg(msg, true, result.Log) + + // 2) Schedule vote operations + + // get submitted proposal + proposalID := types.Uint64FromBytes(result.Data) + proposal, found := k.GetProposal(ctx, proposalID) + if !found { + return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("can't find proposal with ID '%d'", proposalID) + } + + // pick the voters + // num voters determined by whether the proposal should pass or not + numMembers := int64(len(selectedCommittee.Members)) + majority := selectedCommittee.VoteThreshold.Mul(sdk.NewInt(numMembers).ToDec()).Ceil().TruncateInt64() + + numVoters := r.Int63n(majority) // in interval [0, majority) + shouldPass := r.Float64() < proposalPassPercentage + if shouldPass { + numVoters = majority + r.Int63n(numMembers-majority+1) // in interval [majority, numMembers] + } + voters := selectedCommittee.Members[:numVoters] + + // schedule vote operations + var futureOps []simulation.FutureOperation + for _, v := range voters { + voteTime, err := RandomTime(r, ctx.BlockTime(), proposal.Deadline) + if err != nil { + return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("random time generation failed: %w", err) + } + fop := simulation.FutureOperation{ + BlockTime: voteTime, + Op: SimulateMsgVote(k, ak, v, proposal.ID), + } + futureOps = append(futureOps, fop) + } + + return submitOpMsg, futureOps, nil + } +} + +func SimulateMsgVote(k keeper.Keeper, ak AccountKeeper, voter sdk.AccAddress, proposalID uint64) simulation.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string) ( + opMsg simulation.OperationMsg, fOps []simulation.FutureOperation, err error) { + + msg := types.NewMsgVote(voter, proposalID) + + account := ak.GetAccount(ctx, voter) + fees, err := simulation.RandomFees(r, ctx, account.SpendableCoins(ctx.BlockTime())) + if err != nil { + return simulation.NoOpMsg(types.ModuleName), nil, err + } + + voterAcc, found := simulation.FindAccount(accs, voter) + if !found { + return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("address not in account list") + } + + tx := helpers.GenTx( + []sdk.Msg{msg}, + fees, + helpers.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + voterAcc.PrivKey, + ) + + _, result, err := app.Deliver(tx) + if err != nil { + // to aid debugging, add the stack trace to the comment field of the returned opMsg + return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err + } + // to aid debugging, add the result log to the comment field + return simulation.NewOperationMsg(msg, true, result.Log), nil, nil + } +} diff --git a/x/committee/simulation/params.go b/x/committee/simulation/params.go deleted file mode 100644 index 8c1f7aff..00000000 --- a/x/committee/simulation/params.go +++ /dev/null @@ -1,14 +0,0 @@ -package simulation - -import ( - "math/rand" - - "github.com/cosmos/cosmos-sdk/x/simulation" -) - -// ParamChanges defines the parameters that can be modified by param change proposals -// on the simulation -func ParamChanges(r *rand.Rand) []simulation.ParamChange { - // TODO implement this - return []simulation.ParamChange{} -} diff --git a/x/committee/simulation/proposals.go b/x/committee/simulation/proposals.go new file mode 100644 index 00000000..f59504b7 --- /dev/null +++ b/x/committee/simulation/proposals.go @@ -0,0 +1,173 @@ +package simulation + +import ( + "fmt" + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/simulation" + + appparams "github.com/kava-labs/kava/app/params" + "github.com/kava-labs/kava/x/committee/keeper" + "github.com/kava-labs/kava/x/committee/types" +) + +const OpWeightSubmitCommitteeChangeProposal = "op_weight_submit_committee_change_proposal" + +// ProposalContents defines the module weighted proposals' contents +func ProposalContents(k keeper.Keeper, paramChanges []simulation.ParamChange) []simulation.WeightedProposalContent { + return []simulation.WeightedProposalContent{ + { + AppParamsKey: OpWeightSubmitCommitteeChangeProposal, + DefaultWeight: appparams.OpWeightSubmitCommitteeChangeProposal, + ContentSimulatorFn: SimulateCommitteeChangeProposalContent(k, paramChanges), + }, + } +} + +// SimulateCommitteeChangeProposalContent generates gov proposal contents that either: +// - create new committees +// - change existing committees +// - delete committees +// It does not alter the fallback committee. +func SimulateCommitteeChangeProposalContent(k keeper.Keeper, paramChanges []simulation.ParamChange) simulation.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simulation.Account) govtypes.Content { + allowedParams := paramChangeToAllowedParams(paramChanges) + + // get current committees, ignoring the fallback committee + var committees []types.Committee + k.IterateCommittees(ctx, func(com types.Committee) bool { + if com.ID != FallbackCommitteeID { + committees = append(committees, com) + } + return false + }) + if len(committees) < 1 { // create a committee if none exist + com, err := RandomCommittee(r, firstNAccounts(25, accs), allowedParams) // limit num members to avoid overflowing hardcoded gov ops gas limit + if err != nil { + panic(err) + } + return types.NewCommitteeChangeProposal( + simulation.RandStringOfLength(r, 10), + simulation.RandStringOfLength(r, 100), + com, + ) + } + + // create a proposal content + + var content govtypes.Content + switch choice := r.Intn(100); { + + // create committee + case choice < 20: + com, err := RandomCommittee(r, firstNAccounts(25, accs), allowedParams) // limit num members to avoid overflowing hardcoded gov ops gas limit + if err != nil { + panic(err) + } + content = types.NewCommitteeChangeProposal( + simulation.RandStringOfLength(r, 10), + simulation.RandStringOfLength(r, 100), + com, + ) + + // update committee + case choice < 80: + com := committees[r.Intn(len(committees))] + + // update members + if r.Intn(100) < 50 { + if len(accs) == 0 { + panic("must have at least one account availabel to use as committee member") + } + var members []sdk.AccAddress + for len(members) < 1 { + members = RandomAddresses(r, firstNAccounts(25, accs)) // limit num members to avoid overflowing hardcoded gov ops gas limit + } + com.Members = members + } + // update permissions + if r.Intn(100) < 50 { + com.Permissions = RandomPermissions(r, allowedParams) + } + // update proposal duration + if r.Intn(100) < 50 { + dur, err := RandomPositiveDuration(r, 0, AverageBlockTime*100) + if err != nil { + panic(err) + } + com.ProposalDuration = dur + } + // update vote threshold + if r.Intn(100) < 50 { + // VoteThreshold must be in interval (0,1] + com.VoteThreshold = simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("1").Sub(sdk.SmallestDec())).Add(sdk.SmallestDec()) + } + + content = types.NewCommitteeChangeProposal( + simulation.RandStringOfLength(r, 10), + simulation.RandStringOfLength(r, 100), + com, + ) + + // delete committee + default: + com := committees[r.Intn(len(committees))] + content = types.NewCommitteeDeleteProposal( + simulation.RandStringOfLength(r, 10), + simulation.RandStringOfLength(r, 100), + com.ID, + ) + } + + return content + } +} + +/* +// Example custom ParamChangeProposal generator to only generate change to interesting cdp params. +// This allows more control over what params are changed within a simulation. +func SimulateCDPParamChangeProposalContent(cdpKeeper cdpkeeper.Keeper, paramChangePool []simulation.ParamChange) simulation.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, _ []simulation.Account) govtypes.Content { + + var paramChanges []paramstypes.ParamChange + + // alter sub params + cp := cdpKeeper.GetParams(ctx).CollateralParams + if len(cp) == 0 { + return nil + } + cp[0].StabilityFee = sdk.MustNewDecFromStr("0.000001") // TODO generate + paramChanges = append( + paramChanges, + paramstypes.NewParamChange(cdptypes.ModuleName, "?", string(cdptypes.ModuleCdc.MustMarshalJSON(cp))), + ) + + // alter normal param + for _, pc := range paramChangePool { + if pc.Subspace == cdptypes.ModuleName && pc.Key == string(cdptypes.KeyGlobalDebtLimit) { + paramChanges = append( + paramChanges, + paramstypes.NewParamChange(pc.Subspace, pc.Key, pc.SimValue(r)), + ) + } + } + + return paramstypes.NewParameterChangeProposal( + simulation.RandStringOfLength(r, 140), // title + simulation.RandStringOfLength(r, 5000), // description + paramChanges, // set of changes + ) + } +} +*/ +func firstNAccounts(n int, accs []simulation.Account) []simulation.Account { + if n < 0 { + panic(fmt.Sprintf("n must be ≥ 0")) + } + if n > len(accs) { + return accs + } + return accs[:n] +} diff --git a/x/committee/spec/01_concepts.md b/x/committee/spec/01_concepts.md new file mode 100644 index 00000000..d19d6ae5 --- /dev/null +++ b/x/committee/spec/01_concepts.md @@ -0,0 +1,5 @@ +# Concepts + +For a general introduction to governance using the Comsos-SDK, see [x/gov](https://github.com/cosmos/cosmos-sdk/blob/v0.38.3/x/gov/spec/01_concepts.md). + +This module provides companion governance functionality to `x/gov` by allowing the creation of committees, or groups of addresses that can vote on proposals for which they have permission and which bypass the usual on-chain governance structures. Permissions scope the types of proposals that committees can submit and vote on. This allows for committees with unlimited breadth (ie, a committee can have permission to perform any governance action), or narrowly scoped abilities (ie, a committee can only change a single parameter of a single module within a specified range). Further, vote tallying is "first-past-the-post", so proposals can be enacted more rapidly and with greater flexibility than permitted by `x/gov`. diff --git a/x/committee/spec/02_state.md b/x/committee/spec/02_state.md new file mode 100644 index 00000000..362fb2bc --- /dev/null +++ b/x/committee/spec/02_state.md @@ -0,0 +1,19 @@ +# State + +## Genesis state + +`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the committee module to resume. + +```go +// GenesisState is state that must be provided at chain genesis. + type GenesisState struct { + NextProposalID uint64 `json:"next_proposal_id" yaml:"next_proposal_id"` + Committees []Committee `json:"committees" yaml:"committees"` + Proposals []Proposal `json:"proposals" yaml:"proposals"` + Votes []Vote `json:"votes" yaml:"votes"` + } +``` + +## Store + +For complete implementation details for how items are stored, see [keys.go](../types/keys.go). The committee module store state consists of committees, proposals, and votes. When a proposal expires or passes, the proposal and associated votes are deleted from state. diff --git a/x/committee/spec/03_messages.md b/x/committee/spec/03_messages.md new file mode 100644 index 00000000..2db9cf5a --- /dev/null +++ b/x/committee/spec/03_messages.md @@ -0,0 +1,34 @@ +# Messages + +Committee members submit proposals using a `MsgSubmitProposal` + +```go +// MsgSubmitProposal is used by committee members to create a new proposal that they can vote on. +type MsgSubmitProposal struct { + PubProposal PubProposal `json:"pub_proposal" yaml:"pub_proposal"` + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` + CommitteeID uint64 `json:"committee_id" yaml:"committee_id"` +} +``` + +## State Modifications + +* Generate new `ProposalID` +* Create new `Proposal` with deadline equal to the time that the proposal will expire. + +Committee members vote 'yes' on a proposal using a `MsgVote` + +```go +// MsgVote is submitted by committee members to vote on proposals. +type MsgVote struct { + ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"` + Voter sdk.AccAddress `json:"voter" yaml:"voter"` +} +``` + +## State Modifications + +* Create a new `Vote` +* If the proposal is over the threshold: + * Enact the proposal (proposals may cause state modifications) + * Delete the proposal and associated votes \ No newline at end of file diff --git a/x/committee/spec/04_events.md b/x/committee/spec/04_events.md new file mode 100644 index 00000000..0e31e71d --- /dev/null +++ b/x/committee/spec/04_events.md @@ -0,0 +1,33 @@ +# Events + +The `x/committee` module emits the following events: + +## MsgSubmitProposal + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------------|--------------------| +| proposal_submit | committee_id | {committee ID} | +| proposal_submit | proposal_id | {proposal ID} | +| message | module | committee | +| message | sender | {sender address} | + +## MsgVote + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------------|--------------------| +| proposal_vote | committee_id | {committee ID} | +| proposal_vote | proposal_id | {proposal ID} | +| proposal_vote | voter | {voter address} | +| proposal_close | committee_id | {committee ID} | +| proposal_close | proposal_id | {proposal ID} | +| proposal_close | status | {outcome} | +| message | module | committee | +| message | sender | {sender address} | + +## BeginBlock + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------------|--------------------| +| proposal_close | committee_id | {committee ID} | +| proposal_close | proposal_id | {proposal ID} | +| proposal_close | status | proposal_timeout | diff --git a/x/committee/spec/05_params.md b/x/committee/spec/05_params.md new file mode 100644 index 00000000..9785d7f9 --- /dev/null +++ b/x/committee/spec/05_params.md @@ -0,0 +1,3 @@ +# Parameters + +The committee module has no parameters. Committees are created using the `x/gov` module and and inherit the parameters controlling governance proposals from `x/gov`. diff --git a/x/committee/spec/06_begin_block.md b/x/committee/spec/06_begin_block.md new file mode 100644 index 00000000..4e4e602d --- /dev/null +++ b/x/committee/spec/06_begin_block.md @@ -0,0 +1,10 @@ +# Begin Block + +At the start of each block, expired proposals are deleted. The logic is as follows: + +```go +// BeginBlocker runs at the start of every block. +func BeginBlocker(ctx sdk.Context, _ abci.RequestBeginBlock, k Keeper) { + k.CloseExpiredProposals(ctx) +} +``` diff --git a/x/committee/spec/README.md b/x/committee/spec/README.md index bcde3f69..fa9b3dff 100644 --- a/x/committee/spec/README.md +++ b/x/committee/spec/README.md @@ -3,17 +3,25 @@ ## Table of Contents + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[BeginBlock](06_begin_block.md)** + ## Overview -The `x/committee` module is an additional governance module to `cosmos-sdk/x/gov`. +The `x/committee` module is Cosmos-SDK module that acts as an additional governance module to `cosmos-sdk/x/gov`. It allows groups of accounts to vote on and enact proposals without a full chain governance vote. Certain proposal types can then be decided on quickly in emergency situations, or low risk parameter updates can be delegated to a smaller group of individuals. Committees work with "proposals", using the same type from the `gov` module so they are compatible with all existing proposal types such as param changes, or community pool spend, or text proposals. -Committees have members and permissions. +Committees have members and permissions. Committees are 'elected' via traditional `gov` proposals - ie. all coin-holders vote on the creation, deletion, and updating of committees. -Members vote on proposals, with just simple one vote per member, no deposits or slashing. More sophisticated voting could be added. +Members of committees vote on proposals, with one vote per member and no deposits or slashing. Only a member of a committee can submit a proposal for that committee. More sophisticated voting could be added, as well as the ability for committees to edit themselves or other committees. A proposal passes when the number of votes is over the threshold for that committee. Vote thresholds are set per committee. Committee members vote yes by casting a vote and vote no by abstaining from voting and letting the proposal expire. Permissions scope the allowed set of proposals a committee can enact. For example: @@ -21,6 +29,4 @@ Permissions scope the allowed set of proposals a committee can enact. For exampl - allow the committee to change auction bid increments, but only within the range [0, 0.1] - allow the committee to only disable cdp msg types, but not staking or gov -A permission acts as a filter for incoming gov proposals, rejecting them if they do not pass. A permission can be any type with a method `Allows(p Proposal) bool`. They reject all proposals that they don't explicitly allow. - -This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can allow a committee to only change a particular param, or only change params within a certain percentage. +A permission acts as a filter for incoming gov proposals, rejecting them at the handler if they do not have the required permissions. A permission can be any type with a method `Allows(p Proposal) bool`. The handler will reject all proposals that are not explicitly allowed. This allows permissions to be parameterized to allow fine grained control specified at runtime. For example a generic parameter permission type can allow a committee to only change a particular param, or only change params within a certain range. diff --git a/x/committee/types/committee.go b/x/committee/types/committee.go index fcb54720..4d5a8d04 100644 --- a/x/committee/types/committee.go +++ b/x/committee/types/committee.go @@ -63,31 +63,30 @@ func (c Committee) Validate() error { for _, m := range c.Members { // check there are no duplicate members if _, ok := addressMap[m.String()]; ok { - return fmt.Errorf("duplicate member found in committee, %s", m) + return fmt.Errorf("committe cannot have duplicate members, %s", m) } // check for valid addresses if m.Empty() { - return fmt.Errorf("committee %d invalid: found empty member address", c.ID) + return fmt.Errorf("committee cannot have empty member address") } addressMap[m.String()] = true - } if len(c.Members) == 0 { - return fmt.Errorf("committee %d invalid: cannot have zero members", c.ID) + return fmt.Errorf("committee cannot have zero members") } if len(c.Description) > MaxCommitteeDescriptionLength { - return fmt.Errorf("invalid description") + return fmt.Errorf("description length %d longer than max allowed %d", len(c.Description), MaxCommitteeDescriptionLength) } // threshold must be in the range (0,1] if c.VoteThreshold.IsNil() || c.VoteThreshold.LTE(sdk.ZeroDec()) || c.VoteThreshold.GT(sdk.NewDec(1)) { - return fmt.Errorf("invalid threshold") + return fmt.Errorf("invalid threshold: %s", c.VoteThreshold) } if c.ProposalDuration < 0 { - return fmt.Errorf("invalid proposal duration") + return fmt.Errorf("invalid proposal duration: %s", c.ProposalDuration) } return nil diff --git a/x/committee/types/events.go b/x/committee/types/events.go index 8ccbf667..777256a9 100644 --- a/x/committee/types/events.go +++ b/x/committee/types/events.go @@ -10,6 +10,7 @@ const ( AttributeKeyCommitteeID = "committee_id" AttributeKeyProposalID = "proposal_id" AttributeKeyProposalCloseStatus = "status" + AttributeKeyVoter = "voter" AttributeValueProposalPassed = "proposal_passed" AttributeValueProposalTimeout = "proposal_timeout" AttributeValueProposalFailed = "proposal_failed" diff --git a/x/committee/types/genesis.go b/x/committee/types/genesis.go index a7539a45..6ee3c4ce 100644 --- a/x/committee/types/genesis.go +++ b/x/committee/types/genesis.go @@ -65,7 +65,7 @@ func (gs GenesisState) Validate() error { } } - // validate proposals - pp.Val, no duplicate IDs, no ids >= nextID, committee needs to exist + // validate proposals proposalMap := make(map[uint64]bool, len(gs.Proposals)) for _, p := range gs.Proposals { // check there are no duplicate IDs diff --git a/x/incentive/alias.go b/x/incentive/alias.go index 877ddba3..e4774df6 100644 --- a/x/incentive/alias.go +++ b/x/incentive/alias.go @@ -1,9 +1,10 @@ +package incentive + // nolint // autogenerated code using github.com/rigelrozanski/multitool // aliases generated for the following subdirectories: // ALIASGEN: github.com/kava-labs/kava/x/incentive/keeper // ALIASGEN: github.com/kava-labs/kava/x/incentive/types -package incentive import ( "github.com/kava-labs/kava/x/incentive/keeper" @@ -11,18 +12,24 @@ import ( ) const ( - EventTypeClaim = types.EventTypeClaim - AttributeValueCategory = types.AttributeValueCategory - AttributeKeySender = types.AttributeKeySender - ModuleName = types.ModuleName - StoreKey = types.StoreKey - RouterKey = types.RouterKey - DefaultParamspace = types.DefaultParamspace - QuerierRoute = types.QuerierRoute - QueryGetClaims = types.QueryGetClaims - RestClaimOwner = types.RestClaimOwner - RestClaimDenom = types.RestClaimDenom - QueryGetParams = types.QueryGetParams + EventTypeClaim = types.EventTypeClaim + EventTypeRewardPeriod = types.EventTypeRewardPeriod + EventTypeClaimPeriod = types.EventTypeClaimPeriod + EventTypeClaimPeriodExpiry = types.EventTypeClaimPeriodExpiry + AttributeValueCategory = types.AttributeValueCategory + AttributeKeyClaimedBy = types.AttributeKeyClaimedBy + AttributeKeyClaimAmount = types.AttributeKeyClaimAmount + AttributeKeyRewardPeriod = types.AttributeKeyRewardPeriod + AttributeKeyClaimPeriod = types.AttributeKeyClaimPeriod + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + DefaultParamspace = types.DefaultParamspace + QuerierRoute = types.QuerierRoute + QueryGetClaims = types.QueryGetClaims + RestClaimOwner = types.RestClaimOwner + RestClaimDenom = types.RestClaimDenom + QueryGetParams = types.QueryGetParams ) var ( @@ -46,6 +53,7 @@ var ( NewRewardPeriod = types.NewRewardPeriod NewClaimPeriod = types.NewClaimPeriod NewClaim = types.NewClaim + NewRewardPeriodFromReward = types.NewRewardPeriodFromReward // variable aliases ModuleCdc = types.ModuleCdc diff --git a/x/incentive/keeper/payout.go b/x/incentive/keeper/payout.go index 20b339fe..b9a67c5b 100644 --- a/x/incentive/keeper/payout.go +++ b/x/incentive/keeper/payout.go @@ -1,8 +1,6 @@ package keeper import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/auth" @@ -29,10 +27,13 @@ func (k Keeper) PayoutClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, return err } + k.DeleteClaim(ctx, addr, denom, id) + ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeClaim, - sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", addr)), + sdk.NewAttribute(types.AttributeKeyClaimedBy, addr.String()), + sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.Reward.String()), ), ) return nil @@ -103,6 +104,13 @@ func (k Keeper) DeleteExpiredClaimsAndClaimPeriods(ctx sdk.Context) { return false }) k.DeleteClaimPeriod(ctx, cp.ID, cp.Denom) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaimPeriodExpiry, + sdk.NewAttribute(types.AttributeKeyClaimPeriod, cp.String()), + ), + ) + return false }) } diff --git a/x/incentive/keeper/rewards.go b/x/incentive/keeper/rewards.go index 9fd11d20..d8c2054b 100644 --- a/x/incentive/keeper/rewards.go +++ b/x/incentive/keeper/rewards.go @@ -20,18 +20,15 @@ func (k Keeper) HandleRewardPeriodExpiry(ctx sdk.Context, rp types.RewardPeriod) // CreateNewRewardPeriod creates a new reward period from the input reward func (k Keeper) CreateNewRewardPeriod(ctx sdk.Context, reward types.Reward) { - // reward periods store the amount of rewards paid PER SECOND - rewardsPerSecond := sdk.NewDecFromInt(reward.AvailableRewards.Amount).Quo(sdk.NewDecFromInt(sdk.NewInt(int64(reward.Duration.Seconds())))).TruncateInt() - rewardCoinPerSecond := sdk.NewCoin(reward.AvailableRewards.Denom, rewardsPerSecond) - rp := types.RewardPeriod{ - Denom: reward.Denom, - Start: ctx.BlockTime(), - End: ctx.BlockTime().Add(reward.Duration), - Reward: rewardCoinPerSecond, - ClaimEnd: ctx.BlockTime().Add(reward.Duration).Add(reward.ClaimDuration), - ClaimTimeLock: reward.TimeLock, - } + rp := types.NewRewardPeriodFromReward(reward, ctx.BlockTime()) k.SetRewardPeriod(ctx, rp) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeRewardPeriod, + sdk.NewAttribute(types.AttributeKeyRewardPeriod, rp.String()), + ), + ) } // CreateAndDeleteRewardPeriods creates reward periods for active rewards that don't already have a reward period and deletes reward periods for inactive rewards that currently have a reward period @@ -95,6 +92,12 @@ func (k Keeper) ApplyRewardsToCdps(ctx sdk.Context) { func (k Keeper) CreateUniqueClaimPeriod(ctx sdk.Context, denom string, end time.Time, timeLock time.Duration) { id := k.GetNextClaimPeriodID(ctx, denom) claimPeriod := types.NewClaimPeriod(denom, id, end, timeLock) + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeClaimPeriod, + sdk.NewAttribute(types.AttributeKeyClaimPeriod, claimPeriod.String()), + ), + ) k.SetClaimPeriod(ctx, claimPeriod) k.SetNextClaimPeriodID(ctx, denom, id+1) } diff --git a/x/incentive/spec/01_concepts.md b/x/incentive/spec/01_concepts.md new file mode 100644 index 00000000..3074feb0 --- /dev/null +++ b/x/incentive/spec/01_concepts.md @@ -0,0 +1,5 @@ +# Concepts + +This module presents an implementation of user incentives that are controlled by governance. When users take a certain action, in this case opening a CDP, they become eligible for rewards. Rewards are __opt in__ meaning that users must submit a message before the claim deadline to claim their rewards. The goals and background of this module were subject of a previous Kava governance proposal, which can be found [here](https://ipfs.io/ipfs/QmSYedssC3nyQacDJmNcREtgmTPyaMx2JX7RNkMdAVkdkr/user-growth-fund-proposal.pdf). + +When governance adds a collateral type to be eligible for rewards, they set the rate (coins/time) at which rewards are given to users, the length of each reward period, the length of each claim period, and the amount of time reward coins must vest before users who claim them can transfer them. For the duration of a reward period, any user that has minted USDX using an eligible collateral type will ratably accumulate rewards in a `Claim` object. For example, if a user has minted 10% of all USDX for the duration of the reward period, they will earn 10% of all rewards for that period. When the reward period ends, the claim period begins immediately, at which point users can submit a message to claim their rewards. Rewards are time-locked, meaning that when a user claims rewards they will receive them as a vesting balance on their account. Vesting balances can be used to stake coins, but cannot be transferred until the vesting period ends. diff --git a/x/incentive/spec/02_state.md b/x/incentive/spec/02_state.md new file mode 100644 index 00000000..34b5258d --- /dev/null +++ b/x/incentive/spec/02_state.md @@ -0,0 +1,57 @@ +# State + +## Parameters and Genesis State + +`Parameters` define the collateral types which are eligible for rewards, the rate at which rewards are given to users, and the amount of time rewards must vest before users can transfer them. + +```go +// Params governance parameters for the incentive module +type Params struct { + Active bool `json:"active" yaml:"active"` // top level governance switch to disable all rewards + Rewards Rewards `json:"rewards" yaml:"rewards"` +} + +// Reward stores the specified state for a single reward period. +type Reward struct { + Active bool `json:"active" yaml:"active"` // governance switch to disable a period + Denom string `json:"denom" yaml:"denom"` // the collateral denom rewards apply to, must be found in the cdp collaterals + AvailableRewards sdk.Coin `json:"available_rewards" yaml:"available_rewards"` // the total amount of coins distributed per period + Duration time.Duration `json:"duration" yaml:"duration"` // the duration of the period + TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` // how long rewards for this period are timelocked + ClaimDuration time.Duration `json:"claim_duration" yaml:"claim_duration"` // how long users have after the period ends to claim their rewards +} +``` + +`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the incentive module to resume. + +```go +// GenesisState is the state that must be provided at genesis. +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` + RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"` + ClaimPeriods ClaimPeriods `json:"claim_periods" yaml:"claim_periods"` + Claims Claims `json:"claims" yaml:"claims"` + NextClaimPeriodIDs GenesisClaimPeriodIDs `json:"next_claim_period_ids" yaml:"next_claim_period_ids"` +} +``` + +## Store + +For complete details for how items are stored, see [keys.go](../types/keys.go). + +### Reward Period Creation + +At genesis, or when a collateral is added to rewards, a `RewardPeriod` is created in the store by adding to the existing array of `[]RewardPeriod`. If the previous period for that collateral expired, it is deleted. This implies that, for each collateral, there will only ever be one reward period. + +### Reward Period Deletion + +When a `RewardPeriod` expires, a new `ClaimPeriod` is created in the store with the next sequential ID for that collateral (ie, if the previous claim period was ID 1, the next one will be ID 2) and the current `RewardPeriod` is deleted from the array of `[]RewardPeriod`. + +### Reward Claim Creation + +Every block, CDPs are iterated over and the collateral denom is checked for rewards eligibility. For eligible CDPs, a `Claim` is created in the store for all CDP owners, if one doesn't already exist. The claim object is associated with a `ClaimPeriod` via the ID. This implies that a `Claim` is created before `ClaimPeriod` are created. Therefore, a user who submits a `MsgClaimReward` will only be paid out IF 1) they have one or more active `Claim` objects, and 2) the `ClaimPeriod` with the associated ID for that object exists AND the current block time is between the start time and end time for that `ClaimPeriod`. + +### Reward Claim Deletion + +For claimed rewards, the `Claim` is deleted from the store by deleting the key associated with that denom, ID, and owner. Unclaimed rewards are handled as follows: Each block, the `ClaimPeriod` objects for each denom are iterated over and checked for expiry. If expired, all `Claim` objects for that ID are deleted, as well as the `ClaimPeriod` object. Since claim periods are monotonically increasing, once a non-expired claim period is reached, the iteration can be stopped. diff --git a/x/incentive/spec/03_messages.md b/x/incentive/spec/03_messages.md new file mode 100644 index 00000000..8d1910b2 --- /dev/null +++ b/x/incentive/spec/03_messages.md @@ -0,0 +1,16 @@ +# Messages + +Users claim rewards using a `MsgClaimReward`. + +```go +// MsgClaimReward message type used to claim rewards +type MsgClaimReward struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Denom string `json:"denom" yaml:"denom"` +} +``` + +## State Modifications + +* Accumulated rewards for active claims are transferred from the `kavadist` module account to the users account as vesting coins +* The corresponding claim object(s) are deleted from the store diff --git a/x/incentive/spec/04_events.md b/x/incentive/spec/04_events.md new file mode 100644 index 00000000..8bc1d8a6 --- /dev/null +++ b/x/incentive/spec/04_events.md @@ -0,0 +1,20 @@ +# Events + +The `x/incentive` module emits the following events: + +## MsgClaimReward + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------------|--------------------| +| claim_reward | claimed_by | {claiming address} | +| claim_reward | claim_amount | {amount claimed} | +| message | module | incentive | +| message | sender | {sender address} | + +## BeginBlock + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------------|--------------------| +| new_claim_period | claim_period | {claim period} | +| new_reward_period | reward_period | {reward period} | +| claim_period_expiry | claim_period | {claim period} | diff --git a/x/incentive/spec/05_params.md b/x/incentive/spec/05_params.md new file mode 100644 index 00000000..ce0b1ac9 --- /dev/null +++ b/x/incentive/spec/05_params.md @@ -0,0 +1,19 @@ +# Parameters + +The incentive module contains the following parameters: + +| Key | Type | Example | Description | +|------------|----------------|---------------|--------------------------------------------------| +| Active | bool | "true" | boolean for if this module is active | +| Rewards | array (Reward) | [{see below}] | array of params for each inflationary period | + +Each `Reward` has the following parameters + +| Key | Type | Example | Description | +|------------------|--------------------|----------------------------------|----------------------------------------------------------------| +| Active | bool | "true | boolean for if rewards for this collateral are active | +| Denom | string | "bnb" | the collateral for which rewards are eligible | +| AvailableRewards | object (coin) | {"denom":"kava","amount":"1000"} | the rewards available per reward period | +| Duration | string (time ns) | "172800000000000" | the duration of each reward period | +| TimeLock | string (time ns) | "172800000000000" | the duration for which claimed rewards will be vesting | +| ClaimDuration | string (time ns) | "172800000000000" | how long users have to claim rewards before they expire | diff --git a/x/incentive/spec/06_begin_block.md b/x/incentive/spec/06_begin_block.md new file mode 100644 index 00000000..47825384 --- /dev/null +++ b/x/incentive/spec/06_begin_block.md @@ -0,0 +1,11 @@ +# Begin Block + +At the start of each block, expired claims and claim periods are deleted, rewards are applied to CDPs for any ongoing reward periods, expired reward periods are deleted and replaced with a new reward period (if active), and claim periods are created for expiring reward periods. The logic is as follows: + +```go +func BeginBlocker(ctx sdk.Context, k Keeper) { + k.DeleteExpiredClaimsAndClaimPeriods(ctx) + k.ApplyRewardsToCdps(ctx) + k.CreateAndDeleteRewardPeriods(ctx) +} +``` diff --git a/x/incentive/spec/README.md b/x/incentive/spec/README.md new file mode 100644 index 00000000..91b783c5 --- /dev/null +++ b/x/incentive/spec/README.md @@ -0,0 +1,17 @@ +# `incentive` + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[BeginBlock](06_begin_block.md)** + +## Abstract + +`x/incentive` is an implementation of a Cosmos SDK Module that allows for governance controlled user incentives for users who create stable assets by opening a collateralized debt position (CDP). Governance proposes an array of collateral rewards, with each item representing a collateral type that will be eligible for rewards. Each collateral reward specifies the number of coins awarded per period, the length of rewards periods, the length of claim periods. Governance can alter the collateral rewards using parameter change proposals as well as adding or removing collateral types. All changes to parameters would take place in the _next_ period. + +### Dependencies + +This module depends on `x/cdp` for users to be able to create CDPs and on `x/kavadist`, which controls the module account from where rewards are spent. In the event that the module account is not funded, user's attempt to claim rewards will fail. diff --git a/x/incentive/types/events.go b/x/incentive/types/events.go index 09d0cd47..e431617a 100644 --- a/x/incentive/types/events.go +++ b/x/incentive/types/events.go @@ -1,8 +1,15 @@ package types +// Events emitted by the incentive module const ( - EventTypeClaim = "claim_reward" + EventTypeClaim = "claim_reward" + EventTypeRewardPeriod = "new_reward_period" + EventTypeClaimPeriod = "new_claim_period" + EventTypeClaimPeriodExpiry = "claim_period_expiry" - AttributeValueCategory = ModuleName - AttributeKeySender = "sender" + AttributeValueCategory = ModuleName + AttributeKeyClaimedBy = "claimed_by" + AttributeKeyClaimAmount = "claim_amount" + AttributeKeyRewardPeriod = "reward_period" + AttributeKeyClaimPeriod = "claim_period" ) diff --git a/x/incentive/types/rewards.go b/x/incentive/types/rewards.go index 237b222a..76539c86 100644 --- a/x/incentive/types/rewards.go +++ b/x/incentive/types/rewards.go @@ -25,8 +25,8 @@ func (rp RewardPeriod) String() string { End: %s, Reward: %s, Claim End: %s, - Claim Time Lock: %s`, - rp.Denom, rp.Start, rp.End, rp.Reward, rp.ClaimEnd, rp.ClaimTimeLock) + Claim Time Lock: %s + `, rp.Denom, rp.Start, rp.End, rp.Reward, rp.ClaimEnd, rp.ClaimTimeLock) } // NewRewardPeriod returns a new RewardPeriod @@ -52,6 +52,16 @@ type ClaimPeriod struct { TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` } +// String implements fmt.Stringer +func (cp ClaimPeriod) String() string { + return fmt.Sprintf(`Claim Period: + Denom: %s, + ID: %d, + End: %s, + Claim Time Lock: %s + `, cp.Denom, cp.ID, cp.End, cp.TimeLock) +} + // NewClaimPeriod returns a new ClaimPeriod func NewClaimPeriod(denom string, id uint64, end time.Time, timeLock time.Duration) ClaimPeriod { return ClaimPeriod{ @@ -89,9 +99,24 @@ func (c Claim) String() string { Owner: %s, Denom: %s, Reward: %s, - Claim Period ID: %d,`, - c.Owner, c.Denom, c.Reward, c.ClaimPeriodID) + Claim Period ID: %d, + `, c.Owner, c.Denom, c.Reward, c.ClaimPeriodID) } // Claims array of Claim type Claims []Claim + +// NewRewardPeriodFromReward returns a new reward period from the input reward and block time +func NewRewardPeriodFromReward(reward Reward, blockTime time.Time) RewardPeriod { + // note: reward periods store the amount of rewards paid PER SECOND + rewardsPerSecond := sdk.NewDecFromInt(reward.AvailableRewards.Amount).Quo(sdk.NewDecFromInt(sdk.NewInt(int64(reward.Duration.Seconds())))).TruncateInt() + rewardCoinPerSecond := sdk.NewCoin(reward.AvailableRewards.Denom, rewardsPerSecond) + return RewardPeriod{ + Denom: reward.Denom, + Start: blockTime, + End: blockTime.Add(reward.Duration), + Reward: rewardCoinPerSecond, + ClaimEnd: blockTime.Add(reward.Duration).Add(reward.ClaimDuration), + ClaimTimeLock: reward.TimeLock, + } +} diff --git a/x/kavadist/keeper/mint.go b/x/kavadist/keeper/mint.go index 8f909cd7..c7ae61dc 100644 --- a/x/kavadist/keeper/mint.go +++ b/x/kavadist/keeper/mint.go @@ -81,7 +81,7 @@ func (k Keeper) mintInflationaryCoins(ctx sdk.Context, inflationRate sdk.Dec, ti ctx.EventManager().EmitEvent( sdk.NewEvent( types.EventTypeKavaDist, - sdk.NewAttribute(types.AttributeKeyInflation, amountToMint.String()), + sdk.NewAttribute(types.AttributeKeyInflation, sdk.NewCoin(denom, amountToMint).String()), ), ) return nil diff --git a/x/kavadist/spec/01_concepts.md b/x/kavadist/spec/01_concepts.md new file mode 100644 index 00000000..4c9c4bd7 --- /dev/null +++ b/x/kavadist/spec/01_concepts.md @@ -0,0 +1,3 @@ +# Concepts + +The minting mechanism in this module is designed to allow governance to determine a set of inflationary periods and the APR rate of inflation for each period. This module mints coins each block according to the schedule such that after 1 year the APR inflation worth of coins will have been minted. Governance can alter the APR inflation using a parameter change proposal. Parameter change proposals that change the APR will take effect in the block after they pass. \ No newline at end of file diff --git a/x/kavadist/spec/02_state.md b/x/kavadist/spec/02_state.md new file mode 100644 index 00000000..cf34c8d6 --- /dev/null +++ b/x/kavadist/spec/02_state.md @@ -0,0 +1,30 @@ +# State + +## Parameters and Genesis State + +`Parameters` define the rate at which inflationary coins are minted and for how long inflationary periods last. + +```go +// Params governance parameters for kavadist module +type Params struct { + Active bool `json:"active" yaml:"active"` + Periods Periods `json:"periods" yaml:"periods"` +} + +// Period stores the specified start and end dates, and the inflation, expressed as a decimal representing the yearly APR of tokens that will be minted during that period +type Period struct { + Start time.Time `json:"start" yaml:"start"` // example "2020-03-01T15:20:00Z" + End time.Time `json:"end" yaml:"end"` // example "2020-06-01T15:20:00Z" + Inflation sdk.Dec `json:"inflation" yaml:"inflation"` // example "1.000000003022265980" - 10% inflation +} +``` + +`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the kavadist module to resume. + +```go +// GenesisState is the state that must be provided at genesis. +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` +} +``` diff --git a/x/kavadist/spec/03_messages.md b/x/kavadist/spec/03_messages.md new file mode 100644 index 00000000..24f902d9 --- /dev/null +++ b/x/kavadist/spec/03_messages.md @@ -0,0 +1,3 @@ +# Messages + +There are no messages in the kavadist module. All state transitions are controlled by parameters, which can be updated via parameter change proposals. diff --git a/x/kavadist/spec/04_events.md b/x/kavadist/spec/04_events.md new file mode 100644 index 00000000..37f328ab --- /dev/null +++ b/x/kavadist/spec/04_events.md @@ -0,0 +1,10 @@ +# Events + +The `x/kavadist` module emits the following events: + +## BeginBlock + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------------|-----------------| +| kavadist | kava_dist_inflation | {amount} | +| kavadist | kava_dist_status | "inactive" | diff --git a/x/kavadist/spec/05_params.md b/x/kavadist/spec/05_params.md new file mode 100644 index 00000000..050384b7 --- /dev/null +++ b/x/kavadist/spec/05_params.md @@ -0,0 +1,15 @@ +# Parameters + +The kavadist module has the following parameters: + +| Key | Type | Example | Description | +|------------|----------------|---------------|--------------------------------------------------| +| Periods | array (Period) | [{see below}] | array of params for each inflationary period | + +Each `Period` has the following parameters + +| Key | Type | Example | Description | +|------------|--------------------|--------------------------|----------------------------------------------------------------| +| Start | time.Time | "2020-03-01T15:20:00Z" | the time when the period will start | +| End | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end | +| Inflation | sdk.Dec | "1.000000003022265980" | the per-second inflation for the period | diff --git a/x/kavadist/spec/06_begin_block.md b/x/kavadist/spec/06_begin_block.md new file mode 100644 index 00000000..d19ebe38 --- /dev/null +++ b/x/kavadist/spec/06_begin_block.md @@ -0,0 +1,12 @@ +# Begin Block + +At the start of each block, the inflationary coins for the ongoing period, if any, are minted. The logic is as follows: + +```go + func BeginBlocker(ctx sdk.Context, k Keeper) { + err := k.MintPeriodInflation(ctx) + if err != nil { + panic(err) + } + } +``` diff --git a/x/kavadist/spec/README.md b/x/kavadist/spec/README.md new file mode 100644 index 00000000..f846f459 --- /dev/null +++ b/x/kavadist/spec/README.md @@ -0,0 +1,13 @@ +# `kavadist` + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[BeginBlock](06_begin_block.md)** + +## Abstract + +`x/kavadist` is an implementation of a Cosmos SDK Module that allows for governance controlled minting of coins into a module account. Coins are minted during inflationary periods, which each period have a governance specified APR and duration. This module does not provide functionality for spending or distributing the minted coins. diff --git a/x/pricefeed/abci.go b/x/pricefeed/abci.go index 1e56f918..49623353 100644 --- a/x/pricefeed/abci.go +++ b/x/pricefeed/abci.go @@ -9,15 +9,15 @@ import ( // EndBlocker updates the current pricefeed func EndBlocker(ctx sdk.Context, k Keeper) { // Update the current price of each asset. - for _, a := range k.GetMarkets(ctx) { - if a.Active { - err := k.SetCurrentPrices(ctx, a.MarketID) + for _, market := range k.GetMarkets(ctx) { + if market.Active { + err := k.SetCurrentPrices(ctx, market.MarketID) if err != nil { // In the event of failure, emit an event. ctx.EventManager().EmitEvent( sdk.NewEvent( EventTypeNoValidPrices, - sdk.NewAttribute(AttributeKeyPriceUpdateFailed, fmt.Sprintf("%s", a.MarketID)), + sdk.NewAttribute(AttributeMarketID, fmt.Sprintf("%s", market.MarketID)), ), ) continue diff --git a/x/pricefeed/alias.go b/x/pricefeed/alias.go index 75affe59..6a734e06 100644 --- a/x/pricefeed/alias.go +++ b/x/pricefeed/alias.go @@ -12,26 +12,25 @@ import ( // nolint const ( - EventTypeMarketPriceUpdated = types.EventTypeMarketPriceUpdated - EventTypeOracleUpdatedPrice = types.EventTypeOracleUpdatedPrice - EventTypeNoValidPrices = types.EventTypeNoValidPrices - AttributeValueCategory = types.AttributeValueCategory - AttributeMarketID = types.AttributeMarketID - AttributeMarketPrice = types.AttributeMarketPrice - AttributeOracle = types.AttributeOracle - AttributeExpiry = types.AttributeExpiry - AttributeKeyPriceUpdateFailed = types.AttributeKeyPriceUpdateFailed - ModuleName = types.ModuleName - StoreKey = types.StoreKey - RouterKey = types.RouterKey - QuerierRoute = types.QuerierRoute - DefaultParamspace = types.DefaultParamspace - TypeMsgPostPrice = types.TypeMsgPostPrice - QueryGetParams = types.QueryGetParams - QueryMarkets = types.QueryMarkets - QueryOracles = types.QueryOracles - QueryRawPrices = types.QueryRawPrices - QueryPrice = types.QueryPrice + EventTypeMarketPriceUpdated = types.EventTypeMarketPriceUpdated + EventTypeOracleUpdatedPrice = types.EventTypeOracleUpdatedPrice + EventTypeNoValidPrices = types.EventTypeNoValidPrices + AttributeValueCategory = types.AttributeValueCategory + AttributeMarketID = types.AttributeMarketID + AttributeMarketPrice = types.AttributeMarketPrice + AttributeOracle = types.AttributeOracle + AttributeExpiry = types.AttributeExpiry + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + QuerierRoute = types.QuerierRoute + DefaultParamspace = types.DefaultParamspace + TypeMsgPostPrice = types.TypeMsgPostPrice + QueryGetParams = types.QueryGetParams + QueryMarkets = types.QueryMarkets + QueryOracles = types.QueryOracles + QueryRawPrices = types.QueryRawPrices + QueryPrice = types.QueryPrice ) // nolint diff --git a/x/pricefeed/spec/01_concepts.md b/x/pricefeed/spec/01_concepts.md new file mode 100644 index 00000000..76627d8d --- /dev/null +++ b/x/pricefeed/spec/01_concepts.md @@ -0,0 +1,3 @@ +# Concepts + +Prices can be posted by any account which is added as an oracle. Oracles are specific to each market and can be updated via param change proposals. When an oracle posts a price, they submit a message to the blockchain that contains the current price for that market and a time when that price should be considered expired. If an oracle posts a new price, that price becomes the current price for that oracle, regardless of the previous price's expiry. A group of prices posted by a set of oracles for a particular market are referred to as 'raw prices' and the current median price of all valid oracle prices is referred to as the 'current price'. Each block, the current price for each market is determined by calculating the median of the raw prices. diff --git a/x/pricefeed/spec/02_state.md b/x/pricefeed/spec/02_state.md new file mode 100644 index 00000000..5ff2e5a8 --- /dev/null +++ b/x/pricefeed/spec/02_state.md @@ -0,0 +1,44 @@ +# State + +## Parameters and genesis state + +`Paramaters` determine which markets are tracked by the pricefeed and which oracles are authorized to post prices for a given market. There is only one active parameter set at any given time. Updates to parameters can be made via on-chain parameter update proposals. + +```go +// Params params for pricefeed. Can be altered via governance +type Params struct { + Markets Markets `json:"markets" yaml:"markets"` // Array containing the markets supported by the pricefeed +} + +// Market an asset in the pricefeed +type Market struct { + MarketID string `json:"market_id" yaml:"market_id"` + BaseAsset string `json:"base_asset" yaml:"base_asset"` + QuoteAsset string `json:"quote_asset" yaml:"quote_asset"` + Oracles []sdk.AccAddress `json:"oracles" yaml:"oracles"` + Active bool `json:"active" yaml:"active"` +} + +type Markets []Market +``` + +`GenesisState` defines the state that must be persisted when the blockchain stops/stars in order for the normal function of the pricefeed to resume. + +```go +// GenesisState - pricefeed state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` + PostedPrices []PostedPrice `json:"posted_prices" yaml:"posted_prices"` +} + +// PostedPrice price for market posted by a specific oracle +type PostedPrice struct { + MarketID string `json:"market_id" yaml:"market_id"` + OracleAddress sdk.AccAddress `json:"oracle_address" yaml:"oracle_address"` + Price sdk.Dec `json:"price" yaml:"price"` + Expiry time.Time `json:"expiry" yaml:"expiry"` +} + +type PostedPrices []PostedPrice +``` + diff --git a/x/pricefeed/spec/03_messages.md b/x/pricefeed/spec/03_messages.md new file mode 100644 index 00000000..8ddb5f07 --- /dev/null +++ b/x/pricefeed/spec/03_messages.md @@ -0,0 +1,20 @@ +# Messages + +## Posting Prices + +An authorized oraclef for a particular market can post the current price for that market using the `MsgPostPrice` type. + +```go +// MsgPostPrice struct representing a posted price message. +// Used by oracles to input prices to the pricefeed +type MsgPostPrice struct { + From sdk.AccAddress `json:"from" yaml:"from"` // client that sent in this address + MarketID string `json:"market_id" yaml:"market_id"` // asset code used by exchanges/api + Price sdk.Dec `json:"price" yaml:"price"` // price in decimal (max precision 18) + Expiry time.Time `json:"expiry" yaml:"expiry"` // expiry time +} +``` + +### State Modifications + +* Update the raw price for the oracle for this market. This replaces any previous price for that oracle. diff --git a/x/pricefeed/spec/04_events.md b/x/pricefeed/spec/04_events.md new file mode 100644 index 00000000..d114dcec --- /dev/null +++ b/x/pricefeed/spec/04_events.md @@ -0,0 +1,22 @@ +# Events + +The `x/pricefeed` module emits the following events: + +## MsgPostPrice + +| Type | Attribute Key | Attribute Value | +|----------------------|---------------|------------------| +| oracle_updated_price | market_id | {market ID} | +| oracle_updated_price | oracle | {oracle} | +| oracle_updated_price | market_price | {price} | +| oracle_updated_price | expiry | {expiry} | +| message | module | pricefeed | +| message | sender | {sender address} | + +## BeginBlock + +| Type | Attribute Key | Attribute Value | +|----------------------|-----------------|-----------------| +| market_price_updated | market_id | {market ID} | +| market_price_updated | market_price | {price} | +| no_valid_prices | market_id | {market ID} | diff --git a/x/pricefeed/spec/05_params.md b/x/pricefeed/spec/05_params.md new file mode 100644 index 00000000..1dc18f41 --- /dev/null +++ b/x/pricefeed/spec/05_params.md @@ -0,0 +1,17 @@ +# Parameters + +The pricefeed module has the following parameters: + +| Key | Type | Example | Description | +|------------|----------------|---------------|--------------------------------------------------| +| Markets | array (Market) | [{see below}] | array of params for each market in the pricefeed | + +Each `Market` has the following parameters + +| Key | Type | Example | Description | +|------------|--------------------|--------------------------|----------------------------------------------------------------| +| MarketID | string | "bnb:usd" | identifier for the market -- **must** be unique across markets | +| BaseAsset | string | "bnb" | the base asset for the market pair | +| QuoteAsset | string | "usd" | the quote asset for the market pair | +| Oracles | array (AccAddress) | ["kava1...", "kava1..."] | addresses which can post prices for the market | +| Active | bool | true | flag to disable oracle interactions with the module | diff --git a/x/pricefeed/spec/06_end_block.md b/x/pricefeed/spec/06_end_block.md new file mode 100644 index 00000000..4907cced --- /dev/null +++ b/x/pricefeed/spec/06_end_block.md @@ -0,0 +1,26 @@ +# End Block + +At the end of each block, the current price is calculated as the median of all raw prices for each market. The logic is as follows: + +```go +// EndBlocker updates the current pricefeed +func EndBlocker(ctx sdk.Context, k Keeper) { + // Update the current price of each asset. + for _, market := range k.GetMarkets(ctx) { + if market.Active { + err := k.SetCurrentPrices(ctx, market.MarketID) + if err != nil { + // In the event of failure, emit an event. + ctx.EventManager().EmitEvent( + sdk.NewEvent( + EventTypeNoValidPrices, + sdk.NewAttribute(AttributeMarketID, fmt.Sprintf("%s", market.MarketID)), + ), + ) + continue + } + } + } + return +} +``` diff --git a/x/pricefeed/spec/README.md b/x/pricefeed/spec/README.md new file mode 100644 index 00000000..32d88e86 --- /dev/null +++ b/x/pricefeed/spec/README.md @@ -0,0 +1,13 @@ +# `pricefeed` + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[EndBlock](06_end_block.md)** + +## Abstract + +`x/pricefeed` is an implementation of a Cosmos SDK Module that handles the posting of prices for various markets by a group of whitelisted oracles. At the end of each block, the median price of all oracle posted prices is determined for each market and stored. diff --git a/x/pricefeed/types/events.go b/x/pricefeed/types/events.go index 07a0007d..683a379b 100644 --- a/x/pricefeed/types/events.go +++ b/x/pricefeed/types/events.go @@ -6,10 +6,9 @@ const ( EventTypeOracleUpdatedPrice = "oracle_updated_price" EventTypeNoValidPrices = "no_valid_prices" - AttributeValueCategory = ModuleName - AttributeMarketID = "market_id" - AttributeMarketPrice = "market_price" - AttributeOracle = "oracle" - AttributeExpiry = "expiry" - AttributeKeyPriceUpdateFailed = "price_update_failed" + AttributeValueCategory = ModuleName + AttributeMarketID = "market_id" + AttributeMarketPrice = "market_price" + AttributeOracle = "oracle" + AttributeExpiry = "expiry" ) diff --git a/x/validator-vesting/spec/01_concepts.md b/x/validator-vesting/spec/01_concepts.md index 01dd9f5f..d2402318 100644 --- a/x/validator-vesting/spec/01_concepts.md +++ b/x/validator-vesting/spec/01_concepts.md @@ -1,8 +1,7 @@ # Concepts -The validator-vesting module is responsible for managing Validator Vesting Accounts, a vesting account for which the release of coins is tied to the validation of the blockchain. Validator Vesting Accounts implemnt the cosmos-sdk vesting account spec, which can be found [here](https://github.com/cosmos/cosmos-sdk/tree/master/x/auth/spec). +The validator-vesting module is responsible for managing Validator Vesting Accounts, a vesting account for which the release of coins is tied to the validation of the blockchain. Validator Vesting Accounts implement the cosmos-sdk vesting account spec, which can be found [here](https://github.com/cosmos/cosmos-sdk/tree/master/x/auth/spec). The main concept the Validator Vesting Account introduces is that of _conditional vesting_, or vesting accounts in which it is possible for some or all of the vesting coins to fail to vest. For Validator Vesting Accounts, vesting is broken down into user-specified __vesting periods__. Each vesting period specifies an amount of coins that could vest in that period, and how long the vesting period lasts. -For each vesting period, a __signing threshold__ is specified, which is the percentage of blocks that must be signed for the coins to successfully vest. After a period ends, coins that are successfully vested become freely spendable. Coins that do not successfuly vest are burned, or sent to an optional return address. - +For each vesting period, a __signing threshold__ is specified, which is the percentage of blocks that must be signed for the coins to successfully vest. After a period ends, coins that are successfully vested become freely spendable. Coins that do not successfully vest are burned, or sent to an optional return address. diff --git a/x/validator-vesting/spec/02_state.md b/x/validator-vesting/spec/02_state.md index 43d9b7bb..b849296d 100644 --- a/x/validator-vesting/spec/02_state.md +++ b/x/validator-vesting/spec/02_state.md @@ -2,24 +2,26 @@ ## Validator Vesting Account type -Validator Vesting Accounts implement the `cosmos-sdk` vesting account spec and extend the `PeriodicVestingAccountType`: +Validator Vesting Accounts implement the `cosmos-sdk` vesting account interfaces and extends the `PeriodicVestingAccountType`: ```go type ValidatorVestingAccount struct { *vesting.PeriodicVestingAccount + ValidatorAddress sdk.ConsAddress // The validator address which will be used to check if blocks were signed ReturnAddress sdk.AccAddress `json:"return_address" yaml:"return_address"` // The account where coins will be returned in the event of a failed vesting period SigningThreshold int64 `json:"signing_threshold" yaml:"signing_threshold"` // The percentage of blocks, as an integer between 0 and 100, that must be signed each period for coins to successfully vest. MissingSignCount []int64 `json:"missing_sign_count" yaml:"missing_sign_count"` // An array of two integers which track the number of blocks that were not signed during the current period and the total number of blocks which have passed during the current period, respectively. - VestingPeriodProgress [][]int `json:"vesting_period_progress" yaml:"vesting_period_progress"` //An 2d array with length equal to the number of vesting periods. After each period, the value at the first index of that period is updated with 1 to represent that the period is over. The value at the second index is updated to 0 for unsucessful vesting and 1 for successful vesting. - DebtAfterFailedVesting sdk.Coins `json:"debt_after_failed_vesting" yaml:"debt_after_failed_vesting"` // The debt currently owed by the account. Debt accumulates in the event of unsuccessful vesting periods. -} + VestingPeriodProgress [][]int `json:"vesting_period_progress" yaml:"vesting_period_progress"` //An 2d array with length equal to the number of vesting periods. After each period, the value at the first index of that period is updated with 1 to represent that the period is over. The value at the second index is updated to 0 for unsuccessful vesting and 1 for successful vesting. + DebtAfterFailedVesting sdk.Coins `json:"debt_after_failed_vesting" yaml:"debt_after_failed_vesting"` // The debt currently owed by the account. Debt accumulates in the event of unsuccessful vesting periods. + } ``` ## Stores There is one `KVStore` in `validator-vesting` which stores + * A mapping from each ValidatorVestingAccount `address` to `[]Byte{0}` * A mapping from `previous_block_time_prefix` to `time.Time` -The use of `[]Byte{0}` value for each `address` key reflects that this module only accesses the store to get or iterate over keys, and does not require storing an value. +The use of `[]Byte{0}` value for each `address` key reflects that this module only accesses the store to get or iterate over keys, and does not require storing an value. The storage of the actual account state is done by the cosmos-sdk `x/auth` `AccountKeeper`. diff --git a/x/validator-vesting/spec/03_begin_block.md b/x/validator-vesting/spec/03_begin_block.md index 967bb1ca..ffb1a1b8 100644 --- a/x/validator-vesting/spec/03_begin_block.md +++ b/x/validator-vesting/spec/03_begin_block.md @@ -1,6 +1,6 @@ # Begin Block -At each `BeginBlock`, all validator vesting accounts are iterated over to update the status of the current vesting period. Note that the address of each account is retreived by iterating over the keys in the `validator-vesting` store, while the account objects are stored and accessed using the `auth` module's `AccountKeeper`. For each account, the block count is incremented, the missed sign count is incremented if the validator did not sign the block or was not found in the validator set. By comparing the blocktime of the current `BeginBlock`, with the value of `previousBlockTime` stored in the `validator-vesting` store, it is determined if the end of the current period has been reached. If the current period has ended, the `VestingPeriodProgress` field is updated to reflect if the coins for the ending period successfully vested or not. After updates are made regarding the status of the current vesting period, any outstanding debt on the account is attempted to be collected. If there is enough `SpendableBalance` on the account to cover the debt, coins are sent to the `ReturnAdress` or burned. If there is not enough `SpendableBalance` to cover the debt, all delegations of the account are `Unbonded`. Once those unbonding events reach maturity, the coins freed from the undonding will be used to cover the debt. Finally, the time of the previous block is stored in the validator vesting account keeper, which is used to determine when a period has ended. +At each `BeginBlock`, all validator vesting accounts are iterated over to update the status of the current vesting period. Note that the address of each account is retrieved by iterating over the keys in the `validator-vesting` store, while the account objects are stored and accessed using the `auth` module's `AccountKeeper`. For each account, the block count is incremented, the missed sign count is incremented if the validator did not sign the block or was not found in the validator set. By comparing the blocktime of the current `BeginBlock`, with the value of `previousBlockTime` stored in the `validator-vesting` store, it is determined if the end of the current period has been reached. If the current period has ended, the `VestingPeriodProgress` field is updated to reflect if the coins for the ending period successfully vested or not. After updates are made regarding the status of the current vesting period, any outstanding debt on the account is attempted to be collected. If there is enough `SpendableBalance` on the account to cover the debt, coins are sent to the `ReturnAdress` or burned. If there is not enough `SpendableBalance` to cover the debt, all delegations of the account are `Unbonded`. Once those unbonding events reach maturity, the coins freed from the unbonding will be used to cover the debt. Finally, the time of the previous block is stored in the validator vesting account keeper, which is used to determine when a period has ended. ```go func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) { @@ -42,4 +42,3 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) k.SetPreviousBlockTime(ctx, currentBlockTime) } ``` - diff --git a/x/validator-vesting/spec/README.md b/x/validator-vesting/spec/README.md new file mode 100644 index 00000000..186b18ae --- /dev/null +++ b/x/validator-vesting/spec/README.md @@ -0,0 +1,14 @@ +# `validator-vesting` + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[BeginBlock](03_begin_block.md)** + +## Abstract + +`x/validator-vesting` is an implementation of a Cosmos SDK sub-module that defines a new type of vesting account, `ValidatorVestingAccount`. This account implements the Cosmos SDK `VestingAccount` interface and extends it to add conditions to the vesting balance. In this implementation, in order to receive the vesting balance, the validator vesting account specifies a validator that must sign a given `SigningThreshold` of blocks during each vesting period in order for coins to successfully vest. + +## Dependencies + +This module uses the Cosmos SDK `x/auth` module and `x/auth/vesting` sub-module definitions of `Account` and `VestingAccount`. The actual state of a `ValidatorVestingAccount` is stored in the `x/auth` keeper, while this module merely stores a list of addresses that correspond to validator vesting accounts for fast iteration.