Merge branch 'master' into ro-auction-audit-revisions

This commit is contained in:
rhuairahrighairigh 2020-05-12 01:09:32 +01:00
commit a5d70d1424
88 changed files with 2183 additions and 444 deletions

View File

@ -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)

View File

@ -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)))

View File

@ -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
)

View File

@ -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",

View File

@ -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 }

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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 <account-name> <amount> --vesting-amount <amount> --vesting-end-time <unix-timestamp> --vesting-start-time <unix-timestamp> --vesting-periods <path/to/vesting.json>`, 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
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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"

View File

@ -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 {

View File

@ -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:

View File

@ -135,7 +135,6 @@ type (
QueryAssetSupply = types.QueryAssetSupply
QueryAtomicSwapByID = types.QueryAtomicSwapByID
QueryAtomicSwaps = types.QueryAtomicSwaps
SupplyKeeper = types.SupplyKeeper
SwapDirection = types.SwapDirection
SwapStatus = types.SwapStatus
)

View File

@ -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))
}

View File

@ -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)
}
}
}

View File

@ -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()))

View File

@ -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
}

View File

@ -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])

View File

@ -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,

View File

@ -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
}

View File

@ -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"+

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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))

View File

@ -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} |

View File

@ -12,7 +12,7 @@ const (
EventTypeBeginBlockerFatal = "cdp_begin_block_error"
AttributeKeyCdpID = "cdp_id"
AttributeKeyDepositor = "depositor"
AttributeKeyDeposit = "deposit"
AttributeValueCategory = "cdp"
AttributeKeyError = "error_message"
)

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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]))
}
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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{}
}

View File

@ -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]
}

View File

@ -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`.

View File

@ -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.

View File

@ -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

View File

@ -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 |

View File

@ -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`.

View File

@ -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)
}
```

View File

@ -3,17 +3,25 @@
## Table of Contents
<!-- TOC -->
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.

View File

@ -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

View File

@ -10,6 +10,7 @@ const (
AttributeKeyCommitteeID = "committee_id"
AttributeKeyProposalID = "proposal_id"
AttributeKeyProposalCloseStatus = "status"
AttributeKeyVoter = "voter"
AttributeValueProposalPassed = "proposal_passed"
AttributeValueProposalTimeout = "proposal_timeout"
AttributeValueProposalFailed = "proposal_failed"

View File

@ -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

View File

@ -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

View File

@ -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
})
}

View File

@ -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)
}

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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} |

View File

@ -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 |

View File

@ -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)
}
```

View File

@ -0,0 +1,17 @@
# `incentive`
<!-- TOC -->
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.

View File

@ -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"
)

View File

@ -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,
}
}

View File

@ -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

View File

@ -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.

View File

@ -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"`
}
```

View File

@ -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.

View File

@ -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" |

View File

@ -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 |

View File

@ -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)
}
}
```

13
x/kavadist/spec/README.md Normal file
View File

@ -0,0 +1,13 @@
# `kavadist`
<!-- TOC -->
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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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
```

View File

@ -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.

View File

@ -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} |

View File

@ -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 |

View File

@ -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
}
```

View File

@ -0,0 +1,13 @@
# `pricefeed`
<!-- TOC -->
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.

View File

@ -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"
)

View File

@ -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.

View File

@ -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`.

View File

@ -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)
}
```

View File

@ -0,0 +1,14 @@
# `validator-vesting`
<!-- TOC -->
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.