Swaps accumulate global rewards (#947)

* add get set methods for swap reward indexes

* add get set methods for swap accrual time

* tidy up location of multi periods

* add swap reward periods to params

* add initial legacy types for incentive

* minor refactor of migration code

* add incentive migration for swap params

* minor incentive test refactors

* add math methods to RewardIndexes

* add keeper method to increment global indexes

* add swap keeper to incentive keeper

* indicate if pool shares were found or not

* add accumulator to compute new rewards each block

* accumulate swap rewards globally

* remove unecessary keeper method

* expand doc comments on accumulator methods

* test precision not lost in accumulation

* minor fixes from merge

* rename storeGlobalDelegatorFactor to match others

* fix migration from merge

* fix bug in app setup

* fix accumulation bug when starting with no state

* rename swap files to match others

* add swap accumulation times to genesis

* remove old migration refactor

* minor updates to spec

* add high level description of how rewards work
This commit is contained in:
Ruaridh 2021-07-07 14:23:06 +01:00 committed by GitHub
parent bc33b94822
commit c7962e45c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1637 additions and 259 deletions

View File

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

View File

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

View File

@ -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
v0_14Codec.MustUnmarshalJSON(v0_14AppState[v0_14committee.ModuleName], &committeeGS)
delete(v0_14AppState, v0_14committee.ModuleName)
// Marshal v15 committee genesis state
v0_14AppState[v0_15committee.ModuleName] = v0_15Codec.MustMarshalJSON(Committee(committeeGS))
}
}
func makeV014Codec() *codec.Codec {
cdc := codec.New()
sdk.RegisterCodec(cdc)
v0_14committee.RegisterCodec(cdc)
cdc.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))
}
return v0_15AppState
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,
)

View File

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

View File

@ -33,4 +33,7 @@ func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
panic(err)
}
}
for _, rp := range params.SwapRewardPeriods {
k.AccumulateSwapRewards(ctx, rp)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

10
x/swap/keeper/hooks.go Normal file
View File

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