From a54777b581dd76f693ebeb3bf317ec6330085539 Mon Sep 17 00:00:00 2001 From: Adam Tucker Date: Tue, 26 Jul 2022 13:02:00 -0500 Subject: [PATCH] feat(incentives): create gauge and add to gauge fee charge (backport #2227) (#2237) * feat(incentives)!: create gauge and add to gauge fee charge (#2227) * feat(incentives)!: create gauge and add to gauge fee charge * initialize txfees keeper before incentives * finish TestChargeFee * refactor to charge fee in message server * more tests * clean up * test balances * test create gauge fees (#2228) * test create gauge fees * add mod account to test * test create gauge fees * add mod account to test * move to msg server * merge * add comments * account keeper comment * fix TestCreateGaugeFee * apply appparams.BaseCoinUnit * remove txfees keeper from incentives and revert order * clean up * remove unused keepers fromm incentives keeper * Update x/incentives/keeper/gauge.go * Update x/incentives/keeper/gauge.go * clean up * fixture names * chargeFeeIfSufficientFeeDenomBalance test name * changelog * comment * finished tests (#2230) * finished tests * use keeper instead of math * remove unused tests * clean up * sim only allow accounts with enough to pay fee * lint * move fee to types and reuse in simulation (#2234) Co-authored-by: Adam Tucker Co-authored-by: Adam Tucker * merge gauge fee changes * go mod updates Co-authored-by: Roman --- app/keepers/keepers.go | 1 + go.mod | 4 +- x/incentives/keeper/export_test.go | 10 +- x/incentives/keeper/gauge.go | 19 ++ x/incentives/keeper/gauge_test.go | 96 ++++++++++ x/incentives/keeper/keeper.go | 5 +- x/incentives/keeper/msg_server.go | 8 + x/incentives/keeper/msg_server_test.go | 243 +++++++++++++++++++++++++ x/incentives/simulation/operations.go | 6 +- x/incentives/types/expected_keepers.go | 6 + x/incentives/types/gauge.go | 8 + 11 files changed, 399 insertions(+), 7 deletions(-) create mode 100644 x/incentives/keeper/msg_server_test.go diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 1ee5809f1e1..ed21fa60f91 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -279,6 +279,7 @@ func (appKeepers *AppKeepers) InitNormalKeepers( appKeepers.BankKeeper, appKeepers.LockupKeeper, appKeepers.EpochsKeeper, + appKeepers.DistrKeeper, ) appKeepers.SuperfluidKeeper = superfluidkeeper.NewKeeper( diff --git a/go.mod b/go.mod index 505eda37103..cfedec3600e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.18 require ( github.com/CosmWasm/wasmd v0.24.0 + github.com/CosmWasm/wasmvm v1.0.0 + github.com/cosmos/btcutil v1.0.4 github.com/cosmos/cosmos-sdk v0.45.6 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/iavl v0.17.3 @@ -41,7 +43,6 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v1.1.0 // indirect github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect - github.com/CosmWasm/wasmvm v1.0.0 github.com/DataDog/zstd v1.4.5 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect github.com/GaijinEntertainment/go-exhaustruct/v2 v2.1.0 // indirect @@ -71,7 +72,6 @@ require ( github.com/coinbase/rosetta-sdk-go v0.7.0 // indirect github.com/confio/ics23/go v0.7.0 // indirect github.com/containerd/continuity v0.3.0 // indirect - github.com/cosmos/btcutil v1.0.4 // indirect github.com/cosmos/gorocksdb v1.2.0 // indirect github.com/cosmos/ledger-cosmos-go v0.11.1 // indirect github.com/cosmos/ledger-go v0.9.2 // indirect diff --git a/x/incentives/keeper/export_test.go b/x/incentives/keeper/export_test.go index a547a6f404c..d6c5da0b05a 100644 --- a/x/incentives/keeper/export_test.go +++ b/x/incentives/keeper/export_test.go @@ -6,8 +6,9 @@ import ( "github.com/osmosis-labs/osmosis/v10/x/incentives/types" ) -func (k Keeper) AddGaugeRefByKey(ctx sdk.Context, key []byte, guageID uint64) error { - return k.addGaugeRefByKey(ctx, key, guageID) +// AddGaugeRefByKey appends the provided gauge ID into an array associated with the provided key. +func (k Keeper) AddGaugeRefByKey(ctx sdk.Context, key []byte, gaugeID uint64) error { + return k.addGaugeRefByKey(ctx, key, gaugeID) } func (k Keeper) DeleteGaugeRefByKey(ctx sdk.Context, key []byte, guageID uint64) error { @@ -29,3 +30,8 @@ func (k Keeper) MoveUpcomingGaugeToActiveGauge(ctx sdk.Context, gauge types.Gaug func (k Keeper) MoveActiveGaugeToFinishedGauge(ctx sdk.Context, gauge types.Gauge) error { return k.moveActiveGaugeToFinishedGauge(ctx, gauge) } + +// ChargeFeeIfSufficientFeeDenomBalance see chargeFeeIfSufficientFeeDenomBalance spec. +func (k Keeper) ChargeFeeIfSufficientFeeDenomBalance(ctx sdk.Context, address sdk.AccAddress, fee sdk.Int, gaugeCoins sdk.Coins) error { + return k.chargeFeeIfSufficientFeeDenomBalance(ctx, address, fee, gaugeCoins) +} diff --git a/x/incentives/keeper/gauge.go b/x/incentives/keeper/gauge.go index 1dccbba58af..9ad0bac30ca 100644 --- a/x/incentives/keeper/gauge.go +++ b/x/incentives/keeper/gauge.go @@ -6,9 +6,11 @@ import ( "strings" "time" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/gogo/protobuf/proto" db "github.com/tendermint/tm-db" + appparams "github.com/osmosis-labs/osmosis/v10/app/params" epochtypes "github.com/osmosis-labs/osmosis/v10/x/epochs/types" "github.com/osmosis-labs/osmosis/v10/x/incentives/types" lockuptypes "github.com/osmosis-labs/osmosis/v10/x/lockup/types" @@ -281,3 +283,20 @@ func (k Keeper) GetEpochInfo(ctx sdk.Context) epochtypes.EpochInfo { params := k.GetParams(ctx) return k.ek.GetEpochInfo(ctx, params.DistrEpochIdentifier) } + +// chargeFeeIfSufficientFeeDenomBalance charges fee in the base denom on the address if the address has +// balance that is less than fee + amount of the coin from gaugeCoins that is of base denom. +// gaugeCoins might not have a coin of tx base denom. In that case, fee is only compared to balance. +// The fee is sent to the community pool. +// Returns nil on success, error otherwise. +func (k Keeper) chargeFeeIfSufficientFeeDenomBalance(ctx sdk.Context, address sdk.AccAddress, fee sdk.Int, gaugeCoins sdk.Coins) (err error) { + totalCost := gaugeCoins.AmountOf(appparams.BaseCoinUnit).Add(fee) + accountBalance := k.bk.GetBalance(ctx, address, appparams.BaseCoinUnit).Amount + if accountBalance.LT(totalCost) { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "account's balance of %s (%s) is less than the total cost of the message (%s)", appparams.BaseCoinUnit, accountBalance, totalCost) + } + if err := k.dk.FundCommunityPool(ctx, sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, fee)), address); err != nil { + return err + } + return nil +} diff --git a/x/incentives/keeper/gauge_test.go b/x/incentives/keeper/gauge_test.go index f5abed4d155..edc084e2202 100644 --- a/x/incentives/keeper/gauge_test.go +++ b/x/incentives/keeper/gauge_test.go @@ -3,6 +3,7 @@ package keeper_test import ( "time" + appparams "github.com/osmosis-labs/osmosis/v10/app/params" "github.com/osmosis-labs/osmosis/v10/x/incentives/types" lockuptypes "github.com/osmosis-labs/osmosis/v10/x/lockup/types" @@ -310,3 +311,98 @@ func (suite *KeeperTestSuite) TestGaugesByDenom() { testGaugeByDenom(true) testGaugeByDenom(false) } + +func (suite *KeeperTestSuite) TestChargeFeeIfSufficientFeeDenomBalance() { + const baseFee = int64(100) + + testcases := map[string]struct { + accountBalanceToFund sdk.Coin + feeToCharge int64 + gaugeCoins sdk.Coins + + expectError bool + }{ + "fee + base denom gauge coin == acount balance, success": { + accountBalanceToFund: sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee)), + feeToCharge: baseFee / 2, + gaugeCoins: sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee/2))), + }, + "fee + base denom gauge coin < acount balance, success": { + accountBalanceToFund: sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee)), + feeToCharge: baseFee/2 - 1, + gaugeCoins: sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee/2))), + }, + "fee + base denom gauge coin > acount balance, error": { + accountBalanceToFund: sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(baseFee)), + feeToCharge: baseFee/2 + 1, + gaugeCoins: sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee/2))), + + expectError: true, + }, + "fee + base denom gauge coin < acount balance, custom values, success": { + accountBalanceToFund: sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(11793193112)), + feeToCharge: 55, + gaugeCoins: sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(328812))), + }, + "account funded with coins other than base denom, error": { + accountBalanceToFund: sdk.NewCoin("usdc", sdk.NewInt(baseFee)), + feeToCharge: baseFee, + gaugeCoins: sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee/2))), + + expectError: true, + }, + "fee == account balance, no gauge coins, success": { + accountBalanceToFund: sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee)), + feeToCharge: baseFee, + }, + "gauge coins == account balance, no fee, success": { + accountBalanceToFund: sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee)), + gaugeCoins: sdk.NewCoins(sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee))), + }, + "fee == account balance, gauge coins in denom other than base, success": { + accountBalanceToFund: sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee)), + feeToCharge: baseFee, + gaugeCoins: sdk.NewCoins(sdk.NewCoin("usdc", sdk.NewInt(baseFee*2))), + }, + "fee + gauge coins == account balance, multiple gauge coins, one in denom other than base, success": { + accountBalanceToFund: sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee)), + feeToCharge: baseFee / 2, + gaugeCoins: sdk.NewCoins(sdk.NewCoin("usdc", sdk.NewInt(baseFee*2)), sdk.NewCoin(appparams.DefaultBondDenom, sdk.NewInt(baseFee/2))), + }, + } + + for name, tc := range testcases { + suite.Run(name, func() { + suite.SetupTest() + + testAccount := suite.TestAccs[0] + + ctx := suite.Ctx + incentivesKeepers := suite.App.IncentivesKeeper + bankKeeper := suite.App.BankKeeper + + // Pre-fund account. + suite.FundAcc(testAccount, sdk.NewCoins(tc.accountBalanceToFund)) + + oldBalanceAmount := bankKeeper.GetBalance(ctx, testAccount, appparams.DefaultBondDenom).Amount + + // System under test. + err := incentivesKeepers.ChargeFeeIfSufficientFeeDenomBalance(ctx, testAccount, sdk.NewInt(tc.feeToCharge), tc.gaugeCoins) + + // Assertions. + newBalanceAmount := bankKeeper.GetBalance(ctx, testAccount, appparams.DefaultBondDenom).Amount + if tc.expectError { + suite.Require().Error(err) + + // check account balance unchanged + suite.Require().Equal(oldBalanceAmount, newBalanceAmount) + } else { + suite.Require().NoError(err) + + // check account balance changed. + expectedNewBalanceAmount := oldBalanceAmount.Sub(sdk.NewInt(tc.feeToCharge)) + suite.Require().Equal(expectedNewBalanceAmount.String(), newBalanceAmount.String()) + } + }) + } +} diff --git a/x/incentives/keeper/keeper.go b/x/incentives/keeper/keeper.go index 69936dbef2e..53b159131bb 100644 --- a/x/incentives/keeper/keeper.go +++ b/x/incentives/keeper/keeper.go @@ -21,9 +21,11 @@ type Keeper struct { bk types.BankKeeper lk types.LockupKeeper ek types.EpochKeeper + dk types.DistrKeeper } -func NewKeeper(cdc codec.Codec, storeKey sdk.StoreKey, paramSpace paramtypes.Subspace, bk types.BankKeeper, lk types.LockupKeeper, ek types.EpochKeeper) *Keeper { +// NewKeeper returns a new instance of the incentive module keeper struct. +func NewKeeper(cdc codec.Codec, storeKey sdk.StoreKey, paramSpace paramtypes.Subspace, bk types.BankKeeper, lk types.LockupKeeper, ek types.EpochKeeper, dk types.DistrKeeper) *Keeper { if !paramSpace.HasKeyTable() { paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable()) } @@ -35,6 +37,7 @@ func NewKeeper(cdc codec.Codec, storeKey sdk.StoreKey, paramSpace paramtypes.Sub bk: bk, lk: lk, ek: ek, + dk: dk, } } diff --git a/x/incentives/keeper/msg_server.go b/x/incentives/keeper/msg_server.go index d79330cfbfb..edd81f7e713 100644 --- a/x/incentives/keeper/msg_server.go +++ b/x/incentives/keeper/msg_server.go @@ -30,6 +30,10 @@ func (server msgServer) CreateGauge(goCtx context.Context, msg *types.MsgCreateG return nil, err } + if err := server.keeper.chargeFeeIfSufficientFeeDenomBalance(ctx, owner, types.CreateGaugeFee, msg.Coins); err != nil { + return nil, err + } + gaugeID, err := server.keeper.CreateGauge(ctx, msg.IsPerpetual, owner, msg.Coins, msg.DistributeTo, msg.StartTime, msg.NumEpochsPaidOver) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, err.Error()) @@ -51,6 +55,10 @@ func (server msgServer) AddToGauge(goCtx context.Context, msg *types.MsgAddToGau if err != nil { return nil, err } + + if err := server.keeper.chargeFeeIfSufficientFeeDenomBalance(ctx, owner, types.AddToGaugeFee, msg.Rewards); err != nil { + return nil, err + } err = server.keeper.AddToGaugeRewards(ctx, owner, msg.Rewards, msg.GaugeId) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, err.Error()) diff --git a/x/incentives/keeper/msg_server_test.go b/x/incentives/keeper/msg_server_test.go new file mode 100644 index 00000000000..497a5d2d92b --- /dev/null +++ b/x/incentives/keeper/msg_server_test.go @@ -0,0 +1,243 @@ +package keeper_test + +import ( + "time" + + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + appparams "github.com/osmosis-labs/osmosis/v10/app/params" + "github.com/osmosis-labs/osmosis/v10/x/incentives/keeper" + "github.com/osmosis-labs/osmosis/v10/x/incentives/types" + lockuptypes "github.com/osmosis-labs/osmosis/v10/x/lockup/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ = suite.TestingSuite(nil) + +func (suite *KeeperTestSuite) TestCreateGauge_Fee() { + tests := []struct { + name string + accountBalanceToFund sdk.Coins + gaugeAddition sdk.Coins + expectedEndBalance sdk.Coins + isPerpetual bool + isModuleAccount bool + expectErr bool + }{ + { + name: "user creates a non-perpetual gauge and fills gauge with all remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(60000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + }, + { + name: "user creates a non-perpetual gauge and fills gauge with some remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(70000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + }, + { + name: "user with multiple denoms creates a non-perpetual gauge and fills gauge with some remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(70000000)), sdk.NewCoin("foo", sdk.NewInt(70000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + }, + { + name: "module account creates a perpetual gauge and fills gauge with some remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(70000000)), sdk.NewCoin("foo", sdk.NewInt(70000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + isPerpetual: true, + isModuleAccount: true, + }, + { + name: "user with multiple denoms creates a perpetual gauge and fills gauge with some remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(70000000)), sdk.NewCoin("foo", sdk.NewInt(70000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + isPerpetual: true, + }, + { + name: "user tries to create a non-perpetual gauge but does not have enough funds to pay for the create gauge fee", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(40000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + expectErr: true, + }, + { + name: "user tries to create a non-perpetual gauge but does not have the correct fee denom", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(60000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(10000000))), + expectErr: true, + }, + { + name: "one user tries to create a gauge, has enough funds to pay for the create gauge fee but not enough to fill the gauge", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(60000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(30000000))), + expectErr: true, + }, + } + + for _, tc := range tests { + suite.SetupTest() + + testAccountPubkey := secp256k1.GenPrivKeyFromSecret([]byte("acc")).PubKey() + testAccountAddress := sdk.AccAddress(testAccountPubkey.Address()) + + ctx := suite.Ctx + bankKeeper := suite.App.BankKeeper + accountKeeper := suite.App.AccountKeeper + msgServer := keeper.NewMsgServerImpl(suite.App.IncentivesKeeper) + + suite.FundAcc(testAccountAddress, tc.accountBalanceToFund) + + if tc.isModuleAccount { + modAcc := authtypes.NewModuleAccount(authtypes.NewBaseAccount(testAccountAddress, testAccountPubkey, 1, 0), + "module", + "permission", + ) + accountKeeper.SetModuleAccount(ctx, modAcc) + } + + suite.SetupManyLocks(1, defaultLiquidTokens, defaultLPTokens, defaultLockDuration) + distrTo := lockuptypes.QueryCondition{ + LockQueryType: lockuptypes.ByDuration, + Denom: defaultLPDenom, + Duration: defaultLockDuration, + } + + msg := &types.MsgCreateGauge{ + IsPerpetual: tc.isPerpetual, + Owner: testAccountAddress.String(), + DistributeTo: distrTo, + Coins: tc.gaugeAddition, + StartTime: time.Now(), + NumEpochsPaidOver: 1, + } + // System under test. + _, err := msgServer.CreateGauge(sdk.WrapSDKContext(ctx), msg) + + if tc.expectErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + } + + balanceAmount := bankKeeper.GetAllBalances(ctx, testAccountAddress) + + if tc.expectErr { + suite.Require().Equal(tc.accountBalanceToFund.String(), balanceAmount.String(), "test: %v", tc.name) + } else { + fee := sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, types.CreateGaugeFee)) + accountBalance := tc.accountBalanceToFund.Sub(tc.gaugeAddition) + finalAccountBalance := accountBalance.Sub(fee) + suite.Require().Equal(finalAccountBalance.String(), balanceAmount.String(), "test: %v", tc.name) + } + } +} + +func (suite *KeeperTestSuite) TestAddToGauge_Fee() { + + tests := []struct { + name string + accountBalanceToFund sdk.Coins + gaugeAddition sdk.Coins + nonexistentGauge bool + isPerpetual bool + isModuleAccount bool + expectErr bool + }{ + { + name: "user creates a non-perpetual gauge and fills gauge with all remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(35000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + }, + { + name: "user creates a non-perpetual gauge and fills gauge with some remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(70000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + }, + { + name: "user with multiple denoms creates a non-perpetual gauge and fills gauge with some remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(70000000)), sdk.NewCoin("foo", sdk.NewInt(70000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + }, + { + name: "module account creates a perpetual gauge and fills gauge with some remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(70000000)), sdk.NewCoin("foo", sdk.NewInt(70000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + isPerpetual: true, + isModuleAccount: true, + }, + { + name: "user with multiple denoms creates a perpetual gauge and fills gauge with some remaining tokens", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(70000000)), sdk.NewCoin("foo", sdk.NewInt(70000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + isPerpetual: true, + }, + { + name: "user tries to create a non-perpetual gauge but does not have enough funds to pay for the create gauge fee", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(20000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(10000000))), + expectErr: true, + }, + { + name: "user tries to add to a non-perpetual gauge but does not have the correct fee denom", + accountBalanceToFund: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(60000000))), + gaugeAddition: sdk.NewCoins(sdk.NewCoin("foo", sdk.NewInt(10000000))), + expectErr: true, + }, + } + + for _, tc := range tests { + suite.SetupTest() + + testAccountPubkey := secp256k1.GenPrivKeyFromSecret([]byte("acc")).PubKey() + testAccountAddress := sdk.AccAddress(testAccountPubkey.Address()) + + ctx := suite.Ctx + bankKeeper := suite.App.BankKeeper + incentivesKeeper := suite.App.IncentivesKeeper + accountKeeper := suite.App.AccountKeeper + msgServer := keeper.NewMsgServerImpl(incentivesKeeper) + + suite.FundAcc(testAccountAddress, tc.accountBalanceToFund) + + if tc.isModuleAccount { + modAcc := authtypes.NewModuleAccount(authtypes.NewBaseAccount(testAccountAddress, testAccountPubkey, 1, 0), + "module", + "permission", + ) + accountKeeper.SetModuleAccount(ctx, modAcc) + } + + // System under test. + coins := sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, sdk.NewInt(500000000))) + gaugeID, _, _, _ := suite.SetupNewGauge(true, coins) + if tc.nonexistentGauge { + gaugeID = incentivesKeeper.GetLastGaugeID(ctx) + 1 + } + msg := &types.MsgAddToGauge{ + Owner: testAccountAddress.String(), + GaugeId: gaugeID, + Rewards: tc.gaugeAddition, + } + + _, err := msgServer.AddToGauge(sdk.WrapSDKContext(ctx), msg) + + if tc.expectErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + } + + bal := bankKeeper.GetAllBalances(ctx, testAccountAddress) + + if tc.expectErr { + suite.Require().Equal(tc.accountBalanceToFund.String(), bal.String(), "test: %v", tc.name) + } else { + fee := sdk.NewCoins(sdk.NewCoin(appparams.BaseCoinUnit, types.AddToGaugeFee)) + accountBalance := tc.accountBalanceToFund.Sub(tc.gaugeAddition) + finalAccountBalance := accountBalance.Sub(fee) + suite.Require().Equal(finalAccountBalance.String(), bal.String(), "test: %v", tc.name) + } + } +} diff --git a/x/incentives/simulation/operations.go b/x/incentives/simulation/operations.go index ae50d573944..ea1bae7358d 100644 --- a/x/incentives/simulation/operations.go +++ b/x/incentives/simulation/operations.go @@ -18,6 +18,8 @@ import ( simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + appparams "github.com/osmosis-labs/osmosis/v10/app/params" ) // Simulation operation weights constants. @@ -116,7 +118,7 @@ func SimulateMsgCreateGauge(ak stakingTypes.AccountKeeper, bk stakingTypes.BankK ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { simAccount, _ := simtypes.RandomAcc(r, accs) simCoins := bk.SpendableCoins(ctx, simAccount.Address) - if simCoins.Len() <= 0 { + if simCoins.Len() <= 0 || simCoins.AmountOf(appparams.BaseCoinUnit).LT(types.CreateGaugeFee) { return simtypes.NoOpMsg( types.ModuleName, types.TypeMsgCreateGauge, "Account have no coin"), nil, nil } @@ -154,7 +156,7 @@ func SimulateMsgAddToGauge(ak stakingTypes.AccountKeeper, bk stakingTypes.BankKe ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { simAccount, _ := simtypes.RandomAcc(r, accs) simCoins := bk.SpendableCoins(ctx, simAccount.Address) - if simCoins.Len() <= 0 { + if simCoins.Len() <= 0 || simCoins.AmountOf(appparams.BaseCoinUnit).LT(types.AddToGaugeFee) { return simtypes.NoOpMsg( types.ModuleName, types.TypeMsgAddToGauge, "Account have no coin"), nil, nil } diff --git a/x/incentives/types/expected_keepers.go b/x/incentives/types/expected_keepers.go index 088536d13e9..125fb3929cc 100644 --- a/x/incentives/types/expected_keepers.go +++ b/x/incentives/types/expected_keepers.go @@ -12,6 +12,7 @@ import ( // BankKeeper defines the expected interface needed to retrieve account balances. type BankKeeper interface { GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin HasSupply(ctx sdk.Context, denom string) bool @@ -35,3 +36,8 @@ type LockupKeeper interface { type EpochKeeper interface { GetEpochInfo(ctx sdk.Context, identifier string) epochstypes.EpochInfo } + +// DistrKeeper defines the contract needed to be fulfilled for distribution keeper. +type DistrKeeper interface { + FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error +} diff --git a/x/incentives/types/gauge.go b/x/incentives/types/gauge.go index 642100e0289..ac4cee521dc 100644 --- a/x/incentives/types/gauge.go +++ b/x/incentives/types/gauge.go @@ -8,6 +8,14 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +var ( + // CreateGaugeFee is the fee required to create a new gauge. + CreateGaugeFee = sdk.NewInt(50 * 1_000_000) + // AddToGagugeFee is the fee required to add to gauge. + AddToGaugeFee = sdk.NewInt(25 * 1_000_000) +) + +// NewGauge creates a new gauge struct given the required gauge parameters. func NewGauge(id uint64, isPerpetual bool, distrTo lockuptypes.QueryCondition, coins sdk.Coins, startTime time.Time, numEpochsPaidOver uint64, filledEpochs uint64, distrCoins sdk.Coins) Gauge { return Gauge{ Id: id,