diff --git a/app/ante.go b/app/ante.go index ec45267fb39..787c5509ca0 100644 --- a/app/ante.go +++ b/app/ante.go @@ -3,14 +3,13 @@ package app import ( wasm "github.com/CosmWasm/wasmd/x/wasm" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" - channelkeeper "github.com/cosmos/ibc-go/v2/modules/core/04-channel/keeper" - ibcante "github.com/cosmos/ibc-go/v2/modules/core/ante" servertypes "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" ante "github.com/cosmos/cosmos-sdk/x/auth/ante" "github.com/cosmos/cosmos-sdk/x/auth/signing" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + channelkeeper "github.com/cosmos/ibc-go/v2/modules/core/04-channel/keeper" + ibcante "github.com/cosmos/ibc-go/v2/modules/core/ante" txfeeskeeper "github.com/osmosis-labs/osmosis/v7/x/txfees/keeper" txfeestypes "github.com/osmosis-labs/osmosis/v7/x/txfees/types" @@ -23,7 +22,7 @@ func NewAnteHandler( wasmConfig wasm.Config, txCounterStoreKey sdk.StoreKey, ak ante.AccountKeeper, - bankKeeper authtypes.BankKeeper, + bankKeeper txfeestypes.BankKeeper, txFeesKeeper *txfeeskeeper.Keeper, spotPriceCalculator txfeestypes.SpotPriceCalculator, sigGasConsumer ante.SignatureVerificationGasConsumer, @@ -32,7 +31,7 @@ func NewAnteHandler( ) sdk.AnteHandler { mempoolFeeOptions := txfeestypes.NewMempoolFeeOptions(appOpts) mempoolFeeDecorator := txfeeskeeper.NewMempoolFeeDecorator(*txFeesKeeper, mempoolFeeOptions) - + deductFeeDecorator := txfeeskeeper.NewDeductFeeDecorator(*txFeesKeeper, ak, bankKeeper, nil) return sdk.ChainAnteDecorators( ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first wasmkeeper.NewLimitSimulationGasDecorator(wasmConfig.SimulationGasLimit), @@ -45,7 +44,7 @@ func NewAnteHandler( ante.TxTimeoutHeightDecorator{}, ante.NewValidateMemoDecorator(ak), ante.NewConsumeGasForTxSizeDecorator(ak), - ante.NewDeductFeeDecorator(ak, bankKeeper, nil), + deductFeeDecorator, ante.NewSetPubKeyDecorator(ak), // SetPubKeyDecorator must be called before all signature verification decorators ante.NewValidateSigCountDecorator(ak), ante.NewSigGasConsumeDecorator(ak, sigGasConsumer), diff --git a/app/apptesting/test_suite.go b/app/apptesting/test_suite.go index 8227d037e2d..23a22e9460f 100644 --- a/app/apptesting/test_suite.go +++ b/app/apptesting/test_suite.go @@ -22,6 +22,10 @@ import ( gammtypes "github.com/osmosis-labs/osmosis/v7/x/gamm/types" lockupkeeper "github.com/osmosis-labs/osmosis/v7/x/lockup/keeper" lockuptypes "github.com/osmosis-labs/osmosis/v7/x/lockup/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" ) type KeeperTestHelper struct { @@ -228,6 +232,26 @@ func (keeperTestHelper *KeeperTestHelper) LockTokens(addr sdk.AccAddress, coins return msgResponse.ID } +func (keeperTestHelper *KeeperTestHelper) BuildTx( + txBuilder client.TxBuilder, + msgs []sdk.Msg, + sigV2 signing.SignatureV2, + memo string, txFee sdk.Coins, + gasLimit uint64, +) authsigning.Tx { + err := txBuilder.SetMsgs(msgs[0]) + keeperTestHelper.Require().NoError(err) + + err = txBuilder.SetSignatures(sigV2) + keeperTestHelper.Require().NoError(err) + + txBuilder.SetMemo(memo) + txBuilder.SetFeeAmount(txFee) + txBuilder.SetGasLimit(gasLimit) + + return txBuilder.GetTx() +} + // CreateRandomAccounts is a function return a list of randomly generated AccAddresses func CreateRandomAccounts(numAccts int) []sdk.AccAddress { testAddrs := make([]sdk.AccAddress, numAccts) diff --git a/app/keepers.go b/app/keepers.go index cca81eed4c5..f8746ef1b1d 100644 --- a/app/keepers.go +++ b/app/keepers.go @@ -338,10 +338,17 @@ func (app *OsmosisApp) InitNormalKeepers( ) app.PoolIncentivesKeeper = &poolIncentivesKeeper + // Note: gammKeeper is expected to satisfy the SpotPriceCalculator interface parameter txFeesKeeper := txfeeskeeper.NewKeeper( appCodec, + app.AccountKeeper, + app.BankKeeper, + app.EpochsKeeper, keys[txfeestypes.StoreKey], app.GAMMKeeper, + app.GAMMKeeper, + txfeestypes.FeeCollectorName, + txfeestypes.NonNativeFeeCollectorName, ) app.TxFeesKeeper = &txFeesKeeper @@ -448,6 +455,7 @@ func (app *OsmosisApp) SetupHooks() { app.EpochsKeeper.SetHooks( epochstypes.NewMultiEpochHooks( // insert epoch hooks receivers here + app.TxFeesKeeper.Hooks(), app.SuperfluidKeeper.Hooks(), app.IncentivesKeeper.Hooks(), app.MintKeeper.Hooks(), diff --git a/app/modules.go b/app/modules.go index 29a716c7bc6..5ff7a9bff7f 100644 --- a/app/modules.go +++ b/app/modules.go @@ -131,6 +131,7 @@ var moduleAccountPermissions = map[string][]string{ poolincentivestypes.ModuleName: nil, superfluidtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, txfeestypes.ModuleName: nil, + txfeestypes.NonNativeFeeCollectorName: nil, wasm.ModuleName: {authtypes.Burner}, } diff --git a/x/txfees/keeper/feedecorator.go b/x/txfees/keeper/feedecorator.go index 17e30c27b70..950f224f47c 100644 --- a/x/txfees/keeper/feedecorator.go +++ b/x/txfees/keeper/feedecorator.go @@ -1,11 +1,15 @@ package keeper import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/osmosis-labs/osmosis/v7/x/txfees/keeper/txfee_filters" "github.com/osmosis-labs/osmosis/v7/x/txfees/types" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) // MempoolFeeDecorator will check if the transaction's fee is at least as large @@ -121,3 +125,114 @@ func (mfd MempoolFeeDecorator) GetMinBaseGasPriceForTx(ctx sdk.Context, baseDeno } return cfgMinGasPrice } + +// DeductFeeDecorator deducts fees from the first signer of the tx. +// If the first signer does not have the funds to pay for the fees, we return an InsufficientFunds error. +// We call next AnteHandler if fees successfully deducted. +// +// CONTRACT: Tx must implement FeeTx interface to use DeductFeeDecorator +type DeductFeeDecorator struct { + ak types.AccountKeeper + bankKeeper types.BankKeeper + feegrantKeeper types.FeegrantKeeper + txFeesKeeper Keeper +} + +func NewDeductFeeDecorator(tk Keeper, ak types.AccountKeeper, bk types.BankKeeper, fk types.FeegrantKeeper) DeductFeeDecorator { + return DeductFeeDecorator{ + ak: ak, + bankKeeper: bk, + feegrantKeeper: fk, + txFeesKeeper: tk, + } +} + +func (dfd DeductFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return ctx, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + // checks to make sure the module account has been set to collect fees in base token + if addr := dfd.ak.GetModuleAddress(types.FeeCollectorName); addr == nil { + return ctx, fmt.Errorf("Fee collector module account (%s) has not been set", types.FeeCollectorName) + } + + // checks to make sure a separate module account has been set to collect fees not in base token + if addrNonNativeFee := dfd.ak.GetModuleAddress(types.NonNativeFeeCollectorName); addrNonNativeFee == nil { + return ctx, fmt.Errorf("non native fee collector module account (%s) has not been set", types.NonNativeFeeCollectorName) + } + + // fee can be in any denom (checked for validity later) + fee := feeTx.GetFee() + feePayer := feeTx.FeePayer() + feeGranter := feeTx.FeeGranter() + + // set the fee payer as the default address to deduct fees from + deductFeesFrom := feePayer + + // If a fee granter was set, deduct fee from the fee granter's account. + if feeGranter != nil { + if dfd.feegrantKeeper == nil { + return ctx, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee grants is not enabled") + } else if !feeGranter.Equals(feePayer) { + err := dfd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, fee, tx.GetMsgs()) + if err != nil { + return ctx, sdkerrors.Wrapf(err, "%s not allowed to pay fees from %s", feeGranter, feePayer) + } + } + + // if no errors, change the account that is charged for fees to the fee granter + deductFeesFrom = feeGranter + } + + deductFeesFromAcc := dfd.ak.GetAccount(ctx, deductFeesFrom) + if deductFeesFromAcc == nil { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownAddress, "fee payer address: %s does not exist", deductFeesFrom) + } + + // deducts the fees and transfer them to the module account + if !feeTx.GetFee().IsZero() { + err = DeductFees(dfd.txFeesKeeper, dfd.bankKeeper, ctx, deductFeesFromAcc, feeTx.GetFee()) + if err != nil { + return ctx, err + } + } + + ctx.EventManager().EmitEvents(sdk.Events{sdk.NewEvent(sdk.EventTypeTx, + sdk.NewAttribute(sdk.AttributeKeyFee, feeTx.GetFee().String()), + )}) + + return next(ctx, tx, simulate) +} + +// DeductFees deducts fees from the given account and transfers them to the set module account. +func DeductFees(txFeesKeeper types.TxFeesKeeper, bankKeeper types.BankKeeper, ctx sdk.Context, acc authtypes.AccountI, fees sdk.Coins) error { + // Checks the validity of the fee tokens (sorted, have positive amount, valid and unique denomination) + if !fees.IsValid() { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees) + } + + // pulls base denom from TxFeesKeeper (should be uOSMO) + baseDenom, err := txFeesKeeper.GetBaseDenom(ctx) + if err != nil { + return err + } + + // checks if input fee is uOSMO (assumes only one fee token exists in the fees array (as per the check in mempoolFeeDecorator)) + if fees[0].Denom == baseDenom { + // sends to FeeCollectorName module account + err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), types.FeeCollectorName, fees) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + } else { + // sends to NonNativeFeeCollectorName module account + err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), types.NonNativeFeeCollectorName, fees) + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + } + + return nil +} diff --git a/x/txfees/keeper/feedecorator_test.go b/x/txfees/keeper/feedecorator_test.go index 95c11ff6e91..204918a77e4 100644 --- a/x/txfees/keeper/feedecorator_test.go +++ b/x/txfees/keeper/feedecorator_test.go @@ -1,13 +1,16 @@ package keeper_test import ( - "github.com/osmosis-labs/osmosis/v7/x/txfees/types" - + clienttx "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" "github.com/osmosis-labs/osmosis/v7/x/txfees/keeper" + "github.com/osmosis-labs/osmosis/v7/x/txfees/types" ) func (suite *KeeperTestSuite) TestFeeDecorator() { @@ -32,6 +35,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested uint64 isCheckTx bool expectPass bool + baseDenomGas bool }{ { name: "no min gas price - checktx", @@ -40,6 +44,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: true, expectPass: true, + baseDenomGas: true, }, { name: "no min gas price - delivertx", @@ -48,6 +53,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: false, expectPass: true, + baseDenomGas: true, }, { name: "works with valid basedenom fee", @@ -57,6 +63,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: true, expectPass: true, + baseDenomGas: true, }, { name: "doesn't work with not enough fee in checktx", @@ -66,6 +73,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: true, expectPass: false, + baseDenomGas: true, }, { name: "works with not enough fee in delivertx", @@ -75,6 +83,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: false, expectPass: true, + baseDenomGas: true, }, { name: "works with valid converted fee", @@ -84,6 +93,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: true, expectPass: true, + baseDenomGas: false, }, { name: "doesn't work with not enough converted fee in checktx", @@ -93,6 +103,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: true, expectPass: false, + baseDenomGas: false, }, { name: "works with not enough converted fee in delivertx", @@ -102,6 +113,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: false, expectPass: true, + baseDenomGas: false, }, { name: "multiple fee coins - checktx", @@ -110,6 +122,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: true, expectPass: false, + baseDenomGas: false, }, { name: "multiple fee coins - delivertx", @@ -118,6 +131,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: false, expectPass: false, + baseDenomGas: false, }, { name: "invalid fee denom", @@ -126,6 +140,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: false, expectPass: false, + baseDenomGas: false, }, { name: "mingasprice not containing basedenom gets treated as min gas price 0", @@ -134,6 +149,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: 10000, isCheckTx: true, expectPass: true, + baseDenomGas: false, }, { name: "tx with gas wanted more than allowed should not pass", @@ -142,6 +158,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: mempoolFeeOpts.MaxGasWantedPerTx + 1, isCheckTx: true, expectPass: false, + baseDenomGas: false, }, { name: "tx with high gas and not enough fee should no pass", @@ -150,6 +167,7 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: mempoolFeeOpts.HighGasTxThreshold, isCheckTx: true, expectPass: false, + baseDenomGas: false, }, { name: "tx with high gas and enough fee should pass", @@ -158,23 +176,63 @@ func (suite *KeeperTestSuite) TestFeeDecorator() { gasRequested: mempoolFeeOpts.HighGasTxThreshold, isCheckTx: true, expectPass: true, + baseDenomGas: false, }, } for _, tc := range tests { - suite.Ctx = suite.Ctx.WithIsCheckTx(tc.isCheckTx) + // reset pool and accounts for each test + suite.SetupTest(false) + uionPoolId := suite.PrepareUni2PoolWithAssets( + sdk.NewInt64Coin(sdk.DefaultBondDenom, 500), + sdk.NewInt64Coin(uion, 500), + ) + suite.ExecuteUpgradeFeeTokenProposal(uion, uionPoolId) + + suite.Ctx = suite.Ctx.WithIsCheckTx(tc.isCheckTx).WithMinGasPrices(tc.minGasPrices) suite.Ctx = suite.Ctx.WithMinGasPrices(tc.minGasPrices) - tx := legacytx.NewStdTx([]sdk.Msg{}, legacytx.NewStdFee( - tc.gasRequested, - tc.txFee, - ), []legacytx.StdSignature{}, "") + // TxBuilder components reset for every test case + txBuilder := suite.clientCtx.TxConfig.NewTxBuilder() + priv0, _, addr0 := testdata.KeyTestPubAddr() + acc1 := suite.App.AccountKeeper.NewAccountWithAddress(suite.Ctx, addr0) + suite.App.AccountKeeper.SetAccount(suite.Ctx, acc1) + msgs := []sdk.Msg{testdata.NewTestMsg(addr0)} + privs, accNums, accSeqs := []cryptotypes.PrivKey{priv0}, []uint64{0}, []uint64{0} + signerData := authsigning.SignerData{ + ChainID: suite.Ctx.ChainID(), + AccountNumber: accNums[0], + Sequence: accSeqs[0], + } + + gasLimit := tc.gasRequested + sigV2, _ := clienttx.SignWithPrivKey( + 1, + signerData, + txBuilder, + privs[0], + suite.clientCtx.TxConfig, + accSeqs[0], + ) + + simapp.FundAccount(suite.App.BankKeeper, suite.Ctx, addr0, tc.txFee) + + tx := suite.BuildTx(txBuilder, msgs, sigV2, "", tc.txFee, gasLimit) mfd := keeper.NewMempoolFeeDecorator(*suite.App.TxFeesKeeper, mempoolFeeOpts) - antehandler := sdk.ChainAnteDecorators(mfd) - _, err := antehandler(suite.Ctx, tx, false) + dfd := keeper.NewDeductFeeDecorator(*suite.App.TxFeesKeeper, *suite.App.AccountKeeper, *suite.App.BankKeeper, nil) + antehandlerMFD := sdk.ChainAnteDecorators(mfd, dfd) + _, err := antehandlerMFD(suite.Ctx, tx, false) + if tc.expectPass { + if tc.baseDenomGas && !tc.txFee.IsZero() { + moduleAddr := suite.App.AccountKeeper.GetModuleAddress(types.FeeCollectorName) + suite.Require().Equal(tc.txFee[0], suite.App.BankKeeper.GetBalance(suite.Ctx, moduleAddr, baseDenom), tc.name) + } else if !tc.txFee.IsZero() { + moduleAddr := suite.App.AccountKeeper.GetModuleAddress(types.NonNativeFeeCollectorName) + suite.Require().Equal(tc.txFee[0], suite.App.BankKeeper.GetBalance(suite.Ctx, moduleAddr, tc.txFee[0].Denom), tc.name) + } suite.Require().NoError(err, "test: %s", tc.name) } else { suite.Require().Error(err, "test: %s", tc.name) diff --git a/x/txfees/keeper/hooks.go b/x/txfees/keeper/hooks.go new file mode 100644 index 00000000000..6cb662b36a7 --- /dev/null +++ b/x/txfees/keeper/hooks.go @@ -0,0 +1,65 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/v7/osmoutils" + epochstypes "github.com/osmosis-labs/osmosis/v7/x/epochs/types" + txfeestypes "github.com/osmosis-labs/osmosis/v7/x/txfees/types" +) + +func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64) {} + +// at the end of each epoch, swap all non-OSMO fees into OSMO and transfer to fee module account +func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64) { + nonNativeFeeAddr := k.accountKeeper.GetModuleAddress(txfeestypes.NonNativeFeeCollectorName) + baseDenom, _ := k.GetBaseDenom(ctx) + feeTokens := k.GetFeeTokens(ctx) + + for _, feetoken := range feeTokens { + if feetoken.Denom == baseDenom { + continue + } + coinBalance := k.bankKeeper.GetBalance(ctx, nonNativeFeeAddr, feetoken.Denom) + if coinBalance.Amount.IsZero() { + continue + } + + // Do the swap of this fee token denom to base denom. + _ = osmoutils.ApplyFuncIfNoError(ctx, func(cacheCtx sdk.Context) error { + // We allow full slippage. Theres not really an effective way to bound slippage until TWAP's land, + // but even then the point is a bit moot. + // The only thing that could be done is a costly griefing attack to reduce the amount of osmo given as tx fees. + // However the idea of the txfees FeeToken gating is that the pool is sufficiently liquid for that base token. + minAmountOut := sdk.ZeroInt() + _, err := k.gammKeeper.SwapExactAmountIn(cacheCtx, nonNativeFeeAddr, feetoken.PoolID, coinBalance, baseDenom, minAmountOut) + return err + }) + } + + // Get all of the txfee payout denom in the module account + baseDenomCoins := sdk.NewCoins(k.bankKeeper.GetBalance(ctx, nonNativeFeeAddr, baseDenom)) + + _ = osmoutils.ApplyFuncIfNoError(ctx, func(cacheCtx sdk.Context) error { + err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, txfeestypes.NonNativeFeeCollectorName, txfeestypes.FeeCollectorName, baseDenomCoins) + return err + }) +} + +// Hooks wrapper struct for incentives keeper +type Hooks struct { + k Keeper +} + +var _ epochstypes.EpochHooks = Hooks{} + +// Return the wrapper struct +func (k Keeper) Hooks() Hooks { + return Hooks{k} +} + +func (h Hooks) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64) {} + +func (h Hooks) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64) { + h.k.AfterEpochEnd(ctx, epochIdentifier, epochNumber) +} diff --git a/x/txfees/keeper/hooks_test.go b/x/txfees/keeper/hooks_test.go new file mode 100644 index 00000000000..6f1a0872a7f --- /dev/null +++ b/x/txfees/keeper/hooks_test.go @@ -0,0 +1,84 @@ +package keeper_test + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + + gammtypes "github.com/osmosis-labs/osmosis/v7/x/gamm/types" + "github.com/osmosis-labs/osmosis/v7/x/txfees/types" +) + +var defaultPooledAssetAmount = int64(500) + +func (suite *KeeperTestSuite) preparePool(denom string) (poolID uint64, pool gammtypes.PoolI) { + baseDenom, _ := suite.App.TxFeesKeeper.GetBaseDenom(suite.Ctx) + poolID = suite.PrepareUni2PoolWithAssets( + sdk.NewInt64Coin(baseDenom, defaultPooledAssetAmount), + sdk.NewInt64Coin(denom, defaultPooledAssetAmount), + ) + pool, err := suite.App.GAMMKeeper.GetPoolAndPoke(suite.Ctx, poolID) + suite.Require().NoError(err) + suite.ExecuteUpgradeFeeTokenProposal(denom, poolID) + return poolID, pool +} + +func (suite *KeeperTestSuite) TestTxFeesAfterEpochEnd() { + suite.SetupTest(false) + baseDenom, _ := suite.App.TxFeesKeeper.GetBaseDenom(suite.Ctx) + + // create pools for three separate fee tokens + uion := "uion" + _, uionPool := suite.preparePool(uion) + atom := "atom" + _, atomPool := suite.preparePool(atom) + ust := "ust" + _, ustPool := suite.preparePool(ust) + + // todo make this section onwards table driven + coins := sdk.NewCoins(sdk.NewInt64Coin(uion, 10), + sdk.NewInt64Coin(atom, 20), + sdk.NewInt64Coin(ust, 14)) + + swapFee := sdk.NewDec(0) + + expectedOutput1, err := uionPool.CalcOutAmtGivenIn(suite.Ctx, + sdk.Coins{sdk.Coin{Denom: uion, Amount: coins.AmountOf(uion)}}, + baseDenom, + swapFee) + suite.Require().NoError(err) + expectedOutput2, err := atomPool.CalcOutAmtGivenIn(suite.Ctx, + sdk.Coins{sdk.Coin{Denom: atom, Amount: coins.AmountOf(atom)}}, + baseDenom, + swapFee) + suite.Require().NoError(err) + expectedOutput3, err := ustPool.CalcOutAmtGivenIn(suite.Ctx, + sdk.Coins{sdk.Coin{Denom: ust, Amount: coins.AmountOf(ust)}}, + baseDenom, + swapFee) + suite.Require().NoError(err) + + fullExpectedOutput := expectedOutput1.Add(expectedOutput2).Add(expectedOutput3) + + _, _, addr0 := testdata.KeyTestPubAddr() + simapp.FundAccount(suite.App.BankKeeper, suite.Ctx, addr0, coins) + suite.App.BankKeeper.SendCoinsFromAccountToModule(suite.Ctx, addr0, types.NonNativeFeeCollectorName, coins) + + moduleAddrFee := suite.App.AccountKeeper.GetModuleAddress(types.FeeCollectorName) + moduleAddrNonNativeFee := suite.App.AccountKeeper.GetModuleAddress(types.NonNativeFeeCollectorName) + + // make sure module account is funded with test fee tokens + suite.Require().True(suite.App.BankKeeper.HasBalance(suite.Ctx, moduleAddrNonNativeFee, coins[0])) + suite.Require().True(suite.App.BankKeeper.HasBalance(suite.Ctx, moduleAddrNonNativeFee, coins[1])) + suite.Require().True(suite.App.BankKeeper.HasBalance(suite.Ctx, moduleAddrNonNativeFee, coins[2])) + + params := suite.App.IncentivesKeeper.GetParams(suite.Ctx) + futureCtx := suite.Ctx.WithBlockTime(time.Now().Add(time.Minute)) + + suite.App.EpochsKeeper.AfterEpochEnd(futureCtx, params.DistrEpochIdentifier, int64(1)) + + suite.Require().Empty(suite.App.BankKeeper.GetAllBalances(suite.Ctx, moduleAddrNonNativeFee)) + suite.Require().True(suite.App.BankKeeper.GetBalance(suite.Ctx, moduleAddrFee, baseDenom).Amount.GTE(fullExpectedOutput.Amount.TruncateInt())) +} diff --git a/x/txfees/keeper/keeper.go b/x/txfees/keeper/keeper.go index 8970f424487..1c60dd2add8 100644 --- a/x/txfees/keeper/keeper.go +++ b/x/txfees/keeper/keeper.go @@ -12,24 +12,40 @@ import ( "github.com/osmosis-labs/osmosis/v7/x/txfees/types" ) -type ( - Keeper struct { - cdc codec.Codec - storeKey sdk.StoreKey - - spotPriceCalculator types.SpotPriceCalculator - } -) +type Keeper struct { + cdc codec.Codec + storeKey sdk.StoreKey + + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + epochKeeper types.EpochKeeper + gammKeeper types.GammKeeper + spotPriceCalculator types.SpotPriceCalculator + feeCollectorName string + nonNativeFeeCollectorName string +} func NewKeeper( cdc codec.Codec, + accountKeeper types.AccountKeeper, + bankKeeper types.BankKeeper, + epochKeeper types.EpochKeeper, storeKey sdk.StoreKey, + gammKeeper types.GammKeeper, spotPriceCalculator types.SpotPriceCalculator, + feeCollectorName string, + nonNativeFeeCollectorName string, ) Keeper { return Keeper{ - cdc: cdc, - storeKey: storeKey, - spotPriceCalculator: spotPriceCalculator, + cdc: cdc, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + epochKeeper: epochKeeper, + storeKey: storeKey, + gammKeeper: gammKeeper, + spotPriceCalculator: spotPriceCalculator, + feeCollectorName: feeCollectorName, + nonNativeFeeCollectorName: nonNativeFeeCollectorName, } } diff --git a/x/txfees/keeper/keeper_test.go b/x/txfees/keeper/keeper_test.go index dcbedd7f23b..52f7420ec51 100644 --- a/x/txfees/keeper/keeper_test.go +++ b/x/txfees/keeper/keeper_test.go @@ -5,8 +5,11 @@ import ( "github.com/stretchr/testify/suite" + "github.com/cosmos/cosmos-sdk/client" sdk "github.com/cosmos/cosmos-sdk/types" + osmosisapp "github.com/osmosis-labs/osmosis/v7/app" + "github.com/osmosis-labs/osmosis/v7/app/apptesting" "github.com/osmosis-labs/osmosis/v7/x/txfees/types" ) @@ -14,13 +17,25 @@ import ( type KeeperTestSuite struct { apptesting.KeeperTestHelper + clientCtx client.Context queryClient types.QueryClient } +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + func (suite *KeeperTestSuite) SetupTest(isCheckTx bool) { suite.Setup() suite.queryClient = types.NewQueryClient(suite.QueryHelper) + encodingConfig := osmosisapp.MakeEncodingConfig() + suite.clientCtx = client.Context{}. + WithInterfaceRegistry(encodingConfig.InterfaceRegistry). + WithTxConfig(encodingConfig.TxConfig). + WithLegacyAmino(encodingConfig.Amino). + WithJSONCodec(encodingConfig.Marshaler) + // Mint some assets to the accounts. for _, acc := range suite.TestAccs { suite.FundAcc(acc, @@ -28,16 +43,14 @@ func (suite *KeeperTestSuite) SetupTest(isCheckTx bool) { sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(10000000000)), sdk.NewCoin("uosmo", sdk.NewInt(100000000000000000)), // Needed for pool creation fee sdk.NewCoin("uion", sdk.NewInt(10000000)), + sdk.NewCoin("atom", sdk.NewInt(10000000)), + sdk.NewCoin("ust", sdk.NewInt(10000000)), sdk.NewCoin("foo", sdk.NewInt(10000000)), sdk.NewCoin("bar", sdk.NewInt(10000000)), )) } } -func TestKeeperTestSuite(t *testing.T) { - suite.Run(t, new(KeeperTestSuite)) -} - func (suite *KeeperTestSuite) ExecuteUpgradeFeeTokenProposal(feeToken string, poolId uint64) error { upgradeProp := types.NewUpdateFeeTokenProposal( "Test Proposal", diff --git a/x/txfees/types/expected_keepers.go b/x/txfees/types/expected_keepers.go index a65e78098c6..c8a3d2df789 100644 --- a/x/txfees/types/expected_keepers.go +++ b/x/txfees/types/expected_keepers.go @@ -2,6 +2,9 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + epochstypes "github.com/osmosis-labs/osmosis/v7/x/epochs/types" ) // SpotPriceCalculator defines the contract that must be fulfilled by a spot price calculator @@ -9,3 +12,49 @@ import ( type SpotPriceCalculator interface { CalculateSpotPrice(ctx sdk.Context, poolId uint64, tokenInDenom, tokenOutDenom string) (sdk.Dec, error) } + +// GammKeeper defines the contract needed for AccountKeeper related APIs. +type GammKeeper interface { + SwapExactAmountIn( + ctx sdk.Context, + sender sdk.AccAddress, + poolId uint64, + tokenIn sdk.Coin, + tokenOutDenom string, + tokenOutMinAmount sdk.Int, + ) (tokenOutAmount sdk.Int, err error) +} + +// AccountKeeper defines the contract needed for AccountKeeper related APIs. +// Interface provides support to use non-sdk AccountKeeper for AnteHandler's decorators. +type AccountKeeper interface { + GetParams(ctx sdk.Context) (params authtypes.Params) + GetAccount(ctx sdk.Context, addr sdk.AccAddress) authtypes.AccountI + SetAccount(ctx sdk.Context, acc authtypes.AccountI) + GetModuleAddress(moduleName string) sdk.AccAddress +} + +// FeegrantKeeper defines the expected feegrant keeper. +type FeegrantKeeper interface { + UseGrantedFees(ctx sdk.Context, granter, grantee sdk.AccAddress, fee sdk.Coins, msgs []sdk.Msg) error +} + +// BankKeeper defines the contract needed for supply related APIs (noalias) +type BankKeeper interface { + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error +} + +// TxFeesKeeper defines the expected transaction fee keeper +type TxFeesKeeper interface { + ConvertToBaseToken(ctx sdk.Context, inputFee sdk.Coin) (sdk.Coin, error) + GetBaseDenom(ctx sdk.Context) (denom string, err error) + GetFeeToken(ctx sdk.Context, denom string) (FeeToken, error) +} + +// EpochKeeper defines the contract needed to be fulfilled for epochs keeper +type EpochKeeper interface { + GetEpochInfo(ctx sdk.Context, identifier string) epochstypes.EpochInfo +} diff --git a/x/txfees/types/keys.go b/x/txfees/types/keys.go index d10ac077242..0978070fd39 100644 --- a/x/txfees/types/keys.go +++ b/x/txfees/types/keys.go @@ -10,7 +10,13 @@ const ( // RouterKey is the message route for slashing. RouterKey = ModuleName - // QuerierRoute defines the module's query routing key. + // FeeCollectorName the module account name for the fee collector account address. + FeeCollectorName = "fee_collector" + + // NonNativeFeeCollectorName the module account name for the alt fee collector account address (used for auto-swapping non-OSMO tx fees). + NonNativeFeeCollectorName = "non_native_fee_collector" + + // QuerierRoute defines the module's query routing key QuerierRoute = ModuleName )