From 8e7fc9db4fe174b78f48d9075daa3bf5b1149413 Mon Sep 17 00:00:00 2001 From: Adam Moser <63419657+toteki@users.noreply.github.com> Date: Mon, 19 Jun 2023 16:35:31 -0600 Subject: [PATCH] test: remaining incentive module unit tests (#2061) * genesis validate tests * ++ * ++ * genesis struct tests * test++ * validate genesis on init * comment out test due to experimental * genesis test * grpc query tests * hooks test and fix * verify donate collateral in MsgEmergencyUnbond * todo-- * suggestion++ Co-authored-by: Robert Zaremba * suggestion++ Co-authored-by: Robert Zaremba * update to v5 * use coin fixtures * remove repeated string * remove experimental flag from incentive, thus fix tests * add parallel * lint --------- Co-authored-by: Robert Zaremba --- app/app.go | 44 ++--- app/upgrades.go | 8 +- tools/tools.go | 2 +- x/incentive/genesis.go | 69 ++++++- x/incentive/genesis_test.go | 273 ++++++++++++++++++++++++++ x/incentive/keeper/bond_test.go | 45 +++++ x/incentive/keeper/genesis.go | 4 + x/incentive/keeper/genesis_test.go | 25 +++ x/incentive/keeper/grpc_query_test.go | 178 +++++++++++++++-- x/incentive/keeper/hooks_test.go | 73 +++++++ x/incentive/keeper/mock_test.go | 9 +- x/incentive/keeper/msg_server.go | 2 +- x/incentive/keeper/msg_server_test.go | 42 ++-- x/incentive/keeper/scenario_test.go | 26 +-- x/incentive/keeper/unbonding.go | 4 +- x/incentive/keeper/unit_test.go | 55 ++++++ x/incentive/keys_test.go | 2 + x/incentive/msgs_test.go | 9 +- x/incentive/params_test.go | 2 + 19 files changed, 779 insertions(+), 93 deletions(-) create mode 100644 x/incentive/keeper/bond_test.go create mode 100644 x/incentive/keeper/genesis_test.go create mode 100644 x/incentive/keeper/hooks_test.go diff --git a/app/app.go b/app/app.go index f32d1f6f0d..199b972302 100644 --- a/app/app.go +++ b/app/app.go @@ -196,10 +196,7 @@ func init() { uibcmodule.AppModuleBasic{}, ugovmodule.AppModuleBasic{}, wasm.AppModuleBasic{}, - } - - if Experimental { - moduleBasics = append(moduleBasics, incentivemodule.AppModuleBasic{}) + incentivemodule.AppModuleBasic{}, } ModuleBasics = module.NewBasicManager(moduleBasics...) @@ -337,10 +334,7 @@ func New( leveragetypes.StoreKey, oracletypes.StoreKey, bech32ibctypes.StoreKey, uibc.StoreKey, ugov.StoreKey, wasm.StoreKey, - } - - if Experimental { - storeKeys = append(storeKeys, incentive.StoreKey) + incentive.StoreKey, } keys := sdk.NewKVStoreKeys(storeKeys...) @@ -478,15 +472,13 @@ func New( app.LeverageKeeper.SetTokenHooks(app.OracleKeeper.Hooks()) - if Experimental { - app.IncentiveKeeper = incentivekeeper.NewKeeper( - appCodec, - keys[incentive.StoreKey], - app.BankKeeper, - app.LeverageKeeper, - ) - app.LeverageKeeper.SetBondHooks(app.IncentiveKeeper.BondHooks()) - } + app.IncentiveKeeper = incentivekeeper.NewKeeper( + appCodec, + keys[incentive.StoreKey], + app.BankKeeper, + app.LeverageKeeper, + ) + app.LeverageKeeper.SetBondHooks(app.IncentiveKeeper.BondHooks()) app.UGovKeeperB = ugovkeeper.NewKeeperBuilder(appCodec, keys[ugov.ModuleName]) @@ -720,12 +712,7 @@ func New( uibcmodule.NewAppModule(appCodec, app.UIbcQuotaKeeperB), ugovmodule.NewAppModule(appCodec, app.UGovKeeperB), wasm.NewAppModule(app.appCodec, &app.WasmKeeper, app.StakingKeeper, app.AccountKeeper, app.BankKeeper), - } - if Experimental { - appModules = append( - appModules, - incentivemodule.NewAppModule(appCodec, app.IncentiveKeeper, app.BankKeeper, app.LeverageKeeper), - ) + incentivemodule.NewAppModule(appCodec, app.IncentiveKeeper, app.BankKeeper, app.LeverageKeeper), } app.mm = module.NewManager(appModules...) @@ -752,6 +739,7 @@ func New( uibc.ModuleName, ugov.ModuleName, wasm.ModuleName, + incentive.ModuleName, } endBlockers := []string{ @@ -771,6 +759,7 @@ func New( uibc.ModuleName, ugov.ModuleName, wasm.ModuleName, + incentive.ModuleName, } // NOTE: The genutils module must occur after staking so that pools are @@ -795,6 +784,7 @@ func New( uibc.ModuleName, ugov.ModuleName, wasm.ModuleName, + incentive.ModuleName, } orderMigrations := []string{ @@ -812,13 +802,7 @@ func New( uibc.ModuleName, ugov.ModuleName, wasm.ModuleName, - } - - if Experimental { - beginBlockers = append(beginBlockers, incentive.ModuleName) - endBlockers = append(endBlockers, incentive.ModuleName) - initGenesis = append(initGenesis, incentive.ModuleName) - orderMigrations = append(orderMigrations, incentive.ModuleName) + incentive.ModuleName, } app.mm.SetOrderBeginBlockers(beginBlockers...) diff --git a/app/upgrades.go b/app/upgrades.go index 5478a9e65a..28f7e86e3a 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -52,9 +52,8 @@ func (app UmeeApp) RegisterUpgradeHandlers(bool) { app.registerUpgrade4_3(upgradeInfo) app.registerUpgrade("v4.4", upgradeInfo) app.registerUpgrade("v5.0", upgradeInfo, ugov.ModuleName, wasm.ModuleName) - if Experimental { - app.registerUpgrade("v4.5-alpha1", upgradeInfo, incentive.ModuleName) // TODO: set correct name - } + app.registerUpgrade("v5.1-alpha1", upgradeInfo, incentive.ModuleName) + // TODO: set correct 5.1 name and add borrowFactor migration } // performs upgrade from v4.2 to v4.3 @@ -260,6 +259,7 @@ func (app *UmeeApp) registerUpgrade(planName string, upgradeInfo upgradetypes.Pl if len(newStores) > 0 { app.storeUpgrade(planName, upgradeInfo, storetypes.StoreUpgrades{ - Added: newStores}) + Added: newStores, + }) } } diff --git a/tools/tools.go b/tools/tools.go index 226740cf4b..ea6a417e45 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -13,5 +13,5 @@ import ( _ "mvdan.cc/gofumpt" // unnamed import of statik for swagger UI support - _ "github.com/umee-network/umee/v4/swagger/statik" + _ "github.com/umee-network/umee/v5/swagger/statik" ) diff --git a/x/incentive/genesis.go b/x/incentive/genesis.go index d4f98f449d..6f39ba5dad 100644 --- a/x/incentive/genesis.go +++ b/x/incentive/genesis.go @@ -2,6 +2,7 @@ package incentive import ( "encoding/json" + "fmt" "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/codec" @@ -58,54 +59,98 @@ func (gs GenesisState) Validate() error { return ErrDecreaseLastRewardTime.Wrap("last reward time must not be negative") } - // TODO: enforce no duplicate (account,denom) + m := map[string]bool{} for _, rt := range gs.RewardTrackers { + // enforce no duplicate (account,denom) + s := rt.Account + rt.UToken + if err := noDuplicateString(m, s, "reward trackers"); err != nil { + return err + } if err := rt.Validate(); err != nil { return err } } - // TODO: enforce no duplicate denoms + m = map[string]bool{} for _, ra := range gs.RewardAccumulators { if err := ra.Validate(); err != nil { return err } + // enforce no duplicate denoms + s := ra.UToken + if err := noDuplicateString(m, s, "reward accumulators"); err != nil { + return err + } } - // TODO: enforce no duplicate program IDs + m = map[string]bool{} + // enforce no duplicate program IDs for _, up := range gs.UpcomingPrograms { if err := up.ValidatePassed(); err != nil { return err } + s := fmt.Sprintf("%d", up.ID) + if err := noDuplicateString(m, s, "upcoming program ID"); err != nil { + return err + } } for _, op := range gs.OngoingPrograms { if err := op.ValidatePassed(); err != nil { return err } + s := fmt.Sprintf("%d", op.ID) + if err := noDuplicateString(m, s, "ongoing program ID"); err != nil { + return err + } } for _, cp := range gs.CompletedPrograms { if err := cp.ValidatePassed(); err != nil { return err } + s := fmt.Sprintf("%d", cp.ID) + if err := noDuplicateString(m, s, "completed program ID"); err != nil { + return err + } } - // TODO: enforce no duplicate (account,denom) + m = map[string]bool{} for _, b := range gs.Bonds { if err := b.Validate(); err != nil { return err } + // enforce no duplicate (account,denom) + s := b.Account + b.UToken.Denom + if err := noDuplicateString(m, s, "bonds"); err != nil { + return err + } } - // TODO: enforce no duplicate (account,denom) + m = map[string]bool{} for _, au := range gs.AccountUnbondings { if err := au.Validate(); err != nil { return err } + // enforce no duplicate (account,denom) + s := au.Account + au.UToken + if err := noDuplicateString(m, s, "account unbondings"); err != nil { + return err + } } return nil } +// noDuplicateString checks to see if a string is already present in a map +// and then adds it to the map. If it was already present, an error is returned. +// used to check all uniqueness requirements in genesis state. +func noDuplicateString(m map[string]bool, s, errMsg string) error { + if _, found := m[s]; found { + return fmt.Errorf("duplicate %s: %s", errMsg, s) + } + m[s] = true + return nil +} + // GetGenesisStateFromAppState returns x/incentive GenesisState given raw application // genesis state. func GetGenesisStateFromAppState(cdc codec.JSONCodec, appState map[string]json.RawMessage) *GenesisState { @@ -174,7 +219,7 @@ func (ip IncentiveProgram) Validate() error { return errors.Wrapf(ErrInvalidProgramDuration, "%d", ip.Duration) } if ip.StartTime <= 0 { - return errors.Wrapf(ErrInvalidProgramStart, "%d", ip.Duration) + return ErrInvalidProgramStart.Wrapf("%d", ip.StartTime) } return nil @@ -238,6 +283,9 @@ func (rt RewardTracker) Validate() error { if _, err := sdk.AccAddressFromBech32(rt.Account); err != nil { return err } + if err := sdk.ValidateDenom(rt.UToken); err != nil { + return err + } if !leveragetypes.HasUTokenPrefix(rt.UToken) { return leveragetypes.ErrNotUToken.Wrap(rt.UToken) } @@ -262,6 +310,9 @@ func NewRewardAccumulator(uDenom string, exponent uint32, coins sdk.DecCoins) Re } func (ra RewardAccumulator) Validate() error { + if err := sdk.ValidateDenom(ra.UToken); err != nil { + return err + } if !leveragetypes.HasUTokenPrefix(ra.UToken) { return leveragetypes.ErrNotUToken.Wrap(ra.UToken) } @@ -289,6 +340,9 @@ func (u Unbonding) Validate() error { if u.End < u.Start { return ErrInvalidUnbonding.Wrap("start time > end time") } + if !leveragetypes.HasUTokenPrefix(u.UToken.Denom) { + return leveragetypes.ErrNotUToken.Wrap(u.UToken.Denom) + } return u.UToken.Validate() } @@ -305,6 +359,9 @@ func (au AccountUnbondings) Validate() error { if _, err := sdk.AccAddressFromBech32(au.Account); err != nil { return err } + if err := sdk.ValidateDenom(au.UToken); err != nil { + return err + } if !leveragetypes.HasUTokenPrefix(au.UToken) { return leveragetypes.ErrNotUToken.Wrap(au.UToken) } diff --git a/x/incentive/genesis_test.go b/x/incentive/genesis_test.go index 6434e828f2..8c4b17c4d8 100644 --- a/x/incentive/genesis_test.go +++ b/x/incentive/genesis_test.go @@ -3,10 +3,283 @@ package incentive import ( "testing" + sdk "github.com/cosmos/cosmos-sdk/types" "gotest.tools/v3/assert" + + "github.com/umee-network/umee/v5/util/coin" + leveragetypes "github.com/umee-network/umee/v5/x/leverage/types" ) +const uumee = "uumee" + func TestValidateGenesis(t *testing.T) { + t.Parallel() + + validAddr := "umee1s84d29zk3k20xk9f0hvczkax90l9t94g72n6wm" + genesis := DefaultGenesis() assert.NilError(t, genesis.Validate()) + + invalidParams := DefaultGenesis() + invalidParams.Params.EmergencyUnbondFee = sdk.MustNewDecFromStr("-0.01") + assert.ErrorContains(t, invalidParams.Validate(), "invalid emergency unbonding fee") + + zeroID := DefaultGenesis() + zeroID.NextProgramId = 0 + assert.ErrorIs(t, zeroID.Validate(), ErrInvalidProgramID) + + negativeTime := DefaultGenesis() + negativeTime.LastRewardsTime = -1 + assert.ErrorIs(t, negativeTime.Validate(), ErrDecreaseLastRewardTime) + + invalidRewardTracker := DefaultGenesis() + invalidRewardTracker.RewardTrackers = []RewardTracker{{}} + assert.ErrorContains(t, invalidRewardTracker.Validate(), "empty address string is not allowed") + + rt := RewardTracker{ + Account: validAddr, + UToken: coin.UumeeDenom, + Rewards: sdk.NewDecCoins(), + } + duplicateRewardTracker := DefaultGenesis() + duplicateRewardTracker.RewardTrackers = []RewardTracker{rt, rt} + assert.ErrorContains(t, duplicateRewardTracker.Validate(), "duplicate reward trackers") + + invalidRewardAccumulator := DefaultGenesis() + invalidRewardAccumulator.RewardAccumulators = []RewardAccumulator{{}} + assert.ErrorContains(t, invalidRewardAccumulator.Validate(), "invalid denom") + + ra := RewardAccumulator{ + UToken: coin.UumeeDenom, + Rewards: sdk.NewDecCoins(), + } + duplicateRewardAccumulator := DefaultGenesis() + duplicateRewardAccumulator.RewardAccumulators = []RewardAccumulator{ra, ra} + assert.ErrorContains(t, duplicateRewardAccumulator.Validate(), "duplicate reward accumulators") + + invalidProgram := IncentiveProgram{} + validProgram := NewIncentiveProgram(1, 1, 1, coin.UumeeDenom, coin.Umee1, coin.Zero(uumee), false) + + invalidUpcomingProgram := DefaultGenesis() + invalidUpcomingProgram.UpcomingPrograms = []IncentiveProgram{invalidProgram} + assert.ErrorIs(t, invalidUpcomingProgram.Validate(), ErrInvalidProgramID) + + duplicateUpcomingProgram := DefaultGenesis() + duplicateUpcomingProgram.UpcomingPrograms = []IncentiveProgram{validProgram, validProgram} + assert.ErrorContains(t, duplicateUpcomingProgram.Validate(), "duplicate upcoming program ID") + + invalidOngoingProgram := DefaultGenesis() + invalidOngoingProgram.OngoingPrograms = []IncentiveProgram{invalidProgram} + assert.ErrorIs(t, invalidOngoingProgram.Validate(), ErrInvalidProgramID) + + duplicateOngoingProgram := DefaultGenesis() + duplicateOngoingProgram.UpcomingPrograms = []IncentiveProgram{validProgram} + duplicateOngoingProgram.OngoingPrograms = []IncentiveProgram{validProgram} + assert.ErrorContains(t, duplicateOngoingProgram.Validate(), "duplicate ongoing program ID") + + invalidCompletedProgram := DefaultGenesis() + invalidCompletedProgram.CompletedPrograms = []IncentiveProgram{invalidProgram} + assert.ErrorIs(t, invalidCompletedProgram.Validate(), ErrInvalidProgramID) + + duplicateCompletedProgram := DefaultGenesis() + duplicateCompletedProgram.UpcomingPrograms = []IncentiveProgram{validProgram} + duplicateCompletedProgram.CompletedPrograms = []IncentiveProgram{validProgram} + assert.ErrorContains(t, duplicateCompletedProgram.Validate(), "duplicate completed program ID") + + invalidBond := DefaultGenesis() + invalidBond.Bonds = []Bond{{}} + assert.ErrorContains(t, invalidBond.Validate(), "empty address string is not allowed") + + b := Bond{ + Account: validAddr, + UToken: sdk.NewInt64Coin(coin.UumeeDenom, 1), + } + + duplicateBond := DefaultGenesis() + duplicateBond.Bonds = []Bond{b, b} + assert.ErrorContains(t, duplicateBond.Validate(), "duplicate bonds") + + invalidAccountUnbondings := DefaultGenesis() + invalidAccountUnbondings.AccountUnbondings = []AccountUnbondings{{}} + assert.ErrorContains(t, invalidAccountUnbondings.Validate(), "empty address string is not allowed") + + au := AccountUnbondings{ + Account: validAddr, + UToken: coin.UumeeDenom, + Unbondings: []Unbonding{}, + } + duplicateAccountUnbonding := DefaultGenesis() + duplicateAccountUnbonding.AccountUnbondings = []AccountUnbondings{au, au} + assert.ErrorContains(t, duplicateAccountUnbonding.Validate(), "duplicate account unbondings") +} + +func TestValidateIncentiveProgram(t *testing.T) { + t.Parallel() + + validProgram := NewIncentiveProgram(1, 1, 1, coin.UumeeDenom, coin.Umee1, coin.Zero(uumee), false) + assert.NilError(t, validProgram.Validate()) + + invalidUToken := validProgram + invalidUToken.UToken = "" + assert.ErrorContains(t, invalidUToken.Validate(), "invalid denom") + + invalidUToken.UToken = uumee + assert.ErrorIs(t, invalidUToken.Validate(), leveragetypes.ErrNotUToken) + + invalidTotalRewards := validProgram + invalidTotalRewards.TotalRewards = sdk.Coin{} + assert.ErrorContains(t, invalidTotalRewards.Validate(), "invalid denom") + + invalidTotalRewards.TotalRewards = coin.New(coin.UumeeDenom, 100) + assert.ErrorIs(t, invalidTotalRewards.Validate(), leveragetypes.ErrUToken) + + invalidTotalRewards.TotalRewards = coin.Zero(uumee) + assert.ErrorIs(t, invalidTotalRewards.Validate(), ErrProgramWithoutRewards) + + invalidRemainingRewards := validProgram + invalidRemainingRewards.RemainingRewards = sdk.Coin{} + assert.ErrorContains(t, invalidRemainingRewards.Validate(), "invalid denom") + + invalidRemainingRewards.RemainingRewards = coin.Zero("abcd") + assert.ErrorIs(t, invalidRemainingRewards.Validate(), ErrProgramRewardMismatch) + + invalidRemainingRewards.RemainingRewards = coin.Umee1 + assert.ErrorIs(t, invalidRemainingRewards.Validate(), ErrNonfundedProgramRewards) + + invalidDuration := validProgram + invalidDuration.Duration = 0 + assert.ErrorIs(t, invalidDuration.Validate(), ErrInvalidProgramDuration) + + invalidStartTime := validProgram + invalidStartTime.StartTime = 0 + assert.ErrorIs(t, invalidStartTime.Validate(), ErrInvalidProgramStart) + + // also test validateProposed, which is used for incentive programs in MsgGovCreatePrograms + assert.ErrorIs(t, validProgram.ValidateProposed(), ErrInvalidProgramID, "proposed with nonzero ID") + + validProposed := validProgram + validProposed.ID = 0 + assert.NilError(t, validProposed.ValidateProposed()) + + proposedRemainingRewards := validProposed + proposedRemainingRewards.RemainingRewards = coin.Umee1 + assert.ErrorIs(t, proposedRemainingRewards.ValidateProposed(), ErrNonzeroRemainingRewards, "proposed remaining rewards") + + invalidProposed := validProposed + invalidProposed.StartTime = 0 + assert.ErrorIs(t, invalidProposed.ValidateProposed(), ErrInvalidProgramStart, "proposed invalid program") + + proposedFunded := validProposed + proposedFunded.Funded = true + assert.ErrorIs(t, proposedFunded.ValidateProposed(), ErrProposedFundedProgram, "proposed funded program") + + // also test validatePassed, which is used for incentive programs in genesis state + assert.NilError(t, validProgram.ValidatePassed()) + assert.ErrorIs(t, validProposed.ValidatePassed(), ErrInvalidProgramID, "passed program with zero ID") + assert.ErrorIs(t, invalidStartTime.ValidatePassed(), ErrInvalidProgramStart, "passed invalid program") +} + +func TestValidateStructs(t *testing.T) { + validAddr := "umee1s84d29zk3k20xk9f0hvczkax90l9t94g72n6wm" + validBond := NewBond(validAddr, coin.New(coin.UumeeDenom, 1)) + assert.NilError(t, validBond.Validate()) + + invalidBond := validBond + invalidBond.Account = "" + assert.ErrorContains(t, invalidBond.Validate(), "empty address string is not allowed") + + invalidBond = validBond + invalidBond.UToken = sdk.Coin{} + assert.ErrorContains(t, invalidBond.Validate(), "invalid denom") + + invalidBond = validBond + invalidBond.UToken.Denom = uumee + assert.ErrorIs(t, invalidBond.Validate(), leveragetypes.ErrNotUToken) + + validTracker := NewRewardTracker(validAddr, coin.UumeeDenom, sdk.NewDecCoins( + sdk.NewDecCoin(uumee, sdk.OneInt()), + )) + assert.NilError(t, validTracker.Validate()) + + invalidTracker := validTracker + invalidTracker.UToken = "" + assert.ErrorContains(t, invalidTracker.Validate(), "invalid denom") + + invalidTracker.UToken = uumee + assert.ErrorIs(t, invalidTracker.Validate(), leveragetypes.ErrNotUToken) + + invalidTracker = validTracker + invalidTracker.Account = "" + assert.ErrorContains(t, invalidTracker.Validate(), "empty address string is not allowed") + + invalidTracker = validTracker + invalidTracker.Rewards[0].Denom = "" + assert.ErrorContains(t, invalidTracker.Validate(), "invalid denom") + + invalidTracker = validTracker + invalidTracker.Rewards[0].Denom = coin.UumeeDenom + assert.ErrorIs(t, invalidTracker.Validate(), leveragetypes.ErrUToken) + + validAccumulator := NewRewardAccumulator(coin.UumeeDenom, 6, sdk.NewDecCoins( + sdk.NewDecCoin(uumee, sdk.OneInt()), + )) + assert.NilError(t, validAccumulator.Validate()) + + invalidAccumulator := validAccumulator + invalidAccumulator.UToken = "" + assert.ErrorContains(t, invalidAccumulator.Validate(), "invalid denom") + + invalidAccumulator.UToken = uumee + assert.ErrorIs(t, invalidAccumulator.Validate(), leveragetypes.ErrNotUToken) + + invalidAccumulator = validAccumulator + invalidAccumulator.Rewards[0].Denom = "" + assert.ErrorContains(t, invalidAccumulator.Validate(), "invalid denom") + + invalidAccumulator = validAccumulator + invalidAccumulator.Rewards[0].Denom = coin.UumeeDenom + assert.ErrorIs(t, invalidAccumulator.Validate(), leveragetypes.ErrUToken) + + validUnbonding := NewUnbonding(1, 1, coin.New(coin.UumeeDenom, 1)) + assert.NilError(t, validUnbonding.Validate()) + + invalidUnbonding := validUnbonding + invalidUnbonding.End = 0 + assert.ErrorIs(t, invalidUnbonding.Validate(), ErrInvalidUnbonding) + + invalidUnbonding = validUnbonding + invalidUnbonding.UToken.Denom = uumee + assert.ErrorIs(t, invalidUnbonding.Validate(), leveragetypes.ErrNotUToken) + + invalidUnbonding = validUnbonding + invalidUnbonding.UToken = sdk.Coin{Denom: coin.UumeeDenom, Amount: sdk.NewInt(-1)} + assert.ErrorContains(t, invalidUnbonding.Validate(), "negative coin amount") + + validAccountUnbondings := NewAccountUnbondings(validAddr, coin.UumeeDenom, []Unbonding{validUnbonding}) + assert.NilError(t, validAccountUnbondings.Validate()) + + invalidAccountUnbondings := validAccountUnbondings + invalidAccountUnbondings.Account = "" + assert.ErrorContains(t, invalidAccountUnbondings.Validate(), "empty address") + + invalidAccountUnbondings = validAccountUnbondings + invalidAccountUnbondings.UToken = "" + assert.ErrorContains(t, invalidAccountUnbondings.Validate(), "invalid denom") + + invalidAccountUnbondings = validAccountUnbondings + invalidAccountUnbondings.UToken = uumee + assert.ErrorIs(t, invalidAccountUnbondings.Validate(), leveragetypes.ErrNotUToken) + + invalidAccountUnbondings = validAccountUnbondings + invalidAccountUnbondings.Unbondings[0].UToken.Denom = uumee + assert.ErrorContains(t, invalidAccountUnbondings.Validate(), "does not match") + + invalidAccountUnbondings = validAccountUnbondings + invalidAccountUnbondings.Unbondings[0].End = 0 + assert.ErrorIs(t, invalidAccountUnbondings.Validate(), ErrInvalidUnbonding) + invalidAccountUnbondings.Unbondings[0] = validUnbonding // the value in validAccountUnbondings was modified + + invalidAccountUnbondings = validAccountUnbondings + invalidAccountUnbondings.Unbondings[0].UToken = sdk.Coin{Denom: coin.UumeeDenom, Amount: sdk.NewInt(-1)} + assert.ErrorContains(t, invalidAccountUnbondings.Validate(), "negative coin amount") } diff --git a/x/incentive/keeper/bond_test.go b/x/incentive/keeper/bond_test.go new file mode 100644 index 0000000000..aa550e6b84 --- /dev/null +++ b/x/incentive/keeper/bond_test.go @@ -0,0 +1,45 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/umee-network/umee/v5/util/coin" +) + +func TestBonds(t *testing.T) { + t.Parallel() + k := newTestKeeper(t) + ctx := k.ctx + require := require.New(t) + + // init a supplier with bonded uTokens, and some currently unbonding + alice := k.newBondedAccount( + coin.New(uUmee, 100_000000), + coin.New(uAtom, 20_000000), + ) + k.mustBeginUnbond(alice, coin.New(uUmee, 10_000000)) + k.mustBeginUnbond(alice, coin.New(uUmee, 5_000000)) + + // restricted collateral counts bonded and unbonding uTokens + restricted := k.restrictedCollateral(ctx, alice, uUmee) + require.Equal(coin.New(uUmee, 100_000000), restricted) + restricted = k.restrictedCollateral(ctx, alice, uAtom) + require.Equal(coin.New(uAtom, 20_000000), restricted) + + // bond summary + bonded, unbonding, unbondings := k.BondSummary(ctx, alice, uUmee) + require.Equal(coin.New(uUmee, 85_000000), bonded) + require.Equal(coin.New(uUmee, 15_000000), unbonding) + require.Equal(2, len(unbondings)) + bonded, unbonding, unbondings = k.BondSummary(ctx, alice, uAtom) + require.Equal(coin.New(uAtom, 20_000000), bonded) + require.Equal(coin.Zero(uAtom), unbonding) + require.Equal(0, len(unbondings)) + + // decreaseBond is an internal function that instantly unbonds uTokens + err := k.decreaseBond(ctx, alice, coin.New(uAtom, 15_000000)) + require.NoError(err) + require.Equal(coin.New(uAtom, 5_000000), k.GetBonded(ctx, alice, uAtom)) +} diff --git a/x/incentive/keeper/genesis.go b/x/incentive/keeper/genesis.go index 46956c8fc2..32cb8f02a5 100644 --- a/x/incentive/keeper/genesis.go +++ b/x/incentive/keeper/genesis.go @@ -8,6 +8,10 @@ import ( // InitGenesis initializes the x/incentive module state from a provided genesis state. func (k Keeper) InitGenesis(ctx sdk.Context, gs incentive.GenesisState) { + if err := gs.Validate(); err != nil { + panic(err) + } + if err := k.setParams(ctx, gs.Params); err != nil { panic(err) } diff --git a/x/incentive/keeper/genesis_test.go b/x/incentive/keeper/genesis_test.go new file mode 100644 index 0000000000..a4b73351db --- /dev/null +++ b/x/incentive/keeper/genesis_test.go @@ -0,0 +1,25 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenesis(t *testing.T) { + t.Parallel() + k := newTestKeeper(t) + + // create a complex genesis state by running transactions + _ = k.initScenario1() + + // get genesis state after this scenario + gs1 := k.ExportGenesis(k.ctx) + + // require import-export idempotency on a fresh keeper + k2 := newTestKeeper(t) + k2.InitGenesis(k2.ctx, *gs1) + gs2 := k2.ExportGenesis(k2.ctx) + + require.Equal(t, gs1, gs2, "genesis states equal") +} diff --git a/x/incentive/keeper/grpc_query_test.go b/x/incentive/keeper/grpc_query_test.go index 6e9818cde6..10551ae020 100644 --- a/x/incentive/keeper/grpc_query_test.go +++ b/x/incentive/keeper/grpc_query_test.go @@ -8,10 +8,154 @@ import ( "github.com/umee-network/umee/v5/util/coin" "github.com/umee-network/umee/v5/x/incentive" - "github.com/umee-network/umee/v5/x/leverage/fixtures" ) +func TestQueries(t *testing.T) { + t.Parallel() + k := newTestKeeper(t) + q := Querier{k.Keeper} + + alice := k.initScenario1() + + expect1 := &incentive.QueryAccountBondsResponse{ + Bonded: sdk.NewCoins( + coin.New(uUmee, 90_000000), + coin.New(uAtom, 45_000000), + ), + Unbonding: sdk.NewCoins( + coin.New(uUmee, 10_000000), + coin.New(uAtom, 5_000000), + ), + Unbondings: []incentive.Unbonding{ + { + Start: 90, + End: 86490, + UToken: coin.New(uAtom, 5_000000), + }, + { + Start: 90, + End: 86490, + UToken: coin.New(uUmee, 5_000000), + }, + { + Start: 90, + End: 86490, + UToken: coin.New(uUmee, 5_000000), + }, + }, + } + resp1, err := q.AccountBonds(k.ctx, &incentive.QueryAccountBonds{ + Address: alice.String(), + }) + require.NoError(t, err) + require.Equal(t, expect1, resp1, "account bonds query") + + expect2 := &incentive.QueryPendingRewardsResponse{ + Rewards: sdk.NewCoins( + coin.New(umee, 13_333332), + ), + } + resp2, err := q.PendingRewards(k.ctx, &incentive.QueryPendingRewards{ + Address: alice.String(), + }) + require.NoError(t, err) + require.Equal(t, expect2, resp2, "pending rewards query") + + expect3 := &incentive.QueryTotalBondedResponse{ + Bonded: sdk.NewCoins( + coin.New(uUmee, 90_000000), + coin.New(uAtom, 45_000000), + ), + } + resp3, err := q.TotalBonded(k.ctx, &incentive.QueryTotalBonded{}) + require.NoError(t, err) + require.Equal(t, expect3, resp3, "total bonded query (all denoms)") + + expect4 := &incentive.QueryTotalBondedResponse{ + Bonded: sdk.NewCoins( + coin.New(uUmee, 90_000000), + ), + } + resp4, err := q.TotalBonded(k.ctx, &incentive.QueryTotalBonded{ + Denom: uUmee, + }) + require.NoError(t, err) + require.Equal(t, expect4, resp4, "total bonded query (one denom)") + + expect5 := &incentive.QueryTotalUnbondingResponse{ + Unbonding: sdk.NewCoins( + coin.New(uUmee, 10_000000), + coin.New(uAtom, 5_000000), + ), + } + resp5, err := q.TotalUnbonding(k.ctx, &incentive.QueryTotalUnbonding{}) + require.NoError(t, err) + require.Equal(t, expect5, resp5, "total unbonding query (all denoms)") + + expect6 := &incentive.QueryTotalUnbondingResponse{ + Unbonding: sdk.NewCoins( + coin.New(uUmee, 10_000000), + ), + } + resp6, err := q.TotalUnbonding(k.ctx, &incentive.QueryTotalUnbonding{ + Denom: uUmee, + }) + require.NoError(t, err) + require.Equal(t, expect6, resp6, "total unbonding query (one denom)") + + expect7 := &incentive.QueryLastRewardTimeResponse{ + Time: 100, + } + resp7, err := q.LastRewardTime(k.ctx, &incentive.QueryLastRewardTime{}) + require.NoError(t, err) + require.Equal(t, expect7, resp7, "last reward time query") + + expect8 := &incentive.QueryParamsResponse{ + Params: k.GetParams(k.ctx), + } + resp8, err := q.Params(k.ctx, &incentive.QueryParams{}) + require.NoError(t, err) + require.Equal(t, expect8, resp8, "params query") + + programs, err := k.getAllIncentivePrograms(k.ctx, incentive.ProgramStatusUpcoming) + require.NoError(t, err) + expect9 := &incentive.QueryUpcomingIncentiveProgramsResponse{ + Programs: programs, + } + resp9, err := q.UpcomingIncentivePrograms(k.ctx, &incentive.QueryUpcomingIncentivePrograms{}) + require.NoError(t, err) + require.Equal(t, expect9, resp9, "upcoming programs query") + + programs, err = k.getAllIncentivePrograms(k.ctx, incentive.ProgramStatusOngoing) + require.NoError(t, err) + expect10 := &incentive.QueryOngoingIncentiveProgramsResponse{ + Programs: programs, + } + resp10, err := q.OngoingIncentivePrograms(k.ctx, &incentive.QueryOngoingIncentivePrograms{}) + require.NoError(t, err) + require.Equal(t, expect10, resp10, "ongoing programs query") + + programs, err = k.getAllIncentivePrograms(k.ctx, incentive.ProgramStatusCompleted) + require.NoError(t, err) + expect11 := &incentive.QueryCompletedIncentiveProgramsResponse{ + Programs: programs, + } + resp11, err := q.CompletedIncentivePrograms(k.ctx, &incentive.QueryCompletedIncentivePrograms{}) + require.NoError(t, err) + require.Equal(t, expect11, resp11, "completed programs query") + + program, _, err := k.getIncentiveProgram(k.ctx, 1) + require.NoError(t, err) + expect12 := &incentive.QueryIncentiveProgramResponse{ + Program: program, + } + resp12, err := q.IncentiveProgram(k.ctx, &incentive.QueryIncentiveProgram{Id: 1}) + require.NoError(t, err) + require.Equal(t, expect12, resp12, "incentive program query") +} + func TestAPYQuery(t *testing.T) { + t.Parallel() k := newTestKeeper(t) q := Querier{k.Keeper} k.initCommunityFund( @@ -21,28 +165,28 @@ func TestAPYQuery(t *testing.T) { // init a supplier with bonded uTokens _ = k.newBondedAccount( - coin.New("u/"+fixtures.UmeeDenom, 100_000000), + coin.New(uUmee, 100_000000), ) // create three incentive programs, each of which will run for half a year but which will // start at slightly different times so we can test each one's contribution to total APY - k.addIncentiveProgram(u_umee, 100, 15778800, sdk.NewInt64Coin(umee, 10_000000), true) - k.addIncentiveProgram(u_umee, 120, 15778800, sdk.NewInt64Coin(umee, 30_000000), true) - k.addIncentiveProgram(u_umee, 140, 15778800, sdk.NewInt64Coin(atom, 10_000000), true) + k.addIncentiveProgram(uUmee, 100, 15778800, sdk.NewInt64Coin(umee, 10_000000), true) + k.addIncentiveProgram(uUmee, 120, 15778800, sdk.NewInt64Coin(umee, 30_000000), true) + k.addIncentiveProgram(uUmee, 140, 15778800, sdk.NewInt64Coin(atom, 10_000000), true) // Advance last rewards time to 100, thus starting the first program k.advanceTimeTo(100) - req1 := incentive.QueryCurrentRates{UToken: u_atom} + req1 := incentive.QueryCurrentRates{UToken: uAtom} expect1 := &incentive.QueryCurrentRatesResponse{ - ReferenceBond: coin.New(u_atom, 1), // zero exponent because this asset has never been incentivized + ReferenceBond: coin.New(uAtom, 1), // zero exponent because this asset has never been incentivized Rewards: sdk.NewCoins(), } resp1, err := q.CurrentRates(k.ctx, &req1) require.NoError(t, err) require.Equal(t, expect1, resp1, "zero token rates for bonded atom") - req2 := incentive.QueryActualRates{UToken: u_atom} + req2 := incentive.QueryActualRates{UToken: uAtom} expect2 := &incentive.QueryActualRatesResponse{ APY: sdk.ZeroDec(), } @@ -50,9 +194,9 @@ func TestAPYQuery(t *testing.T) { require.NoError(t, err) require.Equal(t, expect2, resp2, "zero USD rates for bonded atom") - req3 := incentive.QueryCurrentRates{UToken: u_umee} + req3 := incentive.QueryCurrentRates{UToken: uUmee} expect3 := &incentive.QueryCurrentRatesResponse{ - ReferenceBond: coin.New(u_umee, 1_000000), // exponent = 6 due to proper initialization + ReferenceBond: coin.New(uUmee, 1_000000), // exponent = 6 due to proper initialization Rewards: sdk.NewCoins( coin.New(umee, 200_000), // 10 UMEE per 100 u/UMEE bonded, per half year, is 20% per year ), @@ -61,7 +205,7 @@ func TestAPYQuery(t *testing.T) { require.NoError(t, err) require.Equal(t, expect3, resp3, "nonzero token rates for bonded umee") - req4 := incentive.QueryActualRates{UToken: u_umee} + req4 := incentive.QueryActualRates{UToken: uUmee} expect4 := &incentive.QueryActualRatesResponse{ APY: sdk.MustNewDecFromStr("0.2"), } @@ -72,9 +216,9 @@ func TestAPYQuery(t *testing.T) { // Advance last rewards time to 120, thus starting the second program and quadrupling APY k.advanceTimeTo(120) - req5 := incentive.QueryCurrentRates{UToken: u_umee} + req5 := incentive.QueryCurrentRates{UToken: uUmee} expect5 := &incentive.QueryCurrentRatesResponse{ - ReferenceBond: coin.New(u_umee, 1_000000), + ReferenceBond: coin.New(uUmee, 1_000000), Rewards: sdk.NewCoins( coin.New(umee, 800_000), // 40 UMEE per 100 u/UMEE bonded, per half year, is 80% per year ), @@ -83,7 +227,7 @@ func TestAPYQuery(t *testing.T) { require.NoError(t, err) require.Equal(t, expect5, resp5, "increased token rates for bonded umee") - req6 := incentive.QueryActualRates{UToken: u_umee} + req6 := incentive.QueryActualRates{UToken: uUmee} expect6 := &incentive.QueryActualRatesResponse{ APY: sdk.MustNewDecFromStr("0.8"), } @@ -95,9 +239,9 @@ func TestAPYQuery(t *testing.T) { // the ratio of umee and atom prices (very high) to the existing APY k.advanceTimeTo(140) - req7 := incentive.QueryCurrentRates{UToken: u_umee} + req7 := incentive.QueryCurrentRates{UToken: uUmee} expect7 := &incentive.QueryCurrentRatesResponse{ - ReferenceBond: coin.New(u_umee, 1_000000), + ReferenceBond: coin.New(uUmee, 1_000000), Rewards: sdk.NewCoins( coin.New(umee, 800_000), // 40 UMEE per 100 u/UMEE bonded, per half year, is 80% per year coin.New(atom, 200_000), // 10 ATOM per 100 u/UMEE bonded, per half year @@ -107,7 +251,7 @@ func TestAPYQuery(t *testing.T) { require.NoError(t, err) require.Equal(t, expect7, resp7, "multi-token rates for bonded umee") - req8 := incentive.QueryActualRates{UToken: u_umee} + req8 := incentive.QueryActualRates{UToken: uUmee} expect8 := &incentive.QueryActualRatesResponse{ APY: sdk.MustNewDecFromStr("2.670783847980997625"), // a large but complicated APY due to price ratio } diff --git a/x/incentive/keeper/hooks_test.go b/x/incentive/keeper/hooks_test.go new file mode 100644 index 0000000000..73f070abe7 --- /dev/null +++ b/x/incentive/keeper/hooks_test.go @@ -0,0 +1,73 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/umee-network/umee/v5/util/coin" + "github.com/umee-network/umee/v5/x/incentive" +) + +func TestHooks(t *testing.T) { + t.Parallel() + k := newTestKeeper(t) + require := require.New(t) + + // create a complex genesis state by running transactions + alice := k.initScenario1() + + h := k.BondHooks() + + require.Equal(sdk.NewInt(100_000000), h.GetBonded(k.ctx, alice, uUmee), "initial restricted collateral") + require.NoError(h.ForceUnbondTo(k.ctx, alice, coin.New(uUmee, 200_000000)), "liquidation unbond with no effect") + require.Equal(sdk.NewInt(100_000000), h.GetBonded(k.ctx, alice, uUmee), "unchanged restricted collateral") + + // verify scenario 1 state is still unchanged by liquidation + bonded, unbonding, unbondings := k.BondSummary(k.ctx, alice, uUmee) + require.Equal(coin.New(uUmee, 90_000000), bonded) + require.Equal(coin.New(uUmee, 10_000000), unbonding) + require.Equal([]incentive.Unbonding{ + incentive.NewUnbonding(90, 86490, coin.New(uUmee, 5_000000)), + incentive.NewUnbonding(90, 86490, coin.New(uUmee, 5_000000)), + }, unbondings) + + // reduce a single in-progress unbonding by liquidation + require.NoError(h.ForceUnbondTo(k.ctx, alice, coin.New(uUmee, 96_000000)), "liquidation unbond 1") + require.Equal(sdk.NewInt(96_000000), h.GetBonded(k.ctx, alice, uUmee)) + bonded, unbonding, unbondings = k.BondSummary(k.ctx, alice, uUmee) + require.Equal(coin.New(uUmee, 90_000000), bonded) + require.Equal(coin.New(uUmee, 6_000000), unbonding) + require.Equal([]incentive.Unbonding{ + incentive.NewUnbonding(90, 86490, coin.New(uUmee, 1_000000)), + incentive.NewUnbonding(90, 86490, coin.New(uUmee, 5_000000)), + }, unbondings) + + // reduce two in-progress unbondings by liquidation (one is ended altogether) + require.NoError(h.ForceUnbondTo(k.ctx, alice, coin.New(uUmee, 92_000000)), "liquidation unbond 2") + require.Equal(sdk.NewInt(92_000000), h.GetBonded(k.ctx, alice, uUmee)) + bonded, unbonding, unbondings = k.BondSummary(k.ctx, alice, uUmee) + require.Equal(coin.New(uUmee, 90_000000), bonded) + require.Equal(coin.New(uUmee, 2_000000), unbonding) + require.Equal([]incentive.Unbonding{ + incentive.NewUnbonding(90, 86490, coin.New(uUmee, 2_000000)), + }, unbondings) + + // end all unbondings and reduce bonded amount by liquidation + require.NoError(h.ForceUnbondTo(k.ctx, alice, coin.New(uUmee, 46_000000)), "liquidation unbond 3") + require.Equal(sdk.NewInt(46_000000), h.GetBonded(k.ctx, alice, uUmee)) + bonded, unbonding, unbondings = k.BondSummary(k.ctx, alice, uUmee) + require.Equal(coin.New(uUmee, 46_000000), bonded) + require.Equal(coin.Zero(uUmee), unbonding) + require.Equal([]incentive.Unbonding{}, unbondings) + + // clear bonds by liquidation + require.NoError(h.ForceUnbondTo(k.ctx, alice, coin.Zero(uUmee)), "liquidation unbond to zero") + require.Equal(sdk.ZeroInt(), h.GetBonded(k.ctx, alice, uUmee)) + bonded, unbonding, unbondings = k.BondSummary(k.ctx, alice, uUmee) + require.Equal(coin.Zero(uUmee), bonded) + require.Equal(coin.Zero(uUmee), unbonding) + require.Equal([]incentive.Unbonding{}, unbondings) +} diff --git a/x/incentive/keeper/mock_test.go b/x/incentive/keeper/mock_test.go index 5b1b79eaee..423e1b758c 100644 --- a/x/incentive/keeper/mock_test.go +++ b/x/incentive/keeper/mock_test.go @@ -124,13 +124,14 @@ type mockLeverageKeeper struct { // collateral[address] = coins collateral map[string]sdk.Coins // to test emergency unbondings - donatedCollateral sdk.Coins + donatedCollateral *sdk.Coins } func newMockLeverageKeeper() mockLeverageKeeper { + c := sdk.NewCoins() m := mockLeverageKeeper{ collateral: map[string]sdk.Coins{}, - donatedCollateral: sdk.NewCoins(), + donatedCollateral: &c, } return m } @@ -148,7 +149,7 @@ func (m *mockLeverageKeeper) GetCollateral(_ sdk.Context, addr sdk.AccAddress, d func (m *mockLeverageKeeper) DonateCollateral(ctx sdk.Context, addr sdk.AccAddress, uToken sdk.Coin) error { newCollateral := m.GetCollateral(ctx, addr, uToken.Denom).Sub(uToken).Amount.Int64() m.setCollateral(addr, uToken.Denom, newCollateral) - m.donatedCollateral = m.donatedCollateral.Add(uToken) + *m.donatedCollateral = m.donatedCollateral.Add(uToken) return nil } @@ -195,7 +196,7 @@ func (m *mockLeverageKeeper) GetTokenSettings(_ sdk.Context, denom string) (leve // TotalTokenValue implements the expected leverage keeper, with UMEE, ATOM, and DAI registered. func (m *mockLeverageKeeper) TotalTokenValue(_ sdk.Context, coins sdk.Coins, _ leveragetypes.PriceMode) (sdk.Dec, error) { var ( - total = sdk.ZeroDec() + total = sdk.ZeroDec() umeePrice = sdk.MustNewDecFromStr("4.21") atomPrice = sdk.MustNewDecFromStr("39.38") daiPrice = sdk.MustNewDecFromStr("1.00") diff --git a/x/incentive/keeper/msg_server.go b/x/incentive/keeper/msg_server.go index a818073552..b38d228394 100644 --- a/x/incentive/keeper/msg_server.go +++ b/x/incentive/keeper/msg_server.go @@ -131,7 +131,7 @@ func (s msgServer) EmergencyUnbond( return nil, err } - maxEmergencyUnbond := k.restrictedCollateral(ctx, addr, msg.UToken.Denom) + maxEmergencyUnbond := k.restrictedCollateral(ctx, addr, denom) // reject emergency unbondings greater than maximum available amount if msg.UToken.Amount.GT(maxEmergencyUnbond.Amount) { diff --git a/x/incentive/keeper/msg_server_test.go b/x/incentive/keeper/msg_server_test.go index 489725642f..0a0b4fae28 100644 --- a/x/incentive/keeper/msg_server_test.go +++ b/x/incentive/keeper/msg_server_test.go @@ -13,6 +13,7 @@ import ( ) func TestMsgBond(t *testing.T) { + t.Parallel() k := newTestKeeper(t) const ( @@ -97,6 +98,7 @@ func TestMsgBond(t *testing.T) { } func TestMsgBeginUnbonding(t *testing.T) { + t.Parallel() k := newTestKeeper(t) const ( @@ -193,6 +195,7 @@ func TestMsgBeginUnbonding(t *testing.T) { } func TestMsgEmergencyUnbond(t *testing.T) { + t.Parallel() k := newTestKeeper(t) const ( @@ -206,8 +209,8 @@ func TestMsgEmergencyUnbond(t *testing.T) { // having 50u/uumee collateral. No tokens ot uTokens are actually minted. // bond those uTokens. umeeSupplier := k.newAccount() - k.lk.setCollateral(umeeSupplier, uumee, 50) - k.mustBond(umeeSupplier, coin.New(uumee, 50)) + k.lk.setCollateral(umeeSupplier, uumee, 50_000000) + k.mustBond(umeeSupplier, coin.New(uumee, 50_000000)) // create an additional account which has supplied an unregistered denom // which nonetheless has a uToken prefix. Bond those utokens. @@ -231,23 +234,23 @@ func TestMsgEmergencyUnbond(t *testing.T) { _, err = k.msrv.EmergencyUnbond(k.ctx, msg) require.ErrorIs(t, err, leveragetypes.ErrNotUToken) - // attempt to emergency unbond 10 u/uumee out of 50 available + // attempt to emergency unbond 10 u/UMEE out of 50 available msg = &incentive.MsgEmergencyUnbond{ Account: umeeSupplier.String(), - UToken: coin.New(uumee, 10), + UToken: coin.New(uumee, 10_000000), } _, err = k.msrv.EmergencyUnbond(k.ctx, msg) require.Nil(t, err, "emergency unbond 10") - // attempt to emergency unbond 50 u/uumee more (only 40 available) + // attempt to emergency unbond 50 u/UMEE more (only 40 available) msg = &incentive.MsgEmergencyUnbond{ Account: umeeSupplier.String(), - UToken: coin.New(uumee, 50), + UToken: coin.New(uumee, 50_000000), } _, err = k.msrv.EmergencyUnbond(k.ctx, msg) require.ErrorIs(t, err, incentive.ErrInsufficientBonded, "emergency unbond 50") - // attempt to emergency unbond 50 u/atom but from the wrong account + // attempt to emergency unbond 50 u/uatom but from the wrong account msg = &incentive.MsgEmergencyUnbond{ Account: umeeSupplier.String(), UToken: coin.New(uatom, 50), @@ -255,7 +258,7 @@ func TestMsgEmergencyUnbond(t *testing.T) { _, err = k.msrv.EmergencyUnbond(k.ctx, msg) require.ErrorIs(t, err, incentive.ErrInsufficientBonded, "emergency unbond 50 unknown (wrong account)") - // attempt to emergency unbond 50 u/atom but from the correct account + // attempt to emergency unbond 50 u/uatom but from the correct account msg = &incentive.MsgEmergencyUnbond{ Account: atomSupplier.String(), UToken: coin.New(uatom, 50), @@ -266,21 +269,23 @@ func TestMsgEmergencyUnbond(t *testing.T) { // attempt a large number of emergency unbondings which would hit MaxUnbondings if they were not instant msg = &incentive.MsgEmergencyUnbond{ Account: umeeSupplier.String(), - UToken: coin.New(uumee, 1), + UToken: coin.New(uumee, 1_000000), } // 9 more emergency unbondings of u/uumee on this account, which would reach the default maximum of 10 if not instant for i := 1; i < 10; i++ { _, err = k.msrv.EmergencyUnbond(k.ctx, msg) require.Nil(t, err, "repeat emergency unbond 1") } - // this would exceed max unbondings, but because the unbondings are instant, it does not + // this would exceed max unbondings, but because emergency unbondings are instant, it does not _, err = k.msrv.EmergencyUnbond(k.ctx, msg) require.Nil(t, err, "emergency unbond does is not restricted by max unbondings") - // TODO: confirm donated collateral amounts using mock leverage keeper + // verify that the fees were actually donated + require.Equal(t, coin.New(uUmee, 200000), k.lk.getDonatedCollateral(k.ctx, uUmee)) } func TestMsgSponsor(t *testing.T) { + t.Parallel() k := newTestKeeper(t) const ( @@ -341,6 +346,7 @@ func TestMsgSponsor(t *testing.T) { } func TestMsgGovSetParams(t *testing.T) { + t.Parallel() k := newTestKeeper(t) govAccAddr := "govAcct" @@ -378,6 +384,7 @@ func TestMsgGovSetParams(t *testing.T) { } func TestMsgGovCreatePrograms(t *testing.T) { + t.Parallel() k := newTestKeeper(t) const ( @@ -424,7 +431,7 @@ func TestMsgGovCreatePrograms(t *testing.T) { invalidProgram := validProgram invalidProgram.ID = 1 invalidMsg := &incentive.MsgGovCreatePrograms{ - Authority: "", + Authority: govAccAddr, Programs: []incentive.IncentiveProgram{invalidProgram}, FromCommunityFund: true, } @@ -433,6 +440,13 @@ func TestMsgGovCreatePrograms(t *testing.T) { require.ErrorIs(t, err, incentive.ErrInvalidProgramID, "set invalid program") require.Equal(t, uint32(3), k.getNextProgramID(k.ctx), "next ID after 2 programs passed an 1 failed") - // TODO: messages with multiple programs, including partially invalid - // and checking exact equality with upcoming programs set + // a message with both valid and invalid programs + complexMsg := &incentive.MsgGovCreatePrograms{ + Authority: govAccAddr, + Programs: []incentive.IncentiveProgram{validProgram, invalidProgram}, + FromCommunityFund: true, + } + // program should fail to be added, and nextID is unchanged + _, err = k.msrv.GovCreatePrograms(k.ctx, complexMsg) + require.ErrorIs(t, err, incentive.ErrInvalidProgramID, "set valid and invalid program") } diff --git a/x/incentive/keeper/scenario_test.go b/x/incentive/keeper/scenario_test.go index 4a49a6924d..02c6eb5912 100644 --- a/x/incentive/keeper/scenario_test.go +++ b/x/incentive/keeper/scenario_test.go @@ -13,13 +13,14 @@ import ( ) const ( - umee = fixtures.UmeeDenom - atom = fixtures.AtomDenom - u_umee = leveragetypes.UTokenPrefix + fixtures.UmeeDenom - u_atom = leveragetypes.UTokenPrefix + fixtures.AtomDenom + umee = fixtures.UmeeDenom + atom = fixtures.AtomDenom + uUmee = leveragetypes.UTokenPrefix + fixtures.UmeeDenom + uAtom = leveragetypes.UTokenPrefix + fixtures.AtomDenom ) func TestBasicIncentivePrograms(t *testing.T) { + t.Parallel() k := newTestKeeper(t) // init a community fund with 1000 UMEE and 10 ATOM available for funding @@ -36,15 +37,15 @@ func TestBasicIncentivePrograms(t *testing.T) { // init a supplier with bonded uTokens alice := k.newBondedAccount( - coin.New("u/"+fixtures.UmeeDenom, 100_000000), + coin.New(uUmee, 100_000000), ) // create three separate programs for 10UMEE, which will run for 100 seconds // one is funded by the community fund, and two are not. The non-community ones are start later than the first. // The first non-community-funded program will not be sponsored, and should thus be cancelled and create no rewards. - k.addIncentiveProgram(u_umee, 100, 100, sdk.NewInt64Coin(umee, 10_000000), true) - k.addIncentiveProgram(u_umee, 120, 120, sdk.NewInt64Coin(umee, 10_000000), false) - k.addIncentiveProgram(u_umee, 120, 120, sdk.NewInt64Coin(umee, 10_000000), false) + k.addIncentiveProgram(uUmee, 100, 100, sdk.NewInt64Coin(umee, 10_000000), true) + k.addIncentiveProgram(uUmee, 120, 120, sdk.NewInt64Coin(umee, 10_000000), false) + k.addIncentiveProgram(uUmee, 120, 120, sdk.NewInt64Coin(umee, 10_000000), false) // verify all 3 programs added programs, err := k.getAllIncentivePrograms(k.ctx, incentive.ProgramStatusUpcoming) @@ -86,8 +87,8 @@ func TestBasicIncentivePrograms(t *testing.T) { // init a second supplier with bonded uTokens - but he was not present during updateRewards bob := k.newBondedAccount( - coin.New(u_umee, 25_000000), - coin.New(u_atom, 8_000000), + coin.New(uUmee, 25_000000), + coin.New(uAtom, 8_000000), ) // From 100000 rewards distributed, 100% went to alice and 0% went to bob. @@ -208,6 +209,7 @@ func TestBasicIncentivePrograms(t *testing.T) { } func TestZeroBonded(t *testing.T) { + t.Parallel() k := newTestKeeper(t) k.initCommunityFund( coin.New(umee, 1000_000000), @@ -219,7 +221,7 @@ func TestZeroBonded(t *testing.T) { // the remaining time.) programStart := int64(100) - k.addIncentiveProgram(u_umee, programStart, 100, sdk.NewInt64Coin(umee, 10_000000), true) + k.addIncentiveProgram(uUmee, programStart, 100, sdk.NewInt64Coin(umee, 10_000000), true) k.advanceTimeTo(programStart) // starts program, but does not attempt rewards. Do not combine with next line. k.advanceTimeTo(programStart + 50) require.Equal(t, incentive.ProgramStatusOngoing, k.programStatus(1), "program 1 status (time 150)") @@ -228,7 +230,7 @@ func TestZeroBonded(t *testing.T) { // now create a supplier with bonded tokens, halfway through the program k.newBondedAccount( - coin.New(u_umee, 100_000000), + coin.New(uUmee, 100_000000), ) k.advanceTimeTo(programStart + 75) program = k.getProgram(1) diff --git a/x/incentive/keeper/unbonding.go b/x/incentive/keeper/unbonding.go index e372a6d989..989026db9f 100644 --- a/x/incentive/keeper/unbonding.go +++ b/x/incentive/keeper/unbonding.go @@ -98,11 +98,11 @@ func (k Keeper) reduceBondTo(ctx sdk.Context, addr sdk.AccAddress, newCollateral // if we have not returned yet, the only some in-progress unbondings will be // instantly unbonded. amountToUnbond := bonded.Amount.Add(unbonding.Amount).Sub(newCollateral.Amount) - for _, u := range unbondings { + for i, u := range unbondings { // for ongoing unbondings, starting with the oldest specificReduction := sdk.MinInt(amountToUnbond, u.UToken.Amount) // reduce the in-progress unbonding amount, and the remaining instant unbond - u.UToken.Amount = u.UToken.Amount.Sub(specificReduction) + unbondings[i].UToken.Amount = u.UToken.Amount.Sub(specificReduction) amountToUnbond = amountToUnbond.Sub(specificReduction) // if no more unbondings need to be reduced, break out of the loop early if amountToUnbond.IsZero() { diff --git a/x/incentive/keeper/unit_test.go b/x/incentive/keeper/unit_test.go index 2a4d3eb1df..4951835fea 100644 --- a/x/incentive/keeper/unit_test.go +++ b/x/incentive/keeper/unit_test.go @@ -90,6 +90,27 @@ func (k *testKeeper) mustBond(addr sdk.AccAddress, coins ...sdk.Coin) { } } +// mustClaim claims rewards for an account and requires no errors. Use when setting up incentive scenarios. +func (k *testKeeper) mustClaim(addr sdk.AccAddress) { + msg := &incentive.MsgClaim{ + Account: addr.String(), + } + _, err := k.msrv.Claim(k.ctx, msg) + require.NoError(k.t, err, "claim") +} + +// mustBeginUnbond unbonds utokens from an account and requires no errors. Use when setting up incentive scenarios. +func (k *testKeeper) mustBeginUnbond(addr sdk.AccAddress, coins ...sdk.Coin) { + for _, coin := range coins { + msg := &incentive.MsgBeginUnbonding{ + Account: addr.String(), + UToken: coin, + } + _, err := k.msrv.BeginUnbonding(k.ctx, msg) + require.NoError(k.t, err, "begin unbonding") + } +} + // initCommunityFund funds the mock bank keeper's distribution module account with some tokens func (k *testKeeper) initCommunityFund(funds ...sdk.Coin) { k.bk.FundModule(disttypes.ModuleName, funds) @@ -184,3 +205,37 @@ func (k *testKeeper) programFunded(programID uint32) bool { } return program.Funded } + +// initScenario1 creates a scenario with upcoming, ongoing, and completed incentive +// programs as well as a bonded account with ongoing unbondings and both claimed +// and pending rewards. Creates a complex state for genesis and query tests. +func (k *testKeeper) initScenario1() sdk.AccAddress { + // init a community fund with 1000 UMEE and 10 ATOM available for funding + k.initCommunityFund( + coin.New(umee, 1000_000000), + coin.New(atom, 100_000000), + ) + + // init a supplier with bonded uTokens + alice := k.newBondedAccount( + coin.New(uUmee, 100_000000), + coin.New(uAtom, 50_000000), + ) + // create some in-progress unbondings + k.advanceTimeTo(90) + k.mustBeginUnbond(alice, coin.New(uUmee, 5_000000)) + k.mustBeginUnbond(alice, coin.New(uUmee, 5_000000)) + k.mustBeginUnbond(alice, coin.New(uAtom, 5_000000)) + + // create three separate programs, designed to be upcoming, ongoing, and completed at t=100 + k.addIncentiveProgram(uUmee, 10, 20, sdk.NewInt64Coin(umee, 10_000000), true) + k.addIncentiveProgram(uUmee, 90, 20, sdk.NewInt64Coin(umee, 10_000000), true) + k.addIncentiveProgram(uUmee, 140, 20, sdk.NewInt64Coin(umee, 10_000000), true) + + // start programs and claim some rewards to set nonzero reward trackers + k.advanceTimeTo(95) + k.mustClaim(alice) + k.advanceTimeTo(100) + + return alice +} diff --git a/x/incentive/keys_test.go b/x/incentive/keys_test.go index c90af14245..90049d230e 100644 --- a/x/incentive/keys_test.go +++ b/x/incentive/keys_test.go @@ -7,6 +7,8 @@ import ( ) func TestValidateProgramStatus(t *testing.T) { + t.Parallel() + cases := []struct { desc string status ProgramStatus diff --git a/x/incentive/msgs_test.go b/x/incentive/msgs_test.go index c7aee64eb4..e3d5baea99 100644 --- a/x/incentive/msgs_test.go +++ b/x/incentive/msgs_test.go @@ -13,17 +13,20 @@ import ( ) const ( + uumee = "uumee" govAddr = "umee10d07y265gmmuvt4z0w9aw880jnsr700jg5w6jp" ) var ( testAddr, _ = sdk.AccAddressFromBech32("umee1s84d29zk3k20xk9f0hvczkax90l9t94g72n6wm") - uToken = sdk.NewInt64Coin("u/uumee", 10) - token = sdk.NewInt64Coin("uumee", 10) + uToken = sdk.NewInt64Coin(coin.UumeeDenom, 10) + token = sdk.NewInt64Coin(uumee, 10) program = incentive.NewIncentiveProgram(0, 4, 5, uToken.Denom, token, coin.Zero(token.Denom), false) ) func TestMsgs(t *testing.T) { + t.Parallel() + userMsgs := []sdk.Msg{ incentive.NewMsgBond(testAddr, uToken), incentive.NewMsgBeginUnbonding(testAddr, uToken), @@ -62,6 +65,8 @@ type sdkmsg interface { } func TestRoutes(t *testing.T) { + t.Parallel() + msgs := []sdkmsg{ *incentive.NewMsgBond(testAddr, uToken), *incentive.NewMsgBeginUnbonding(testAddr, uToken), diff --git a/x/incentive/params_test.go b/x/incentive/params_test.go index f9fe4e1280..c8d60603e6 100644 --- a/x/incentive/params_test.go +++ b/x/incentive/params_test.go @@ -9,6 +9,8 @@ import ( ) func TestDefaultParams(t *testing.T) { + t.Parallel() + params := DefaultParams() assert.NilError(t, params.Validate())