diff --git a/app/app.go b/app/app.go index a21ebd04..f68cc640 100644 --- a/app/app.go +++ b/app/app.go @@ -380,16 +380,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio app.pricefeedKeeper, app.auctionKeeper, ) - app.incentiveKeeper = incentive.NewKeeper( - app.cdc, - keys[incentive.StoreKey], - incentiveSubspace, - app.supplyKeeper, - &cdpKeeper, - &hardKeeper, - app.accountKeeper, - &stakingKeeper, - ) app.issuanceKeeper = issuance.NewKeeper( app.cdc, keys[issuance.StoreKey], @@ -402,6 +392,17 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio keys[swap.StoreKey], swapSubspace, ) + app.incentiveKeeper = incentive.NewKeeper( + app.cdc, + keys[incentive.StoreKey], + incentiveSubspace, + app.supplyKeeper, + &cdpKeeper, + &hardKeeper, + app.accountKeeper, + &stakingKeeper, + app.swapKeeper, + ) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks diff --git a/migrate/cmd.go b/migrate/cmd.go index 136cf241..0facd30e 100644 --- a/migrate/cmd.go +++ b/migrate/cmd.go @@ -3,22 +3,21 @@ package migrate import ( "fmt" - "github.com/kava-labs/kava/migrate/v0_14" - "github.com/spf13/cobra" - "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/server" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/version" - + "github.com/spf13/cobra" tmtypes "github.com/tendermint/tendermint/types" + + "github.com/kava-labs/kava/migrate/v0_15" ) // MigrateGenesisCmd returns a command to execute genesis state migration. func MigrateGenesisCmd(_ *server.Context, cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "migrate [genesis-file]", - Short: "Migrate genesis file from kava v0.11 (or v0.12) to v0.14", + Short: "Migrate genesis file from kava v0.14 to v0.15", Long: "Migrate the source genesis into the current version, sorts it, and print to STDOUT.", Example: fmt.Sprintf(`%s migrate /path/to/genesis.json`, version.ServerName), Args: cobra.ExactArgs(1), @@ -30,7 +29,7 @@ func MigrateGenesisCmd(_ *server.Context, cdc *codec.Codec) *cobra.Command { return fmt.Errorf("failed to read genesis document from file %s: %w", importGenesis, err) } - newGenDoc := v0_14.Migrate(*genDoc) + newGenDoc := v0_15.Migrate(*genDoc) bz, err := cdc.MarshalJSONIndent(newGenDoc, "", " ") if err != nil { diff --git a/migrate/v0_15/migrate.go b/migrate/v0_15/migrate.go index f24c2c70..8a0d5686 100644 --- a/migrate/v0_15/migrate.go +++ b/migrate/v0_15/migrate.go @@ -6,7 +6,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/genutil" - cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" tmtypes "github.com/tendermint/tendermint/types" @@ -18,8 +17,9 @@ import ( ) var ( - // TODO: update GenesisTime for kava-8 launch + // TODO: update GenesisTime and chain-id for kava-8 launch GenesisTime = time.Date(2021, 4, 8, 15, 0, 0, 0, time.UTC) + ChainID = "kava-8" // TODO: update SWP reward per second amount before production SwpRewardsPerSecond = sdk.NewCoin("swp", sdk.OneInt()) ) @@ -32,49 +32,56 @@ func Migrate(genDoc tmtypes.GenesisDoc) tmtypes.GenesisDoc { cryptoAmino.RegisterAmino(cdc) tmtypes.RegisterEvidences(cdc) + // Old codec does not need all old modules registered on it to correctly decode at this stage + // as it only decodes the app state into a map of module names to json encoded bytes. if err := cdc.UnmarshalJSON(genDoc.AppState, &appStateMap); err != nil { panic(err) } - newAppState := MigrateAppState(appStateMap) + + MigrateAppState(appStateMap) + v0_15Codec := app.MakeCodec() - marshaledNewAppState, err := v0_15Codec.MarshalJSON(newAppState) + marshaledNewAppState, err := v0_15Codec.MarshalJSON(appStateMap) if err != nil { panic(err) } genDoc.AppState = marshaledNewAppState genDoc.GenesisTime = GenesisTime - genDoc.ChainID = "kava-8" + genDoc.ChainID = ChainID return genDoc } // MigrateAppState migrates application state from v0.14 format to a kava v0.15 format -func MigrateAppState(v0_14AppState genutil.AppMap) genutil.AppMap { - v0_15AppState := v0_14AppState - cdc := app.MakeCodec() +// It modifies the provided genesis state in place. +func MigrateAppState(v0_14AppState genutil.AppMap) { + v0_14Codec := makeV014Codec() + v0_15Codec := app.MakeCodec() // Migrate incentive app state if v0_14AppState[v0_14incentive.ModuleName] != nil { var incentiveGenState v0_14incentive.GenesisState - cdc.MustUnmarshalJSON(v0_14AppState[v0_15incentive.ModuleName], &incentiveGenState) + v0_14Codec.MustUnmarshalJSON(v0_14AppState[v0_14incentive.ModuleName], &incentiveGenState) delete(v0_14AppState, v0_14incentive.ModuleName) - v0_15AppState[v0_15incentive.ModuleName] = cdc.MustMarshalJSON(Incentive(incentiveGenState)) + v0_14AppState[v0_15incentive.ModuleName] = v0_15Codec.MustMarshalJSON(Incentive(incentiveGenState)) } // Migrate commmittee app state if v0_14AppState[v0_14committee.ModuleName] != nil { // Unmarshal v14 committee genesis state and delete it var committeeGS v0_14committee.GenesisState - cdc := codec.New() - sdk.RegisterCodec(cdc) - v0_14committee.RegisterCodec(cdc) - cdc.MustUnmarshalJSON(v0_14AppState[v0_14committee.ModuleName], &committeeGS) + v0_14Codec.MustUnmarshalJSON(v0_14AppState[v0_14committee.ModuleName], &committeeGS) delete(v0_14AppState, v0_14committee.ModuleName) // Marshal v15 committee genesis state - cdc = app.MakeCodec() - v0_15AppState[v0_15committee.ModuleName] = cdc.MustMarshalJSON(Committee(committeeGS)) + v0_14AppState[v0_15committee.ModuleName] = v0_15Codec.MustMarshalJSON(Committee(committeeGS)) } +} - return v0_15AppState +func makeV014Codec() *codec.Codec { + cdc := codec.New() + sdk.RegisterCodec(cdc) + v0_14committee.RegisterCodec(cdc) + v0_14incentive.RegisterCodec(cdc) + return cdc } // Committee migrates from a v0.14 committee genesis state to a v0.15 committee genesis state @@ -248,6 +255,7 @@ func Incentive(incentiveGS v0_14incentive.GenesisState) v0_15incentive.GenesisSt hardSupplyRewardPeriods, hardBorrowRewardPeriods, hardDelegatorRewardPeriods, + v0_15incentive.DefaultMultiRewardPeriods, // TODO add expected swap reward periods claimMultipliers, incentiveGS.Params.ClaimEnd, ) @@ -337,6 +345,7 @@ func Incentive(incentiveGS v0_14incentive.GenesisState) v0_15incentive.GenesisSt hardSupplyAccumulationTimes, hardBorrowAccumulationTimes, hardDelegatorAccumulationTimes, + v0_15incentive.DefaultGenesisAccumulationTimes, // There is no previous swap rewards to accumulation starts at genesis time. usdxMintingClaims, hardClaims, ) diff --git a/migrate/v0_15/migrate_test.go b/migrate/v0_15/migrate_test.go index 305c9656..a05218b8 100644 --- a/migrate/v0_15/migrate_test.go +++ b/migrate/v0_15/migrate_test.go @@ -9,12 +9,13 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/kava-labs/kava/app" v0_14committee "github.com/kava-labs/kava/x/committee/legacy/v0_14" v0_15committee "github.com/kava-labs/kava/x/committee/types" v0_14incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_14" v0_15incentive "github.com/kava-labs/kava/x/incentive/types" - "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { diff --git a/x/incentive/abci.go b/x/incentive/abci.go index d6aa570d..2ce8a17b 100644 --- a/x/incentive/abci.go +++ b/x/incentive/abci.go @@ -33,4 +33,7 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { panic(err) } } + for _, rp := range params.SwapRewardPeriods { + k.AccumulateSwapRewards(ctx, rp) + } } diff --git a/x/incentive/genesis.go b/x/incentive/genesis.go index 911d1ee7..67086c96 100644 --- a/x/incentive/genesis.go +++ b/x/incentive/genesis.go @@ -74,6 +74,9 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeep for _, gat := range gs.HardDelegatorAccumulationTimes { k.SetPreviousHardDelegatorRewardAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime) } + for _, gat := range gs.SwapAccumulationTimes { + k.SetSwapRewardAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime) + } for i, claim := range gs.USDXMintingClaims { for j, ri := range claim.RewardIndexes { @@ -195,6 +198,16 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState { hardDelegatorGats = append(hardDelegatorGats, gat) } + var swapGats GenesisAccumulationTimes + for _, rp := range params.SwapRewardPeriods { + pat, found := k.GetSwapRewardAccrualTime(ctx, rp.CollateralType) + if !found { + panic(fmt.Sprintf("expected previous swap reward accrual time to be set in state for %s", rp.CollateralType)) + } + gat := types.NewGenesisAccumulationTime(rp.CollateralType, pat) + swapGats = append(swapGats, gat) + } + return types.NewGenesisState(params, usdxMintingGats, hardSupplyGats, - hardBorrowGats, hardDelegatorGats, synchronizedUsdxClaims, synchronizedHardClaims) + hardBorrowGats, hardDelegatorGats, swapGats, synchronizedUsdxClaims, synchronizedHardClaims) } diff --git a/x/incentive/genesis_test.go b/x/incentive/genesis_test.go index 6760f245..ce5bb83a 100644 --- a/x/incentive/genesis_test.go +++ b/x/incentive/genesis_test.go @@ -64,13 +64,16 @@ func (suite *GenesisTestSuite) SetupTest() { incentive.RewardPeriods{incentive.NewRewardPeriod(true, "bnb-a", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), c("ukava", 122354))}, incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "ukava", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, incentive.Multipliers{incentive.NewMultiplier(incentive.Small, 1, d("0.25")), incentive.NewMultiplier(incentive.Large, 12, d("1.0"))}, + incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "ukava", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, + incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "btcb/usdx", suite.genesisTime.Add(-1*oneYear), suite.genesisTime.Add(oneYear), cs(c("hard", 122354)))}, + incentive.Multipliers{incentive.NewMultiplier(incentive.Small, 1, d("0.25")), incentive.NewMultiplier(incentive.Large, 12, d("1.0"))}, suite.genesisTime.Add(5*oneYear), ), incentive.DefaultGenesisAccumulationTimes, incentive.DefaultGenesisAccumulationTimes, incentive.DefaultGenesisAccumulationTimes, incentive.DefaultGenesisAccumulationTimes, + incentive.DefaultGenesisAccumulationTimes, incentive.DefaultUSDXClaims, incentive.DefaultHardClaims, ) diff --git a/x/incentive/handler_test.go b/x/incentive/handler_test.go index 1f559c29..e6e7ae45 100644 --- a/x/incentive/handler_test.go +++ b/x/incentive/handler_test.go @@ -45,9 +45,10 @@ func (suite *HandlerTestSuite) SetupTest() { incentiveGS := incentive.NewGenesisState( incentive.NewParams( incentive.RewardPeriods{incentive.NewRewardPeriod(true, "bnb-a", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), c("ukava", 122354))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb-a", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb-a", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, - incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb-a", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, + incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, + incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "bnb", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, + incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "ukava", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, + incentive.MultiRewardPeriods{incentive.NewMultiRewardPeriod(true, "btcb/usdx", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), cs(c("ukava", 122354)))}, incentive.Multipliers{incentive.NewMultiplier(incentive.MultiplierName("small"), 1, d("0.25")), incentive.NewMultiplier(incentive.MultiplierName("large"), 12, d("1.0"))}, time.Date(2025, 12, 15, 14, 0, 0, 0, time.UTC), ), @@ -55,6 +56,7 @@ func (suite *HandlerTestSuite) SetupTest() { incentive.DefaultGenesisAccumulationTimes, incentive.DefaultGenesisAccumulationTimes, incentive.DefaultGenesisAccumulationTimes, + incentive.DefaultGenesisAccumulationTimes, incentive.DefaultUSDXClaims, incentive.DefaultHardClaims, ) diff --git a/x/incentive/integration_test.go b/x/incentive/integration_test.go index ebd998c3..4bdb9302 100644 --- a/x/incentive/integration_test.go +++ b/x/incentive/integration_test.go @@ -7,8 +7,6 @@ import ( "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/cdp" - "github.com/kava-labs/kava/x/incentive" - "github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/pricefeed" ) @@ -141,38 +139,39 @@ func NewPricefeedGenStateMulti() app.GenesisState { return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)} } -func NewIncentiveGenState(previousAccumTime, endTime time.Time, rewardPeriods ...incentive.RewardPeriod) app.GenesisState { - var accumulationTimes incentive.GenesisAccumulationTimes - for _, rp := range rewardPeriods { - accumulationTimes = append( - accumulationTimes, - incentive.NewGenesisAccumulationTime( - rp.CollateralType, - previousAccumTime, - ), - ) - } - genesis := incentive.NewGenesisState( - incentive.NewParams( - rewardPeriods, - types.MultiRewardPeriods{}, - types.MultiRewardPeriods{}, - types.MultiRewardPeriods{}, - incentive.Multipliers{ - incentive.NewMultiplier(incentive.Small, 1, d("0.25")), - incentive.NewMultiplier(incentive.Large, 12, d("1.0")), - }, - endTime, - ), - accumulationTimes, - accumulationTimes, - accumulationTimes, - incentive.DefaultGenesisAccumulationTimes, - incentive.DefaultUSDXClaims, - incentive.DefaultHardClaims, - ) - return app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(genesis)} -} +// func NewIncentiveGenState(previousAccumTime, endTime time.Time, rewardPeriods ...incentive.RewardPeriod) app.GenesisState { +// var accumulationTimes incentive.GenesisAccumulationTimes +// for _, rp := range rewardPeriods { +// accumulationTimes = append( +// accumulationTimes, +// incentive.NewGenesisAccumulationTime( +// rp.CollateralType, +// previousAccumTime, +// ), +// ) +// } +// genesis := incentive.NewGenesisState( +// incentive.NewParams( +// rewardPeriods, +// types.MultiRewardPeriods{}, +// types.MultiRewardPeriods{}, +// types.MultiRewardPeriods{}, +// types.MultiRewardPeriods{}, +// incentive.Multipliers{ +// incentive.NewMultiplier(incentive.Small, 1, d("0.25")), +// incentive.NewMultiplier(incentive.Large, 12, d("1.0")), +// }, +// endTime, +// ), +// accumulationTimes, +// accumulationTimes, +// accumulationTimes, +// incentive.DefaultGenesisAccumulationTimes, +// incentive.DefaultUSDXClaims, +// incentive.DefaultHardClaims, +// ) +// return app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(genesis)} +// } func NewCDPGenStateHighInterest() app.GenesisState { cdpGenesis := cdp.GenesisState{ diff --git a/x/incentive/keeper/keeper.go b/x/incentive/keeper/keeper.go index a281ea7a..7be70d51 100644 --- a/x/incentive/keeper/keeper.go +++ b/x/incentive/keeper/keeper.go @@ -14,18 +14,20 @@ import ( type Keeper struct { accountKeeper types.AccountKeeper cdc *codec.Codec - cdpKeeper types.CdpKeeper - hardKeeper types.HardKeeper key sdk.StoreKey paramSubspace types.ParamSubspace supplyKeeper types.SupplyKeeper + cdpKeeper types.CdpKeeper + hardKeeper types.HardKeeper stakingKeeper types.StakingKeeper + swapKeeper types.SwapKeeper } // NewKeeper creates a new keeper func NewKeeper( cdc *codec.Codec, key sdk.StoreKey, paramstore types.ParamSubspace, sk types.SupplyKeeper, cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, stk types.StakingKeeper, + swpk types.SwapKeeper, ) Keeper { if !paramstore.HasKeyTable() { @@ -35,12 +37,13 @@ func NewKeeper( return Keeper{ accountKeeper: ak, cdc: cdc, - cdpKeeper: cdpk, - hardKeeper: hk, key: key, paramSubspace: paramstore, supplyKeeper: sk, + cdpKeeper: cdpk, + hardKeeper: hk, stakingKeeper: stk, + swapKeeper: swpk, } } @@ -356,3 +359,39 @@ func (k Keeper) SetPreviousHardDelegatorRewardAccrualTime(ctx sdk.Context, denom store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousHardDelegatorRewardAccrualTimeKeyPrefix) store.Set([]byte(denom), k.cdc.MustMarshalBinaryBare(blockTime)) } + +// SetSwapRewardIndexes stores the global reward indexes that track total rewards to a swap pool. +func (k Keeper) SetSwapRewardIndexes(ctx sdk.Context, poolID string, indexes types.RewardIndexes) { + store := prefix.NewStore(ctx.KVStore(k.key), types.SwapRewardIndexesKeyPrefix) + bz := k.cdc.MustMarshalBinaryBare(indexes) + store.Set([]byte(poolID), bz) +} + +// GetSwapRewardIndexes fetches the global reward indexes that track total rewards to a swap pool. +func (k Keeper) GetSwapRewardIndexes(ctx sdk.Context, poolID string) (types.RewardIndexes, bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.SwapRewardIndexesKeyPrefix) + bz := store.Get([]byte(poolID)) + if bz == nil { + return types.RewardIndexes{}, false + } + var rewardIndexes types.RewardIndexes + k.cdc.MustUnmarshalBinaryBare(bz, &rewardIndexes) + return rewardIndexes, true +} + +// GetSwapRewardAccrualTime fetches the last time rewards were accrued for a swap pool. +func (k Keeper) GetSwapRewardAccrualTime(ctx sdk.Context, poolID string) (blockTime time.Time, found bool) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousSwapRewardAccrualTimeKeyPrefix) + bz := store.Get([]byte(poolID)) + if bz == nil { + return time.Time{}, false + } + k.cdc.MustUnmarshalBinaryBare(bz, &blockTime) + return blockTime, true +} + +// SetSwapRewardAccrualTime stores the last time rewards were accrued for a swap pool. +func (k Keeper) SetSwapRewardAccrualTime(ctx sdk.Context, poolID string, blockTime time.Time) { + store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousSwapRewardAccrualTimeKeyPrefix) + store.Set([]byte(poolID), k.cdc.MustMarshalBinaryBare(blockTime)) +} diff --git a/x/incentive/keeper/keeper_test.go b/x/incentive/keeper/keeper_test.go index 8628b14e..df3f3118 100644 --- a/x/incentive/keeper/keeper_test.go +++ b/x/incentive/keeper/keeper_test.go @@ -82,6 +82,120 @@ func (suite *KeeperTestSuite) TestIterateUSDXMintingClaims() { suite.Require().Equal(len(suite.addrs), len(claims)) } +func (suite *KeeperTestSuite) TestGetSetSwapRewardIndexes() { + testCases := []struct { + name string + poolName string + indexes types.RewardIndexes + panics bool + }{ + { + name: "two factors can be written and read", + poolName: "btc/usdx", + indexes: types.RewardIndexes{ + { + CollateralType: "hard", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + name: "indexes with empty pool name can be written and read", + poolName: "", + indexes: types.RewardIndexes{ + { + CollateralType: "hard", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + { + // this test is to detect any changes in behavior, it would be nice if Set didn't panic + name: "setting empty indexes panics", + poolName: "btc/usdx", + indexes: types.RewardIndexes{}, + panics: true, + }, + { + // this test is to detect any changes in behavior, it would be nice if Set didn't panic + name: "setting nil indexes panics", + poolName: "btc/usdx", + indexes: nil, + panics: true, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupApp() + + _, found := suite.keeper.GetSwapRewardIndexes(suite.ctx, tc.poolName) + suite.False(found) + + setFunc := func() { suite.keeper.SetSwapRewardIndexes(suite.ctx, tc.poolName, tc.indexes) } + if tc.panics { + suite.Panics(setFunc) + return + } else { + suite.NotPanics(setFunc) + } + + storedIndexes, found := suite.keeper.GetSwapRewardIndexes(suite.ctx, tc.poolName) + suite.True(found) + suite.Equal(tc.indexes, storedIndexes) + }) + } +} + +func (suite *KeeperTestSuite) TestGetSetSwapRewardAccrualTimes() { + testCases := []struct { + name string + poolName string + accrualTime time.Time + panics bool + }{ + { + name: "normal time can be written and read", + poolName: "btc/usdx", + accrualTime: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "zero time can be written and read", + poolName: "btc/usdx", + accrualTime: time.Time{}, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupApp() + + _, found := suite.keeper.GetSwapRewardAccrualTime(suite.ctx, tc.poolName) + suite.False(found) + + setFunc := func() { suite.keeper.SetSwapRewardAccrualTime(suite.ctx, tc.poolName, tc.accrualTime) } + if tc.panics { + suite.Panics(setFunc) + return + } else { + suite.NotPanics(setFunc) + } + + storedTime, found := suite.keeper.GetSwapRewardAccrualTime(suite.ctx, tc.poolName) + suite.True(found) + suite.Equal(tc.accrualTime, storedTime) + }) + } +} + func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) } diff --git a/x/incentive/keeper/rewards_delegator.go b/x/incentive/keeper/rewards_delegator.go index 53604e26..beaf6cc1 100644 --- a/x/incentive/keeper/rewards_delegator.go +++ b/x/incentive/keeper/rewards_delegator.go @@ -106,9 +106,9 @@ func (k Keeper) SynchronizeHardDelegatorRewards(ctx sdk.Context, delegator sdk.A userRewardIndexes, found := claim.DelegatorRewardIndexes.Get(types.BondDenom) if !found { - // Normally the factor should always be found, as it is added in InitializeHardDelegatorReward when a user delegates. + // Normally the reward indexes should always be found. // However if there were no delegator rewards (ie no reward period in params) then a reward period is added, existing claims will not have the factor. - // So assume the factor is the starting value for any global factor: 0. + // So given the reward period was just added, assume the starting value for any global reward indexes, which is an empty slice. userRewardIndexes = types.RewardIndexes{} } diff --git a/x/incentive/keeper/rewards_delegator_init_test.go b/x/incentive/keeper/rewards_delegator_init_test.go index 6fece8a4..255adadd 100644 --- a/x/incentive/keeper/rewards_delegator_init_test.go +++ b/x/incentive/keeper/rewards_delegator_init_test.go @@ -27,15 +27,9 @@ func TestInitializeHardDelegatorReward(t *testing.T) { suite.Run(t, new(InitializeHardDelegatorRewardTests)) } -// Hardcoded to use bond denom -func (suite *InitializeHardDelegatorRewardTests) storeGlobalDelegatorFactor(multiRewardIndexes types.MultiRewardIndexes) { - multiRewardIndex, _ := multiRewardIndexes.GetRewardIndex(types.BondDenom) - suite.keeper.SetHardDelegatorRewardIndexes(suite.ctx, types.BondDenom, multiRewardIndex.RewardIndexes) -} - func (suite *InitializeHardDelegatorRewardTests) TestClaimIndexesAreSetWhenClaimDoesNotExist() { globalIndex := arbitraryDelegatorRewardIndexes - suite.storeGlobalDelegatorFactor(globalIndex) + suite.storeGlobalDelegatorIndexes(globalIndex) delegator := arbitraryAddress() suite.keeper.InitializeHardDelegatorReward(suite.ctx, delegator) @@ -59,7 +53,7 @@ func (suite *InitializeHardDelegatorRewardTests) TestClaimIsSyncedAndIndexesAreS DelegatorShares: d("1000"), }}, } - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, sk) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, sk, nil) claim := types.HardLiquidityProviderClaim{ BaseMultiClaim: types.BaseMultiClaim{ @@ -77,7 +71,7 @@ func (suite *InitializeHardDelegatorRewardTests) TestClaimIsSyncedAndIndexesAreS // Update the claim object with the new global factor bondIndex, _ := claim.DelegatorRewardIndexes.GetRewardIndexIndex(types.BondDenom) claim.DelegatorRewardIndexes[bondIndex].RewardIndexes = globalIndexes - suite.storeGlobalDelegatorFactor(claim.DelegatorRewardIndexes) + suite.storeGlobalDelegatorIndexes(claim.DelegatorRewardIndexes) suite.keeper.InitializeHardDelegatorReward(suite.ctx, claim.Owner) @@ -86,7 +80,7 @@ func (suite *InitializeHardDelegatorRewardTests) TestClaimIsSyncedAndIndexesAreS suite.Truef(syncedClaim.Reward.IsAllGT(claim.Reward), "'%s' not greater than '%s'", syncedClaim.Reward, claim.Reward) } -// arbitraryDelegatorRewardIndexes contains only one reward index as there is only every one bond denom +// arbitraryDelegatorRewardIndexes contains only one reward index as there is only ever one bond denom var arbitraryDelegatorRewardIndexes = types.MultiRewardIndexes{ types.NewMultiRewardIndex( types.BondDenom, diff --git a/x/incentive/keeper/rewards_delegator_sync_test.go b/x/incentive/keeper/rewards_delegator_sync_test.go index 5ff43d62..9ab0b4a4 100644 --- a/x/incentive/keeper/rewards_delegator_sync_test.go +++ b/x/incentive/keeper/rewards_delegator_sync_test.go @@ -28,16 +28,11 @@ func TestSynchronizeHardDelegatorReward(t *testing.T) { suite.Run(t, new(SynchronizeHardDelegatorRewardTests)) } -func (suite *SynchronizeHardDelegatorRewardTests) storeGlobalDelegatorFactor(multiRewardIndexes types.MultiRewardIndexes) { - multiRewardIndex, _ := multiRewardIndexes.GetRewardIndex(types.BondDenom) - suite.keeper.SetHardDelegatorRewardIndexes(suite.ctx, types.BondDenom, multiRewardIndex.RewardIndexes) -} - func (suite *SynchronizeHardDelegatorRewardTests) TestClaimIndexesAreUnchangedWhenGlobalFactorUnchanged() { delegator := arbitraryAddress() stakingKeeper := fakeStakingKeeper{} // use an empty staking keeper that returns no delegations - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil) claim := types.HardLiquidityProviderClaim{ BaseMultiClaim: types.BaseMultiClaim{ @@ -47,7 +42,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestClaimIndexesAreUnchangedWh } suite.storeClaim(claim) - suite.storeGlobalDelegatorFactor(claim.DelegatorRewardIndexes) + suite.storeGlobalDelegatorIndexes(claim.DelegatorRewardIndexes) suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false) @@ -58,7 +53,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestClaimIndexesAreUnchangedWh func (suite *SynchronizeHardDelegatorRewardTests) TestClaimIndexesAreUpdatedWhenGlobalFactorIncreased() { delegator := arbitraryAddress() - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, fakeStakingKeeper{}) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, fakeStakingKeeper{}, nil) claim := types.HardLiquidityProviderClaim{ BaseMultiClaim: types.BaseMultiClaim{ @@ -74,7 +69,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestClaimIndexesAreUpdatedWhen // Update the claim object with the new global factor bondIndex, _ := claim.DelegatorRewardIndexes.GetRewardIndexIndex(types.BondDenom) claim.DelegatorRewardIndexes[bondIndex].RewardIndexes = globalIndexes - suite.storeGlobalDelegatorFactor(claim.DelegatorRewardIndexes) + suite.storeGlobalDelegatorIndexes(claim.DelegatorRewardIndexes) suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false) @@ -97,7 +92,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsUnchangedWhenGloba unslashedBondedValidator(validatorAddress), }, } - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil) claim := types.HardLiquidityProviderClaim{ BaseMultiClaim: types.BaseMultiClaim{ @@ -118,7 +113,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsUnchangedWhenGloba } suite.storeClaim(claim) - suite.storeGlobalDelegatorFactor(claim.DelegatorRewardIndexes) + suite.storeGlobalDelegatorIndexes(claim.DelegatorRewardIndexes) suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false) @@ -142,7 +137,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsIncreasedWhenNewRe unslashedBondedValidator(validatorAddress), }, } - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil) claim := types.HardLiquidityProviderClaim{ BaseMultiClaim: types.BaseMultiClaim{ @@ -164,7 +159,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsIncreasedWhenNewRe }, }, }} - suite.storeGlobalDelegatorFactor(newGlobalIndexes) + suite.storeGlobalDelegatorIndexes(newGlobalIndexes) suite.keeper.SynchronizeHardDelegatorRewards(suite.ctx, claim.Owner, nil, false) @@ -195,7 +190,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsIncreasedWhenGloba unslashedBondedValidator(validatorAddress), }, } - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil) claim := types.HardLiquidityProviderClaim{ BaseMultiClaim: types.BaseMultiClaim{ @@ -216,7 +211,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestRewardIsIncreasedWhenGloba } suite.storeClaim(claim) - suite.storeGlobalDelegatorFactor( + suite.storeGlobalDelegatorIndexes( types.MultiRewardIndexes{ types.NewMultiRewardIndex( types.BondDenom, @@ -304,7 +299,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestGetDelegatedWhenValAddrIsN unslashedNotBondedValidator(validatorAddresses[3]), }, } - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil) suite.Equal( d("11"), // delegation to bonded validators @@ -347,7 +342,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestGetDelegatedWhenExcludingA unslashedNotBondedValidator(validatorAddresses[3]), }, } - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil) suite.Equal( d("10"), @@ -390,7 +385,7 @@ func (suite *SynchronizeHardDelegatorRewardTests) TestGetDelegatedWhenIncludingA unslashedNotBondedValidator(validatorAddresses[3]), }, } - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, stakingKeeper, nil) suite.Equal( d("111"), diff --git a/x/incentive/keeper/rewards_swap.go b/x/incentive/keeper/rewards_swap.go new file mode 100644 index 00000000..e5495169 --- /dev/null +++ b/x/incentive/keeper/rewards_swap.go @@ -0,0 +1,37 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/incentive/types" +) + +// AccumulateSwapRewards calculates new rewards to distribute this block and updates the global indexes to reflect this. +// The provided rewardPeriod must be valid to avoid panics in calculating time durations. +func (k Keeper) AccumulateSwapRewards(ctx sdk.Context, rewardPeriod types.MultiRewardPeriod) { + + previousAccrualTime, found := k.GetSwapRewardAccrualTime(ctx, rewardPeriod.CollateralType) + if !found { + previousAccrualTime = ctx.BlockTime() + } + + indexes, found := k.GetSwapRewardIndexes(ctx, rewardPeriod.CollateralType) + if !found { + indexes = types.RewardIndexes{} + } + + acc := types.NewAccumulator(previousAccrualTime, indexes) + + totalShares, found := k.swapKeeper.GetPoolShares(ctx, rewardPeriod.CollateralType) + if !found { + totalShares = sdk.ZeroDec() + } + + acc.Accumulate(rewardPeriod, totalShares, ctx.BlockTime()) + + k.SetSwapRewardAccrualTime(ctx, rewardPeriod.CollateralType, acc.PreviousAccumulationTime) + if len(acc.Indexes) > 0 { + // the store panics when setting empty or nil indexes + k.SetSwapRewardIndexes(ctx, rewardPeriod.CollateralType, acc.Indexes) + } +} diff --git a/x/incentive/keeper/rewards_swap_test.go b/x/incentive/keeper/rewards_swap_test.go new file mode 100644 index 00000000..f2c52595 --- /dev/null +++ b/x/incentive/keeper/rewards_swap_test.go @@ -0,0 +1,263 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/suite" + + "github.com/kava-labs/kava/x/incentive/types" +) + +type AccumulateSwapRewardsTests struct { + unitTester +} + +func (suite *AccumulateSwapRewardsTests) checkStoredTimeEquals(poolID string, expected time.Time) { + storedTime, found := suite.keeper.GetSwapRewardAccrualTime(suite.ctx, poolID) + suite.True(found) + suite.Equal(expected, storedTime) +} + +func (suite *AccumulateSwapRewardsTests) checkStoredIndexesEqual(poolID string, expected types.RewardIndexes) { + storedIndexes, found := suite.keeper.GetSwapRewardIndexes(suite.ctx, poolID) + suite.True(found) + suite.Equal(expected, storedIndexes) +} + +func TestAccumulateSwapRewards(t *testing.T) { + suite.Run(t, new(AccumulateSwapRewardsTests)) +} + +func (suite *AccumulateSwapRewardsTests) TestStateUpdatedWhenBlockTimeHasIncreased() { + swapKeeper := &fakeSwapKeeper{d("1000000")} + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper) + + pool := "btc/usdx" + suite.storeGlobalSwapIndexes(types.MultiRewardIndexes{ + { + CollateralType: pool, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + }) + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.keeper.SetSwapRewardAccrualTime(suite.ctx, pool, previousAccrualTime) + + newAccrualTime := previousAccrualTime.Add(1 * time.Hour) + suite.ctx = suite.ctx.WithBlockTime(newAccrualTime) + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), // same denoms as in global indexes + ) + + suite.keeper.AccumulateSwapRewards(suite.ctx, period) + + // check time and factors + + suite.checkStoredTimeEquals(pool, newAccrualTime) + + expectedIndexes := types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("7.22"), + }, + { + CollateralType: "ukava", + RewardFactor: d("3.64"), + }, + } + suite.checkStoredIndexesEqual(pool, expectedIndexes) +} + +func (suite *AccumulateSwapRewardsTests) TestLimitsOfAccumulationPrecision() { + swapKeeper := &fakeSwapKeeper{d("100000000000000000")} // approximate shares in a $1B pool of 10^8 precision assets + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper) + + pool := "btc/usdx" + suite.storeGlobalSwapIndexes(types.MultiRewardIndexes{ + { + CollateralType: pool, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.0"), + }, + }, + }, + }) + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.keeper.SetSwapRewardAccrualTime(suite.ctx, pool, previousAccrualTime) + + newAccrualTime := previousAccrualTime.Add(1 * time.Second) // 1 second is the smallest increment accrual happens over + suite.ctx = suite.ctx.WithBlockTime(newAccrualTime) + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), + distantFuture, + cs(c("swap", 1)), // single unit of any denom is the smallest reward amount + ) + + suite.keeper.AccumulateSwapRewards(suite.ctx, period) + + // check time and factors + + suite.checkStoredTimeEquals(pool, newAccrualTime) + + expectedIndexes := types.RewardIndexes{ + { + CollateralType: "swap", + // smallest reward amount over smallest accumulation duration does not go past 10^-18 decimal precision + RewardFactor: d("0.000000000000000010"), + }, + } + suite.checkStoredIndexesEqual(pool, expectedIndexes) +} + +func (suite *AccumulateSwapRewardsTests) TestStateUnchangedWhenBlockTimeHasNotIncreased() { + swapKeeper := &fakeSwapKeeper{d("1000000")} + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper) + + pool := "btc/usdx" + suite.storeGlobalSwapIndexes(types.MultiRewardIndexes{ + { + CollateralType: pool, + RewardIndexes: types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + }, + }, + }) + previousAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.keeper.SetSwapRewardAccrualTime(suite.ctx, pool, previousAccrualTime) + + suite.ctx = suite.ctx.WithBlockTime(previousAccrualTime) + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), // same denoms as in global indexes + ) + + suite.keeper.AccumulateSwapRewards(suite.ctx, period) + + // check time and factors + + suite.checkStoredTimeEquals(pool, previousAccrualTime) + + expectedIndexes := types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.04"), + }, + } + suite.checkStoredIndexesEqual(pool, expectedIndexes) +} + +func (suite *AccumulateSwapRewardsTests) TestStateAddedWhenStateDoesNotExist() { + swapKeeper := &fakeSwapKeeper{d("1000000")} + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper) + + pool := "btc/usdx" + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(c("swap", 2000), c("ukava", 1000)), + ) + + firstAccrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.ctx = suite.ctx.WithBlockTime(firstAccrualTime) + + suite.keeper.AccumulateSwapRewards(suite.ctx, period) + + // After the first accumulation only the current block time should be stored. + // This indexes will be zero as no time has passed since the previous block because it didn't exist. + suite.checkStoredTimeEquals(pool, firstAccrualTime) + + secondAccrualTime := firstAccrualTime.Add(10 * time.Second) + suite.ctx = suite.ctx.WithBlockTime(secondAccrualTime) + + suite.keeper.AccumulateSwapRewards(suite.ctx, period) + + // After the second accumulation both current block time and indexes should be stored. + suite.checkStoredTimeEquals(pool, secondAccrualTime) + + expectedIndexes := types.RewardIndexes{ + { + CollateralType: "swap", + RewardFactor: d("0.02"), + }, + { + CollateralType: "ukava", + RewardFactor: d("0.01"), + }, + } + suite.checkStoredIndexesEqual(pool, expectedIndexes) +} +func (suite *AccumulateSwapRewardsTests) TestNoPanicWhenStateDoesNotExist() { + swapKeeper := &fakeSwapKeeper{d("0")} + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, swapKeeper) + + pool := "btc/usdx" + + period := types.NewMultiRewardPeriod( + true, + pool, + time.Unix(0, 0), // ensure the test is within start and end times + distantFuture, + cs(), + ) + + accrualTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC) + suite.ctx = suite.ctx.WithBlockTime(accrualTime) + + // Accumulate with no swap shares and no rewards per second will result in no increment to the indexes. + // No increment and no previous indexes stored, results in an updated of nil. Setting this in the state panics. + // Check there is no panic. + suite.NotPanics(func() { + suite.keeper.AccumulateSwapRewards(suite.ctx, period) + }) + + suite.checkStoredTimeEquals(pool, accrualTime) +} + +type fakeSwapKeeper struct { + poolShares sdk.Dec +} + +func (k fakeSwapKeeper) GetPoolShares(ctx sdk.Context, poolID string) (sdk.Dec, bool) { + return k.poolShares, true +} + +// note: amino panics when encoding times ≥ the start of year 10000. +var distantFuture = time.Date(9000, 1, 1, 0, 0, 0, 0, time.UTC) diff --git a/x/incentive/keeper/rewards_usdx_unit_test.go b/x/incentive/keeper/rewards_usdx_unit_test.go index 4350e08d..9840a5fa 100644 --- a/x/incentive/keeper/rewards_usdx_unit_test.go +++ b/x/incentive/keeper/rewards_usdx_unit_test.go @@ -36,7 +36,7 @@ func (suite *InitializeUSDXMintingClaimTests) TestClaimIndexIsSetWhenClaimDoesNo collateralType := "bnb-a" subspace := paramsWithSingleUSDXRewardPeriod(collateralType) - suite.keeper = suite.NewKeeper(subspace, nil, nil, nil, nil, nil) + suite.keeper = suite.NewKeeper(subspace, nil, nil, nil, nil, nil, nil) cdp := NewCDPBuilder(arbitraryAddress(), collateralType).Build() @@ -57,7 +57,7 @@ func (suite *InitializeUSDXMintingClaimTests) TestClaimIndexIsSetWhenClaimExists collateralType := "bnb-a" subspace := paramsWithSingleUSDXRewardPeriod(collateralType) - suite.keeper = suite.NewKeeper(subspace, nil, nil, nil, nil, nil) + suite.keeper = suite.NewKeeper(subspace, nil, nil, nil, nil, nil, nil) claim := types.USDXMintingClaim{ BaseClaim: types.BaseClaim{ diff --git a/x/incentive/keeper/unit_test.go b/x/incentive/keeper/unit_test.go index 8fad7091..2fcd8828 100644 --- a/x/incentive/keeper/unit_test.go +++ b/x/incentive/keeper/unit_test.go @@ -51,7 +51,7 @@ func (suite *unitTester) SetupSuite() { func (suite *unitTester) SetupTest() { suite.ctx = NewTestContext(suite.incentiveStoreKey) - suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil) + suite.keeper = suite.NewKeeper(&fakeParamSubspace{}, nil, nil, nil, nil, nil, nil) } func (suite *unitTester) TearDownTest() { @@ -59,8 +59,8 @@ func (suite *unitTester) TearDownTest() { suite.ctx = sdk.Context{} } -func (suite *unitTester) NewKeeper(paramSubspace types.ParamSubspace, sk types.SupplyKeeper, cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, stk types.StakingKeeper) keeper.Keeper { - return keeper.NewKeeper(suite.cdc, suite.incentiveStoreKey, paramSubspace, sk, cdpk, hk, ak, stk) +func (suite *unitTester) NewKeeper(paramSubspace types.ParamSubspace, sk types.SupplyKeeper, cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, stk types.StakingKeeper, swk types.SwapKeeper) keeper.Keeper { + return keeper.NewKeeper(suite.cdc, suite.incentiveStoreKey, paramSubspace, sk, cdpk, hk, ak, stk, swk) } func (suite *unitTester) storeGlobalBorrowIndexes(indexes types.MultiRewardIndexes) { @@ -73,6 +73,16 @@ func (suite *unitTester) storeGlobalSupplyIndexes(indexes types.MultiRewardIndex suite.keeper.SetHardSupplyRewardIndexes(suite.ctx, i.CollateralType, i.RewardIndexes) } } +func (suite *unitTester) storeGlobalDelegatorIndexes(multiRewardIndexes types.MultiRewardIndexes) { + // Hardcoded to use bond denom + multiRewardIndex, _ := multiRewardIndexes.GetRewardIndex(types.BondDenom) + suite.keeper.SetHardDelegatorRewardIndexes(suite.ctx, types.BondDenom, multiRewardIndex.RewardIndexes) +} +func (suite *unitTester) storeGlobalSwapIndexes(indexes types.MultiRewardIndexes) { + for _, i := range indexes { + suite.keeper.SetSwapRewardIndexes(suite.ctx, i.CollateralType, i.RewardIndexes) + } +} func (suite *unitTester) storeClaim(claim types.HardLiquidityProviderClaim) { suite.keeper.SetHardLiquidityProviderClaim(suite.ctx, claim) @@ -89,7 +99,7 @@ func (subspace *fakeParamSubspace) SetParamSet(_ sdk.Context, ps params.ParamSet subspace.params = *(ps.(*types.Params)) } func (subspace *fakeParamSubspace) HasKeyTable() bool { - // return true so the keeper does no try to set the key table, which does nothing + // return true so the keeper does not try to call WithKeyTable, which does nothing return true } func (subspace *fakeParamSubspace) WithKeyTable(params.KeyTable) params.Subspace { diff --git a/x/incentive/legacy/v0_14/types.go b/x/incentive/legacy/v0_14/types.go index 06219e28..0c015789 100644 --- a/x/incentive/legacy/v0_14/types.go +++ b/x/incentive/legacy/v0_14/types.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/params" @@ -47,6 +48,13 @@ var ( IncentiveMacc = kavadistTypes.ModuleName ) +// RegisterCodec registers the necessary types for incentive module +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterInterface((*Claim)(nil), nil) + cdc.RegisterConcrete(USDXMintingClaim{}, "incentive/USDXMintingClaim", nil) + cdc.RegisterConcrete(HardLiquidityProviderClaim{}, "incentive/HardLiquidityProviderClaim", nil) +} + // GenesisState is the state that must be provided at genesis. type GenesisState struct { Params Params `json:"params" yaml:"params"` diff --git a/x/incentive/spec/01_concepts.md b/x/incentive/spec/01_concepts.md index 24eaefe4..e727630c 100644 --- a/x/incentive/spec/01_concepts.md +++ b/x/incentive/spec/01_concepts.md @@ -6,6 +6,22 @@ order: 1 This module presents an implementation of user incentives that are controlled by governance. When users take a certain action, for example 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) +## General Reward Distribution + +Rewards target various user activity. For example, usdx borrowed from bnb CDPs, btcb supplied to the hard money market, or owned shares in a swap kava/usdx pool. + +Each second, the rewards accumulate at a rate set in the params, eg 100 ukava per second. These are then distributed to all users ratably based on their percentage involvement in the rewarded activity. For example if a user holds 1% of all funds deposited to the kava/usdx swap pool. They will receive 1% of the total rewards each second. + +The number tracking a user's involvement is referred to as "reward source" in the code. And the total across all users the "reward source total". + +## Efficiency + +Paying out rewards to every user every block would be slow and lead to long block times. Instead rewards are calculated much less frequently. + +Every block a global tracker adds up total rewards paid out per unit of user involvement. For example, per unit of xrpb supplied to hard, or per share in a kava/usdx swap pool. A user's specific reward can then be calculated as needed based on their current deposit/shares/borrow. + +User's rewards must be updated whenever their reward source changes. This happens through hooks into other modules that run before deposits/borrows/supplies etc. + ## HARD Token distribution The incentive module also distributes the HARD token on the Kava blockchain. HARD tokens are distributed to two types of ecosystem participants: @@ -13,13 +29,12 @@ The incentive module also distributes the HARD token on the Kava blockchain. HAR 1. Kava stakers - any address that stakes (delegates) KAVA tokens will be eligible to claim HARD tokens. For each delegator, HARD tokens are accumulated ratably based on the total number of kava tokens staked. For example, if a user stakes 1 million KAVA tokens and there are 100 million staked KAVA, that user will accumulate 1% of HARD tokens earmarked for stakers during the distribution period. Distribution periods are defined by a start date, an end date, and a number of HARD tokens that are distributed per second. 2. Depositors/Borrows - any address that deposits and/or borrows eligible tokens to the hard module will be eligible to claim HARD tokens. For each depositor, HARD tokens are accumulated ratably based on the total number of tokens staked of that denomination. For example, if a user deposits 1 million "xyz" tokens and there are 100 million xyz deposited, that user will accumulate 1% of HARD tokens earmarked for depositors of that denomination during the distribution period. Distribution periods are defined by a start date, an end date, and a number of HARD tokens that are distributed per second. -Users are not air-dropped tokens, rather they accumulate `Claim` objects that they may submit a transaction in order to claim. In order to better align long term incentives, when users claim HARD tokens, they have three options, called 'multipliers', for how tokens are distributed. +Users are not air-dropped tokens, rather they accumulate `Claim` objects that they may submit a transaction in order to claim. In order to better align long term incentives, when users claim HARD tokens, they have options, called 'multipliers', for how tokens are distributed. The exact multipliers will be voted by governance and can be changed via a governance vote. An example multiplier schedule would be: -- Liquid - 10% multiplier and no lock up. Users receive 10% as many tokens as users who choose long-term locked tokens. -- Medium-term locked - 33% multiplier and 6 month transfer restriction. Users receive 33% as many tokens as users who choose long-term locked tokens. -- Long-term locked - 100% multiplier and 2 year transfer restriction. Users receive 10x as many tokens as users who choose liquid tokens and 3x as many tokens as users who choose medium-term locked tokens. +- Short-term locked - 20% multiplier and 1 month transfer restriction. Users receive 20% as many tokens as users who choose long-term locked tokens. +- Long-term locked - 100% multiplier and 1 year transfer restriction. Users receive 5x as many tokens as users who choose short-term locked tokens. ## USDX Minting Rewards diff --git a/x/incentive/spec/README.md b/x/incentive/spec/README.md index 5a3e1691..d8f2eeba 100644 --- a/x/incentive/spec/README.md +++ b/x/incentive/spec/README.md @@ -18,7 +18,7 @@ parent: ## Abstract -`x/incentive` is an implementation of a Cosmos SDK Module that allows for governance controlled user incentives for users who take certain actions, such as opening a collateralized debt position (CDP). Governance proposes an array of rewards, with each item representing a collateral type that will be eligible for rewards. Each collateral reward specifies the number of coins awarded per second, the length of rewards periods, and 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. User rewards are __opt in__, ie. users must claim rewards in order to receive them. If users fail to claim rewards before the claim period expiry, they are no longer eligible for rewards. +`x/incentive` is an implementation of a Cosmos SDK Module that allows for governance controlled user incentives for users who take certain actions, such as opening a collateralized debt position (CDP). Governance proposes an array of rewards, with each item representing a collateral type that will be eligible for rewards. Each collateral reward specifies the number of coins awarded per second, the length of rewards 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. User rewards are __opt in__, ie. users must claim rewards in order to receive them. If users fail to claim rewards before the claim period expiry, they are no longer eligible for rewards. ### Dependencies diff --git a/x/incentive/types/accumulator.go b/x/incentive/types/accumulator.go new file mode 100644 index 00000000..80ce7e70 --- /dev/null +++ b/x/incentive/types/accumulator.go @@ -0,0 +1,95 @@ +package types + +import ( + "fmt" + "math" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// An Accumulator handles calculating and tracking global reward distributions. +type Accumulator struct { + PreviousAccumulationTime time.Time + Indexes RewardIndexes +} + +func NewAccumulator(previousAccrual time.Time, indexes RewardIndexes) *Accumulator { + return &Accumulator{ + PreviousAccumulationTime: previousAccrual, + Indexes: indexes, + } +} + +// Accumulate accrues rewards up to the current time. +// +// It calculates new rewards and adds them to the reward indexes for the period from PreviousAccumulationTime to currentTime. +// It stores the currentTime in PreviousAccumulationTime to be used for later accumulations. +// +// Rewards are not accrued for times outside of the start and end times of a reward period. +// If a period ends before currentTime, the PreviousAccrualTime is shortened to the end time. This allows accumulate to be called sequentially on consecutive reward periods. +// +// rewardSourceTotal is the total of all user reward sources. For example: total shares in a swap pool, total btcb supplied to hard, or total usdx borrowed from all bnb CDPs. +func (acc *Accumulator) Accumulate(period MultiRewardPeriod, rewardSourceTotal sdk.Dec, currentTime time.Time) { + accumulationDuration := acc.getTimeElapsedWithinLimits(acc.PreviousAccumulationTime, currentTime, period.Start, period.End) + indexesIncrement := acc.calculateNewRewards(period.RewardsPerSecond, rewardSourceTotal, accumulationDuration) + + acc.Indexes = acc.Indexes.Add(indexesIncrement) + acc.PreviousAccumulationTime = minTime(period.End, currentTime) +} + +// getTimeElapsedWithinLimits returns the duration between start and end times, capped by min and max times. +// If the start and end range is outside the min to max time range then zero duration is returned. +func (acc *Accumulator) getTimeElapsedWithinLimits(start, end, limitMin, limitMax time.Time) time.Duration { + if start.After(end) { + panic(fmt.Sprintf("start time (%s) cannot be after end time (%s)", start, end)) + } + if limitMin.After(limitMax) { + panic(fmt.Sprintf("minimum limit time (%s) cannot be after maximum limit time (%s)", limitMin, limitMax)) + } + if start.After(limitMax) || end.Before(limitMin) { + // no intersection between the start-end and limitMin-limitMax time ranges + return 0 + } + return minTime(end, limitMax).Sub(maxTime(start, limitMin)) +} + +// calculateNewRewards calculates the amount to increase the global reward indexes for a given reward rate, duration, and source total. +// The total rewards to distribute in this block are given by reward rate * duration. This value divided by the source total to give +// total rewards per unit of source, which is what the indexes store. +// Note, duration is rounded to the nearest second to keep rewards calculation the same as in kava-7. +func (acc *Accumulator) calculateNewRewards(rewardsPerSecond sdk.Coins, rewardSourceTotal sdk.Dec, duration time.Duration) RewardIndexes { + if rewardSourceTotal.IsZero() { + // When the source total is zero, there is no users with deposits/borrows/delegations to pay out the current block's rewards to. + // So drop the rewards and pay out nothing. + return nil + } + durationSeconds := int64(math.RoundToEven(duration.Seconds())) + increment := newRewardIndexesFromCoins(rewardsPerSecond) + return increment.Mul(sdk.NewDec(durationSeconds)).Quo(rewardSourceTotal) +} + +// minTime returns the earliest of two times. +func minTime(t1, t2 time.Time) time.Time { + if t2.Before(t1) { + return t2 + } + return t1 +} + +// maxTime returns the latest of two times. +func maxTime(t1, t2 time.Time) time.Time { + if t2.After(t1) { + return t2 + } + return t1 +} + +// newRewardIndexesFromCoins is a helper function to initialize a RewardIndexes slice with the values from a Coins slice. +func newRewardIndexesFromCoins(coins sdk.Coins) RewardIndexes { + var indexes RewardIndexes + for _, coin := range coins { + indexes = append(indexes, NewRewardIndex(coin.Denom, coin.Amount.ToDec())) + } + return indexes +} diff --git a/x/incentive/types/accumulator_test.go b/x/incentive/types/accumulator_test.go new file mode 100644 index 00000000..9e4fd626 --- /dev/null +++ b/x/incentive/types/accumulator_test.go @@ -0,0 +1,348 @@ +package types + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestAccumulator(t *testing.T) { + t.Run("getTimeElapsedWithinLimits", func(t *testing.T) { + type args struct { + start, end time.Time + limitMin, limitMax time.Time + } + testcases := []struct { + name string + args args + expected time.Duration + }{ + { + name: "given time range is before limits and is non zero, return 0 duration", + args: args{ + start: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(1998, 1, 1, 0, 0, 1, 0, time.UTC), + limitMin: time.Date(2098, 1, 1, 0, 0, 0, 0, time.UTC), + limitMax: time.Date(2098, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: 0, + }, + { + name: "given time range is after limits and is non zero, return 0 duration", + args: args{ + start: time.Date(2098, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(2098, 1, 1, 0, 0, 1, 0, time.UTC), + limitMin: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + limitMax: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: 0, + }, + { + name: "given time range is within limits and is non zero, return duration", + args: args{ + start: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(1998, 1, 1, 0, 0, 1, 0, time.UTC), + limitMin: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), + limitMax: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: time.Second, + }, + { + name: "given time range is within limits and is zero, return 0 duration", + args: args{ + start: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + limitMin: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), + limitMax: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: 0, + }, + { + name: "given time range overlaps limitMax and is non zero, return capped duration", + args: args{ + start: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(1998, 1, 1, 0, 0, 2, 0, time.UTC), + limitMin: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), + limitMax: time.Date(1998, 1, 1, 0, 0, 1, 0, time.UTC), + }, + expected: time.Second, + }, + { + name: "given time range overlaps limitMin and is non zero, return capped duration", + args: args{ + start: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(1998, 1, 1, 0, 0, 2, 0, time.UTC), + limitMin: time.Date(1998, 1, 1, 0, 0, 1, 0, time.UTC), + limitMax: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: time.Second, + }, + { + name: "given time range is larger than limits, return capped duration", + args: args{ + start: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(1998, 1, 1, 0, 0, 10, 0, time.UTC), + limitMin: time.Date(1998, 1, 1, 0, 0, 1, 0, time.UTC), + limitMax: time.Date(1998, 1, 1, 0, 0, 9, 0, time.UTC), + }, + expected: 8 * time.Second, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + acc := &Accumulator{} + duration := acc.getTimeElapsedWithinLimits(tc.args.start, tc.args.end, tc.args.limitMin, tc.args.limitMax) + + require.Equal(t, tc.expected, duration) + }) + } + }) + t.Run("calculateNewRewards", func(t *testing.T) { + type args struct { + rewardsPerSecond sdk.Coins + duration time.Duration + rewardSourceTotal sdk.Dec + } + testcases := []struct { + name string + args args + expected RewardIndexes + }{ + { + name: "rewards calculated normally", + args: args{ + rewardsPerSecond: cs(c("hard", 1000), c("swap", 100)), + duration: 10 * time.Second, + rewardSourceTotal: d("1000"), + }, + expected: RewardIndexes{ + {CollateralType: "hard", RewardFactor: d("10")}, + {CollateralType: "swap", RewardFactor: d("1")}, + }, + }, + { + name: "duration is rounded to nearest even second", + args: args{ + rewardsPerSecond: cs(c("hard", 1000)), + duration: 10*time.Second + 500*time.Millisecond, + rewardSourceTotal: d("1000"), + }, + expected: RewardIndexes{ + {CollateralType: "hard", RewardFactor: d("10")}, + }, + }, + { + name: "when duration is zero the rewards are zero", + args: args{ + rewardsPerSecond: cs(c("hard", 1000)), + duration: 0, + rewardSourceTotal: d("1000"), + }, + expected: RewardIndexes{{CollateralType: "hard", RewardFactor: d("0")}}, // TODO should this be nil? + }, + { + name: "when rewards per second are nil there is no rewards", + args: args{ + rewardsPerSecond: cs(), + duration: 10 * time.Second, + rewardSourceTotal: d("1000"), + }, + expected: nil, + }, + { + name: "when the source total is zero there is no rewards", + args: args{ + rewardsPerSecond: cs(c("hard", 1000)), + duration: 10 * time.Second, + rewardSourceTotal: d("0"), + }, + expected: nil, + }, + { + name: "when all args are zero there is no rewards", + args: args{ + rewardsPerSecond: cs(), + duration: 0, + rewardSourceTotal: d("0"), + }, + expected: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + acc := &Accumulator{} + indexes := acc.calculateNewRewards(tc.args.rewardsPerSecond, tc.args.rewardSourceTotal, tc.args.duration) + + require.Equal(t, tc.expected, indexes) + }) + } + }) + t.Run("Accumulate", func(t *testing.T) { + type args struct { + accumulator Accumulator + period MultiRewardPeriod + rewardSourceTotal sdk.Dec + currentTime time.Time + } + testcases := []struct { + name string + args args + expected Accumulator + }{ + { + name: "normal", + args: args{ + accumulator: Accumulator{ + PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("0.1")}}, + }, + period: MultiRewardPeriod{ + Start: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + RewardsPerSecond: cs(c("hard", 1000)), + }, + rewardSourceTotal: d("1000"), + currentTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC), + }, + expected: Accumulator{ + PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC), + Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("5.1")}}, + }, + }, + { + name: "nil reward indexes are treated as empty", + args: args{ + accumulator: Accumulator{ + PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + Indexes: nil, + }, + period: MultiRewardPeriod{ + Start: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), + End: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + RewardsPerSecond: cs(c("hard", 1000)), + }, + rewardSourceTotal: d("1000"), + currentTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC), + }, + expected: Accumulator{ + PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC), + Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("5.0")}}, + }, + }, + { + name: "when a period is enclosed within block the accumulation time is set to the period end time", + args: args{ + accumulator: Accumulator{ + PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("0.1")}}, + }, + period: MultiRewardPeriod{ + Start: time.Date(1998, 1, 1, 0, 0, 5, 0, time.UTC), + End: time.Date(1998, 1, 1, 0, 0, 7, 0, time.UTC), + RewardsPerSecond: cs(c("hard", 1000)), + }, + rewardSourceTotal: d("1000"), + currentTime: time.Date(1998, 1, 1, 0, 0, 10, 0, time.UTC), + }, + expected: Accumulator{ + PreviousAccumulationTime: time.Date(1998, 1, 1, 0, 0, 7, 0, time.UTC), + Indexes: RewardIndexes{{CollateralType: "hard", RewardFactor: d("2.1")}}, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tc.args.accumulator.Accumulate(tc.args.period, tc.args.rewardSourceTotal, tc.args.currentTime) + require.Equal(t, tc.expected, tc.args.accumulator) + }) + } + }) +} + +func TestMinTime(t *testing.T) { + type args struct { + t1, t2 time.Time + } + testcases := []struct { + name string + args args + expected time.Time + }{ + { + name: "last arg greater than first", + args: args{ + t1: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + t2: time.Date(1998, 1, 1, 0, 0, 0, 1, time.UTC), + }, + expected: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "first arg greater than last", + args: args{ + t2: time.Date(1998, 1, 1, 0, 0, 0, 1, time.UTC), + t1: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "first and last args equal", + args: args{ + t2: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + t1: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, minTime(tc.args.t1, tc.args.t2)) + }) + } +} +func TestMaxTime(t *testing.T) { + type args struct { + t1, t2 time.Time + } + testcases := []struct { + name string + args args + expected time.Time + }{ + { + name: "last arg greater than first", + args: args{ + t1: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + t2: time.Date(1998, 1, 1, 0, 0, 0, 1, time.UTC), + }, + expected: time.Date(1998, 1, 1, 0, 0, 0, 1, time.UTC), + }, + { + name: "first arg greater than last", + args: args{ + t2: time.Date(1998, 1, 1, 0, 0, 0, 1, time.UTC), + t1: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: time.Date(1998, 1, 1, 0, 0, 0, 1, time.UTC), + }, + { + name: "first and last args equal", + args: args{ + t2: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + t1: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expected: time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, maxTime(tc.args.t1, tc.args.t2)) + }) + } +} diff --git a/x/incentive/types/claims.go b/x/incentive/types/claims.go index db5bb733..7a26a0dc 100644 --- a/x/incentive/types/claims.go +++ b/x/incentive/types/claims.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "strings" - "time" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -264,100 +263,6 @@ func (cs HardLiquidityProviderClaims) Validate() error { return nil } -// ---------------------- Reward periods are used by the params ---------------------- - -// MultiRewardPeriod supports multiple reward types -type MultiRewardPeriod struct { - Active bool `json:"active" yaml:"active"` - CollateralType string `json:"collateral_type" yaml:"collateral_type"` - Start time.Time `json:"start" yaml:"start"` - End time.Time `json:"end" yaml:"end"` - RewardsPerSecond sdk.Coins `json:"rewards_per_second" yaml:"rewards_per_second"` // per second reward payouts -} - -// String implements fmt.Stringer -func (mrp MultiRewardPeriod) String() string { - return fmt.Sprintf(`Reward Period: - Collateral Type: %s, - Start: %s, - End: %s, - Rewards Per Second: %s, - Active %t, - `, mrp.CollateralType, mrp.Start, mrp.End, mrp.RewardsPerSecond, mrp.Active) -} - -// NewMultiRewardPeriod returns a new MultiRewardPeriod -func NewMultiRewardPeriod(active bool, collateralType string, start time.Time, end time.Time, reward sdk.Coins) MultiRewardPeriod { - return MultiRewardPeriod{ - Active: active, - CollateralType: collateralType, - Start: start, - End: end, - RewardsPerSecond: reward, - } -} - -// Validate performs a basic check of a MultiRewardPeriod. -func (mrp MultiRewardPeriod) Validate() error { - if mrp.Start.IsZero() { - return errors.New("reward period start time cannot be 0") - } - if mrp.End.IsZero() { - return errors.New("reward period end time cannot be 0") - } - if mrp.Start.After(mrp.End) { - return fmt.Errorf("end period time %s cannot be before start time %s", mrp.End, mrp.Start) - } - if !mrp.RewardsPerSecond.IsValid() { - return fmt.Errorf("invalid reward amount: %s", mrp.RewardsPerSecond) - } - if strings.TrimSpace(mrp.CollateralType) == "" { - return fmt.Errorf("reward period collateral type cannot be blank: %s", mrp) - } - return nil -} - -// MultiRewardPeriods array of MultiRewardPeriod -type MultiRewardPeriods []MultiRewardPeriod - -// GetMultiRewardPeriod fetches a MultiRewardPeriod from an array of MultiRewardPeriods by its denom -func (mrps MultiRewardPeriods) GetMultiRewardPeriod(denom string) (MultiRewardPeriod, bool) { - for _, rp := range mrps { - if rp.CollateralType == denom { - return rp, true - } - } - return MultiRewardPeriod{}, false -} - -// GetMultiRewardPeriodIndex returns the index of a MultiRewardPeriod inside array MultiRewardPeriods -func (mrps MultiRewardPeriods) GetMultiRewardPeriodIndex(denom string) (int, bool) { - for i, rp := range mrps { - if rp.CollateralType == denom { - return i, true - } - } - return -1, false -} - -// Validate checks if all the RewardPeriods are valid and there are no duplicated -// entries. -func (mrps MultiRewardPeriods) Validate() error { - seenPeriods := make(map[string]bool) - for _, rp := range mrps { - if seenPeriods[rp.CollateralType] { - return fmt.Errorf("duplicated reward period with collateral type %s", rp.CollateralType) - } - - if err := rp.Validate(); err != nil { - return err - } - seenPeriods[rp.CollateralType] = true - } - - return nil -} - // ---------------------- Reward indexes are used internally in the store ---------------------- // RewardIndex stores reward accumulation information @@ -414,8 +319,7 @@ func (ris RewardIndexes) Get(denom string) (sdk.Dec, bool) { // With returns a copy of the indexes with a new reward factor added func (ris RewardIndexes) With(denom string, factor sdk.Dec) RewardIndexes { - newIndexes := make(RewardIndexes, len(ris)) - copy(newIndexes, ris) + newIndexes := ris.copy() for i, ri := range newIndexes { if ri.CollateralType == denom { @@ -446,6 +350,57 @@ func (ris RewardIndexes) Validate() error { return nil } +// Mul returns a copy of RewardIndexes with all factors multiplied by a single value. +func (ris RewardIndexes) Mul(multiplier sdk.Dec) RewardIndexes { + newIndexes := ris.copy() + + for i := range newIndexes { + newIndexes[i].RewardFactor = newIndexes[i].RewardFactor.Mul(multiplier) + } + return newIndexes +} + +// Quo returns a copy of RewardIndexes with all factors divided by a single value. +// It uses sdk.Dec.Quo for the division. +func (ris RewardIndexes) Quo(divisor sdk.Dec) RewardIndexes { + newIndexes := ris.copy() + + for i := range newIndexes { + newIndexes[i].RewardFactor = newIndexes[i].RewardFactor.Quo(divisor) + } + return newIndexes +} + +// Add combines two reward indexes by adding together factors with the same CollateralType. +// Any CollateralTypes unique to either reward indexes are included in the output as is. +func (ris RewardIndexes) Add(addend RewardIndexes) RewardIndexes { + newIndexes := ris.copy() + + for _, addRi := range addend { + found := false + for i, origRi := range newIndexes { + if origRi.CollateralType == addRi.CollateralType { + found = true + newIndexes[i].RewardFactor = newIndexes[i].RewardFactor.Add(addRi.RewardFactor) + } + } + if !found { + newIndexes = append(newIndexes, addRi) + } + } + return newIndexes +} + +// copy returns a copy of the reward indexes slice and underlying array +func (ris RewardIndexes) copy() RewardIndexes { + if ris == nil { // return nil rather than empty slice when ris is nil + return nil + } + newIndexes := make(RewardIndexes, len(ris)) + copy(newIndexes, ris) + return newIndexes +} + // MultiRewardIndex stores reward accumulation information on multiple reward types type MultiRewardIndex struct { CollateralType string `json:"collateral_type" yaml:"collateral_type"` diff --git a/x/incentive/types/claims_test.go b/x/incentive/types/claims_test.go index 03a862c2..f902d791 100644 --- a/x/incentive/types/claims_test.go +++ b/x/incentive/types/claims_test.go @@ -9,6 +9,15 @@ import ( "github.com/tendermint/tendermint/crypto" ) +// d is a helper function for creating sdk.Dec values in tests +func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } + +// c is a helper function for created sdk.Coin types in tests +func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } + +// c is a helper function for created sdk.Coins types in tests +func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } + func TestClaimsValidate(t *testing.T) { owner := sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1"))) @@ -169,6 +178,249 @@ func TestRewardIndexes(t *testing.T) { }) } }) + t.Run("Mul", func(t *testing.T) { + + testcases := []struct { + name string + rewardIndexes RewardIndexes + multiplier sdk.Dec + expected RewardIndexes + }{ + { + name: "non zero values are all multiplied", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + NewRewardIndex("denom2", d("0.2")), + }, + multiplier: d("2.0"), + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.2")), + NewRewardIndex("denom2", d("0.4")), + }, + }, + { + name: "multiplying by zero, zeros all values", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + NewRewardIndex("denom2", d("0.0")), + }, + multiplier: d("0.0"), + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.0")), + NewRewardIndex("denom2", d("0.0")), + }, + }, + { + name: "empty indexes are unchanged", + rewardIndexes: RewardIndexes{}, + multiplier: d("2.0"), + expected: RewardIndexes{}, + }, + { + name: "nil indexes are unchanged", + rewardIndexes: nil, + multiplier: d("2.0"), + expected: nil, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, tc.rewardIndexes.Mul(tc.multiplier)) + }) + } + }) + t.Run("Quo", func(t *testing.T) { + type expected struct { + indexes RewardIndexes + panics bool + } + testcases := []struct { + name string + rewardIndexes RewardIndexes + divisor sdk.Dec + expected expected + }{ + { + name: "non zero values are all divided", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.6")), + NewRewardIndex("denom2", d("0.2")), + }, + divisor: d("3.0"), + expected: expected{ + indexes: RewardIndexes{ + NewRewardIndex("denom", d("0.2")), + NewRewardIndex("denom2", d("0.066666666666666667")), + }, + }, + }, + { + name: "diving by zero panics when values are present", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + NewRewardIndex("denom2", d("0.0")), + }, + divisor: d("0.0"), + expected: expected{ + panics: true, + }, + }, + { + name: "empty indexes are unchanged", + rewardIndexes: RewardIndexes{}, + divisor: d("2.0"), + expected: expected{ + indexes: RewardIndexes{}, + }, + }, + { + name: "nil indexes are unchanged", + rewardIndexes: nil, + divisor: d("2.0"), + expected: expected{ + indexes: nil, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var actual RewardIndexes + quoFunc := func() { actual = tc.rewardIndexes.Quo(tc.divisor) } + if tc.expected.panics { + require.Panics(t, quoFunc) + return + } else { + require.NotPanics(t, quoFunc) + } + require.Equal(t, tc.expected.indexes, actual) + }) + } + }) + t.Run("Add", func(t *testing.T) { + + testcases := []struct { + name string + rewardIndexes RewardIndexes + addend RewardIndexes + expected RewardIndexes + }{ + { + name: "same denoms are added", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + NewRewardIndex("denom2", d("0.2")), + }, + addend: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + NewRewardIndex("denom2", d("0.2")), + }, + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.2")), + NewRewardIndex("denom2", d("0.4")), + }, + }, + { + name: "new denoms are appended", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + addend: RewardIndexes{ + NewRewardIndex("denom", d("0.3")), + NewRewardIndex("denom2", d("0.2")), + }, + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.4")), + NewRewardIndex("denom2", d("0.2")), + }, + }, + { + name: "missing denoms are unchanged", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + NewRewardIndex("denom2", d("0.2")), + }, + addend: RewardIndexes{ + NewRewardIndex("denom2", d("0.2")), + }, + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + NewRewardIndex("denom2", d("0.4")), + }, + }, + { + name: "adding empty indexes does nothing", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + addend: RewardIndexes{}, + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + }, + { + name: "adding nil indexes does nothing", + rewardIndexes: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + addend: nil, + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + }, + { + name: "denom can be added to empty indexes", + rewardIndexes: RewardIndexes{}, + addend: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + }, + { + name: "denom can be added to nil indexes", + rewardIndexes: nil, + addend: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + expected: RewardIndexes{ + NewRewardIndex("denom", d("0.1")), + }, + }, + { + name: "adding empty indexes to nil does nothing", + rewardIndexes: nil, + addend: RewardIndexes{}, + expected: nil, + }, + { + name: "adding nil to empty indexes does nothing", + rewardIndexes: RewardIndexes{}, + addend: nil, + expected: RewardIndexes{}, + }, + { + name: "adding nil to nil indexes does nothing", + rewardIndexes: nil, + addend: nil, + expected: nil, + }, + { + name: "adding empty indexes to empty indexes does nothing", + rewardIndexes: RewardIndexes{}, + addend: RewardIndexes{}, + expected: RewardIndexes{}, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + sum := tc.rewardIndexes.Add(tc.addend) + require.Equal(t, tc.expected, sum) + }) + } + }) } func TestMultiRewardIndexes(t *testing.T) { diff --git a/x/incentive/types/expected_keepers.go b/x/incentive/types/expected_keepers.go index c56145fb..69f44dc3 100644 --- a/x/incentive/types/expected_keepers.go +++ b/x/incentive/types/expected_keepers.go @@ -51,6 +51,11 @@ type HardKeeper interface { GetSuppliedCoins(ctx sdk.Context) (coins sdk.Coins, found bool) } +// SwapKeeper defines the required methods needed by this modules keeper +type SwapKeeper interface { + GetPoolShares(ctx sdk.Context, poolID string) (shares sdk.Dec, found bool) +} + // AccountKeeper defines the expected keeper interface for interacting with account type AccountKeeper interface { GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account diff --git a/x/incentive/types/genesis.go b/x/incentive/types/genesis.go index 888597bb..d91d8c89 100644 --- a/x/incentive/types/genesis.go +++ b/x/incentive/types/genesis.go @@ -13,18 +13,20 @@ type GenesisState struct { HardSupplyAccumulationTimes GenesisAccumulationTimes `json:"hard_supply_accumulation_times" yaml:"hard_supply_accumulation_times"` HardBorrowAccumulationTimes GenesisAccumulationTimes `json:"hard_borrow_accumulation_times" yaml:"hard_borrow_accumulation_times"` HardDelegatorAccumulationTimes GenesisAccumulationTimes `json:"hard_delegator_accumulation_times" yaml:"hard_delegator_accumulation_times"` + SwapAccumulationTimes GenesisAccumulationTimes `json:"swap_accumulation_times" yaml:"swap_accumulation_times"` USDXMintingClaims USDXMintingClaims `json:"usdx_minting_claims" yaml:"usdx_minting_claims"` HardLiquidityProviderClaims HardLiquidityProviderClaims `json:"hard_liquidity_provider_claims" yaml:"hard_liquidity_provider_claims"` } // NewGenesisState returns a new genesis state -func NewGenesisState(params Params, usdxAccumTimes, hardSupplyAccumTimes, hardBorrowAccumTimes, hardDelegatorAccumTimes GenesisAccumulationTimes, c USDXMintingClaims, hc HardLiquidityProviderClaims) GenesisState { +func NewGenesisState(params Params, usdxAccumTimes, hardSupplyAccumTimes, hardBorrowAccumTimes, hardDelegatorAccumTimes, swapAccumTimes GenesisAccumulationTimes, c USDXMintingClaims, hc HardLiquidityProviderClaims) GenesisState { return GenesisState{ Params: params, USDXAccumulationTimes: usdxAccumTimes, HardSupplyAccumulationTimes: hardSupplyAccumTimes, HardBorrowAccumulationTimes: hardBorrowAccumTimes, HardDelegatorAccumulationTimes: hardDelegatorAccumTimes, + SwapAccumulationTimes: swapAccumTimes, USDXMintingClaims: c, HardLiquidityProviderClaims: hc, } @@ -38,6 +40,7 @@ func DefaultGenesisState() GenesisState { HardSupplyAccumulationTimes: GenesisAccumulationTimes{}, HardBorrowAccumulationTimes: GenesisAccumulationTimes{}, HardDelegatorAccumulationTimes: GenesisAccumulationTimes{}, + SwapAccumulationTimes: GenesisAccumulationTimes{}, USDXMintingClaims: DefaultUSDXClaims, HardLiquidityProviderClaims: DefaultHardClaims, } @@ -61,6 +64,9 @@ func (gs GenesisState) Validate() error { if err := gs.HardDelegatorAccumulationTimes.Validate(); err != nil { return err } + if err := gs.SwapAccumulationTimes.Validate(); err != nil { + return err + } if err := gs.HardLiquidityProviderClaims.Validate(); err != nil { return err diff --git a/x/incentive/types/genesis_test.go b/x/incentive/types/genesis_test.go index 1136574c..0278c026 100644 --- a/x/incentive/types/genesis_test.go +++ b/x/incentive/types/genesis_test.go @@ -55,6 +55,7 @@ func TestGenesisStateValidate(t *testing.T) { DefaultMultiRewardPeriods, DefaultMultiRewardPeriods, DefaultMultiRewardPeriods, + DefaultMultiRewardPeriods, Multipliers{ NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33")), }, @@ -130,7 +131,16 @@ func TestGenesisStateValidate(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - gs := NewGenesisState(tc.args.params, tc.args.genAccTimes, tc.args.genAccTimes, tc.args.genAccTimes, tc.args.genAccTimes, tc.args.claims, DefaultHardClaims) + gs := NewGenesisState( + tc.args.params, + tc.args.genAccTimes, + tc.args.genAccTimes, + tc.args.genAccTimes, + tc.args.genAccTimes, + tc.args.genAccTimes, + tc.args.claims, + DefaultHardClaims, + ) err := gs.Validate() if tc.errArgs.expectPass { require.NoError(t, err, tc.name) diff --git a/x/incentive/types/keys.go b/x/incentive/types/keys.go index 38621407..0bd02a6f 100644 --- a/x/incentive/types/keys.go +++ b/x/incentive/types/keys.go @@ -33,6 +33,8 @@ var ( PreviousHardBorrowRewardAccrualTimeKeyPrefix = []byte{0x08} // prefix for key that stores the previous time Hard borrow rewards accrued HardDelegatorRewardIndexesKeyPrefix = []byte{0x09} // prefix for key that stores Hard delegator reward indexes PreviousHardDelegatorRewardAccrualTimeKeyPrefix = []byte{0x10} // prefix for key that stores the previous time Hard delegator rewards accrued + SwapRewardIndexesKeyPrefix = []byte{0x11} // prefix for key that stores swap reward indexes + PreviousSwapRewardAccrualTimeKeyPrefix = []byte{0x12} // prefix for key that stores the previous time swap rewards accrued USDXMintingRewardDenom = "ukava" HardLiquidityRewardDenom = "hard" diff --git a/x/incentive/types/params.go b/x/incentive/types/params.go index e022ed96..7259d59d 100644 --- a/x/incentive/types/params.go +++ b/x/incentive/types/params.go @@ -28,6 +28,7 @@ var ( KeyHardSupplyRewardPeriods = []byte("HardSupplyRewardPeriods") KeyHardBorrowRewardPeriods = []byte("HardBorrowRewardPeriods") KeyHardDelegatorRewardPeriods = []byte("HardDelegatorRewardPeriods") + KeySwapRewardPeriods = []byte("SwapRewardPeriods") KeyClaimEnd = []byte("ClaimEnd") KeyMultipliers = []byte("ClaimMultipliers") DefaultActive = false @@ -49,18 +50,20 @@ type Params struct { HardSupplyRewardPeriods MultiRewardPeriods `json:"hard_supply_reward_periods" yaml:"hard_supply_reward_periods"` HardBorrowRewardPeriods MultiRewardPeriods `json:"hard_borrow_reward_periods" yaml:"hard_borrow_reward_periods"` HardDelegatorRewardPeriods MultiRewardPeriods `json:"hard_delegator_reward_periods" yaml:"hard_delegator_reward_periods"` + SwapRewardPeriods MultiRewardPeriods `json:"swap_reward_periods" json:"swap_reward_periods"` ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"` ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"` } // NewParams returns a new params object -func NewParams(usdxMinting RewardPeriods, hardSupply, hardBorrow, hardDelegator MultiRewardPeriods, +func NewParams(usdxMinting RewardPeriods, hardSupply, hardBorrow, hardDelegator, swap MultiRewardPeriods, multipliers Multipliers, claimEnd time.Time) Params { return Params{ USDXMintingRewardPeriods: usdxMinting, HardSupplyRewardPeriods: hardSupply, HardBorrowRewardPeriods: hardBorrow, HardDelegatorRewardPeriods: hardDelegator, + SwapRewardPeriods: swap, ClaimMultipliers: multipliers, ClaimEnd: claimEnd, } @@ -68,9 +71,14 @@ func NewParams(usdxMinting RewardPeriods, hardSupply, hardBorrow, hardDelegator // DefaultParams returns default params for incentive module func DefaultParams() Params { - return NewParams(DefaultRewardPeriods, DefaultMultiRewardPeriods, - DefaultMultiRewardPeriods, DefaultMultiRewardPeriods, - DefaultMultipliers, DefaultClaimEnd, + return NewParams( + DefaultRewardPeriods, + DefaultMultiRewardPeriods, + DefaultMultiRewardPeriods, + DefaultMultiRewardPeriods, + DefaultMultiRewardPeriods, + DefaultMultipliers, + DefaultClaimEnd, ) } @@ -81,10 +89,11 @@ func (p Params) String() string { Hard Supply Reward Periods: %s Hard Borrow Reward Periods: %s Hard Delegator Reward Periods: %s + Swap Reward Periods: %s Claim Multipliers :%s Claim End Time: %s `, p.USDXMintingRewardPeriods, p.HardSupplyRewardPeriods, p.HardBorrowRewardPeriods, - p.HardDelegatorRewardPeriods, p.ClaimMultipliers, p.ClaimEnd) + p.HardDelegatorRewardPeriods, p.SwapRewardPeriods, p.ClaimMultipliers, p.ClaimEnd) } // ParamKeyTable Key declaration for parameters @@ -99,6 +108,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { params.NewParamSetPair(KeyHardSupplyRewardPeriods, &p.HardSupplyRewardPeriods, validateMultiRewardPeriodsParam), params.NewParamSetPair(KeyHardBorrowRewardPeriods, &p.HardBorrowRewardPeriods, validateMultiRewardPeriodsParam), params.NewParamSetPair(KeyHardDelegatorRewardPeriods, &p.HardDelegatorRewardPeriods, validateMultiRewardPeriodsParam), + params.NewParamSetPair(KeySwapRewardPeriods, &p.SwapRewardPeriods, validateMultiRewardPeriodsParam), params.NewParamSetPair(KeyClaimEnd, &p.ClaimEnd, validateClaimEndParam), params.NewParamSetPair(KeyMultipliers, &p.ClaimMultipliers, validateMultipliersParam), } @@ -123,7 +133,15 @@ func (p Params) Validate() error { return err } - return validateMultiRewardPeriodsParam(p.HardDelegatorRewardPeriods) + if err := validateMultiRewardPeriodsParam(p.HardDelegatorRewardPeriods); err != nil { + return err + } + + if err := validateMultiRewardPeriodsParam(p.SwapRewardPeriods); err != nil { + return err + } + + return nil } func validateRewardPeriodsParam(i interface{}) error { @@ -235,6 +253,98 @@ func (rps RewardPeriods) Validate() error { return nil } +// MultiRewardPeriod supports multiple reward types +type MultiRewardPeriod struct { + Active bool `json:"active" yaml:"active"` + CollateralType string `json:"collateral_type" yaml:"collateral_type"` + Start time.Time `json:"start" yaml:"start"` + End time.Time `json:"end" yaml:"end"` + RewardsPerSecond sdk.Coins `json:"rewards_per_second" yaml:"rewards_per_second"` // per second reward payouts +} + +// String implements fmt.Stringer +func (mrp MultiRewardPeriod) String() string { + return fmt.Sprintf(`Reward Period: + Collateral Type: %s, + Start: %s, + End: %s, + Rewards Per Second: %s, + Active %t, + `, mrp.CollateralType, mrp.Start, mrp.End, mrp.RewardsPerSecond, mrp.Active) +} + +// NewMultiRewardPeriod returns a new MultiRewardPeriod +func NewMultiRewardPeriod(active bool, collateralType string, start time.Time, end time.Time, reward sdk.Coins) MultiRewardPeriod { + return MultiRewardPeriod{ + Active: active, + CollateralType: collateralType, + Start: start, + End: end, + RewardsPerSecond: reward, + } +} + +// Validate performs a basic check of a MultiRewardPeriod. +func (mrp MultiRewardPeriod) Validate() error { + if mrp.Start.IsZero() { + return errors.New("reward period start time cannot be 0") + } + if mrp.End.IsZero() { + return errors.New("reward period end time cannot be 0") + } + if mrp.Start.After(mrp.End) { + return fmt.Errorf("end period time %s cannot be before start time %s", mrp.End, mrp.Start) + } + if !mrp.RewardsPerSecond.IsValid() { + return fmt.Errorf("invalid reward amount: %s", mrp.RewardsPerSecond) + } + if strings.TrimSpace(mrp.CollateralType) == "" { + return fmt.Errorf("reward period collateral type cannot be blank: %s", mrp) + } + return nil +} + +// MultiRewardPeriods array of MultiRewardPeriod +type MultiRewardPeriods []MultiRewardPeriod + +// GetMultiRewardPeriod fetches a MultiRewardPeriod from an array of MultiRewardPeriods by its denom +func (mrps MultiRewardPeriods) GetMultiRewardPeriod(denom string) (MultiRewardPeriod, bool) { + for _, rp := range mrps { + if rp.CollateralType == denom { + return rp, true + } + } + return MultiRewardPeriod{}, false +} + +// GetMultiRewardPeriodIndex returns the index of a MultiRewardPeriod inside array MultiRewardPeriods +func (mrps MultiRewardPeriods) GetMultiRewardPeriodIndex(denom string) (int, bool) { + for i, rp := range mrps { + if rp.CollateralType == denom { + return i, true + } + } + return -1, false +} + +// Validate checks if all the RewardPeriods are valid and there are no duplicated +// entries. +func (mrps MultiRewardPeriods) Validate() error { + seenPeriods := make(map[string]bool) + for _, rp := range mrps { + if seenPeriods[rp.CollateralType] { + return fmt.Errorf("duplicated reward period with collateral type %s", rp.CollateralType) + } + + if err := rp.Validate(); err != nil { + return err + } + seenPeriods[rp.CollateralType] = true + } + + return nil +} + // Multiplier amount the claim rewards get increased by, along with how long the claim rewards are locked type Multiplier struct { Name MultiplierName `json:"name" yaml:"name"` diff --git a/x/incentive/types/params_test.go b/x/incentive/types/params_test.go index 85d84c6a..8c8ed9bb 100644 --- a/x/incentive/types/params_test.go +++ b/x/incentive/types/params_test.go @@ -1,7 +1,6 @@ package types_test import ( - "strings" "testing" "time" @@ -18,49 +17,52 @@ type ParamTestSuite struct { func (suite *ParamTestSuite) SetupTest() {} -func (suite *ParamTestSuite) TestParamValidation() { - type args struct { - usdxMintingRewardPeriods types.RewardPeriods - hardSupplyRewardPeriods types.MultiRewardPeriods - hardBorrowRewardPeriods types.MultiRewardPeriods - hardDelegatorRewardPeriods types.MultiRewardPeriods - multipliers types.Multipliers - end time.Time - } +var rewardPeriodWithInvalidRewardsPerSecond = types.NewRewardPeriod( + true, + "bnb", + time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC), + time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC), + sdk.Coin{Denom: "INVALID!@#😫", Amount: sdk.ZeroInt()}, +) +var rewardMultiPeriodWithInvalidRewardsPerSecond = types.NewMultiRewardPeriod( + true, + "bnb", + time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC), + time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC), + sdk.Coins{sdk.Coin{Denom: "INVALID!@#😫", Amount: sdk.ZeroInt()}}, +) +func (suite *ParamTestSuite) TestParamValidation() { type errArgs struct { expectPass bool contains string } type test struct { name string - args args + params types.Params errArgs errArgs } testCases := []test{ { - "default", - args{ - usdxMintingRewardPeriods: types.DefaultRewardPeriods, - hardSupplyRewardPeriods: types.DefaultMultiRewardPeriods, - hardBorrowRewardPeriods: types.DefaultMultiRewardPeriods, - hardDelegatorRewardPeriods: types.DefaultMultiRewardPeriods, - multipliers: types.DefaultMultipliers, - end: types.DefaultClaimEnd, - }, + "default is valid", + types.DefaultParams(), errArgs{ expectPass: true, - contains: "", }, }, { "valid", - args{ - usdxMintingRewardPeriods: types.RewardPeriods{types.NewRewardPeriod( - true, "bnb-a", time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC), - sdk.NewCoin(types.USDXMintingRewardDenom, sdk.NewInt(122354)))}, - multipliers: types.Multipliers{ + types.Params{ + USDXMintingRewardPeriods: types.RewardPeriods{ + types.NewRewardPeriod( + true, + "bnb-a", + time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC), + time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC), + sdk.NewCoin(types.USDXMintingRewardDenom, sdk.NewInt(122354)), + )}, + ClaimMultipliers: types.Multipliers{ types.NewMultiplier( types.Small, 1, sdk.MustNewDecFromStr("0.25"), ), @@ -68,29 +70,107 @@ func (suite *ParamTestSuite) TestParamValidation() { types.Large, 1, sdk.MustNewDecFromStr("1.0"), ), }, - hardSupplyRewardPeriods: types.DefaultMultiRewardPeriods, - hardBorrowRewardPeriods: types.DefaultMultiRewardPeriods, - hardDelegatorRewardPeriods: types.DefaultMultiRewardPeriods, - end: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), + HardSupplyRewardPeriods: types.DefaultMultiRewardPeriods, + HardBorrowRewardPeriods: types.DefaultMultiRewardPeriods, + HardDelegatorRewardPeriods: types.DefaultMultiRewardPeriods, + SwapRewardPeriods: types.DefaultMultiRewardPeriods, + ClaimEnd: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), }, errArgs{ expectPass: true, - contains: "", + }, + }, + { + "invalid usdx minting period makes params invalid", + types.Params{ + USDXMintingRewardPeriods: types.RewardPeriods{rewardPeriodWithInvalidRewardsPerSecond}, + HardSupplyRewardPeriods: types.DefaultMultiRewardPeriods, + HardBorrowRewardPeriods: types.DefaultMultiRewardPeriods, + HardDelegatorRewardPeriods: types.DefaultMultiRewardPeriods, + SwapRewardPeriods: types.DefaultMultiRewardPeriods, + ClaimMultipliers: types.DefaultMultipliers, + ClaimEnd: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), + }, + errArgs{ + expectPass: false, + contains: "invalid reward amount", + }, + }, + { + "invalid hard supply periods makes params invalid", + types.Params{ + USDXMintingRewardPeriods: types.DefaultRewardPeriods, + HardSupplyRewardPeriods: types.MultiRewardPeriods{rewardMultiPeriodWithInvalidRewardsPerSecond}, + HardBorrowRewardPeriods: types.DefaultMultiRewardPeriods, + HardDelegatorRewardPeriods: types.DefaultMultiRewardPeriods, + SwapRewardPeriods: types.DefaultMultiRewardPeriods, + ClaimMultipliers: types.DefaultMultipliers, + ClaimEnd: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), + }, + errArgs{ + expectPass: false, + contains: "invalid reward amount", + }, + }, + { + "invalid hard borrow periods makes params invalid", + types.Params{ + USDXMintingRewardPeriods: types.DefaultRewardPeriods, + HardSupplyRewardPeriods: types.DefaultMultiRewardPeriods, + HardBorrowRewardPeriods: types.MultiRewardPeriods{rewardMultiPeriodWithInvalidRewardsPerSecond}, + HardDelegatorRewardPeriods: types.DefaultMultiRewardPeriods, + SwapRewardPeriods: types.DefaultMultiRewardPeriods, + ClaimMultipliers: types.DefaultMultipliers, + ClaimEnd: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), + }, + errArgs{ + expectPass: false, + contains: "invalid reward amount", + }, + }, + { + "invalid delegator periods makes params invalid", + types.Params{ + USDXMintingRewardPeriods: types.DefaultRewardPeriods, + HardSupplyRewardPeriods: types.DefaultMultiRewardPeriods, + HardBorrowRewardPeriods: types.DefaultMultiRewardPeriods, + HardDelegatorRewardPeriods: types.MultiRewardPeriods{rewardMultiPeriodWithInvalidRewardsPerSecond}, + SwapRewardPeriods: types.DefaultMultiRewardPeriods, + ClaimMultipliers: types.DefaultMultipliers, + ClaimEnd: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), + }, + errArgs{ + expectPass: false, + contains: "invalid reward amount", + }, + }, + { + "invalid swap periods makes params invalid", + types.Params{ + USDXMintingRewardPeriods: types.DefaultRewardPeriods, + HardSupplyRewardPeriods: types.DefaultMultiRewardPeriods, + HardBorrowRewardPeriods: types.DefaultMultiRewardPeriods, + HardDelegatorRewardPeriods: types.DefaultMultiRewardPeriods, + SwapRewardPeriods: types.MultiRewardPeriods{rewardMultiPeriodWithInvalidRewardsPerSecond}, + ClaimMultipliers: types.DefaultMultipliers, + ClaimEnd: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC), + }, + errArgs{ + expectPass: false, + contains: "invalid reward amount", }, }, } for _, tc := range testCases { suite.Run(tc.name, func() { - params := types.NewParams(tc.args.usdxMintingRewardPeriods, tc.args.hardSupplyRewardPeriods, - tc.args.hardBorrowRewardPeriods, tc.args.hardDelegatorRewardPeriods, tc.args.multipliers, tc.args.end, - ) - err := params.Validate() + err := tc.params.Validate() + if tc.errArgs.expectPass { suite.Require().NoError(err) } else { suite.Require().Error(err) - suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) + suite.Require().Contains(err.Error(), tc.errArgs.contains) } }) } diff --git a/x/swap/keeper/hooks.go b/x/swap/keeper/hooks.go new file mode 100644 index 00000000..7267b99c --- /dev/null +++ b/x/swap/keeper/hooks.go @@ -0,0 +1,10 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (k Keeper) GetPoolShares(ctx sdk.Context, poolID string) (sdk.Dec, bool) { + // FIXME return pool shares once merged with acceptance branch + return sdk.Dec{}, false +}