Skip to content

Commit

Permalink
Test: add unit tests for decorators (#182)
Browse files Browse the repository at this point in the history
* wip: setup mocks for ante test

* remove mock ante handler

* add test for ibc fee

* linting

* add comment on unimplemented feature

* extract duplicated code

* chore: refactor code and add comments

* test: add test for FeeabsDeductFee decorator

* linting

* remove unused code
  • Loading branch information
tuantran1702 authored Apr 22, 2024
1 parent b879162 commit f2241e6
Show file tree
Hide file tree
Showing 6 changed files with 984 additions and 7 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/spf13/cast v1.5.1
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
go.uber.org/mock v0.2.0
golang.org/x/tools v0.6.0
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0
google.golang.org/grpc v1.60.1
Expand Down Expand Up @@ -233,7 +234,7 @@ require (
github.com/gogo/googleapis v1.4.1 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/mock v1.6.0
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand Down
236 changes: 236 additions & 0 deletions x/feeabs/ante/ante_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package ante_test

import (
"testing"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

"github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/ante"
"github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/types"
)

func TestMempoolDecorator(t *testing.T) {
gasLimit := uint64(200000)
// mockHostZoneConfig is used to mock the host zone config, with ibcfee as the ibc fee denom to be used as alternative fee
mockHostZoneConfig := types.HostChainFeeAbsConfig{
IbcDenom: "ibcfee",
OsmosisPoolTokenDenomIn: "osmosis",
PoolId: 1,
Status: types.HostChainFeeAbsStatus_UPDATED,
MinSwapAmount: 0,
}
testCases := []struct {
name string
feeAmount sdk.Coins
minGasPrice sdk.DecCoins
malleate func(*AnteTestSuite)
isErr bool
expErr error
}{
{
"empty fee, should fail",
sdk.Coins{},
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 100))...),
func(suite *AnteTestSuite) {
},
true,
sdkerrors.ErrInsufficientFee,
},
{
"not enough native fee, should fail",
sdk.NewCoins(sdk.NewInt64Coin("native", 100)),
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
func(suite *AnteTestSuite) {},
true,
sdkerrors.ErrInsufficientFee,
},
{
"enough native fee, should pass",
sdk.NewCoins(sdk.NewInt64Coin("native", 1000*int64(gasLimit))),
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
func(suite *AnteTestSuite) {},
false,
nil,
},
{
"unknown ibc fee denom, should fail",
sdk.NewCoins(sdk.NewInt64Coin("ibcfee", 1000*int64(gasLimit))),
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
func(suite *AnteTestSuite) {},
true,
sdkerrors.ErrInvalidCoins,
},
{
"not enough ibc fee, should fail",
sdk.NewCoins(sdk.NewInt64Coin("ibcfee", 999*int64(gasLimit))),
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
func(suite *AnteTestSuite) {
err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, mockHostZoneConfig)
require.NoError(t, err)
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
},
true,
sdkerrors.ErrInsufficientFee,
},

{
"enough ibc fee, should pass",
sdk.NewCoins(sdk.NewInt64Coin("ibcfee", 1000*int64(gasLimit))),
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
func(suite *AnteTestSuite) {
err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, mockHostZoneConfig)
require.NoError(t, err)
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
},
false,
nil,
},
// TODO: Add support for multiple denom fees(--fees 50ibc,50native)
// {
// "half native fee, half ibc fee, should pass",
// sdk.NewCoins(sdk.NewInt64Coin("native", 500*int64(gasLimit)), sdk.NewInt64Coin("ibcfee", 500*int64(gasLimit))),
// sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
// func(suite *AnteTestSuite) {
// err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, types.HostChainFeeAbsConfig{
// IbcDenom: "ibcfee",
// OsmosisPoolTokenDenomIn: "osmosis",
// PoolId: 1,
// Status: types.HostChainFeeAbsStatus_UPDATED,
// MinSwapAmount: 0,
// })
// require.NoError(t, err)
// suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
// suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
// },
// false,
// nil,
// },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
suite := SetupTestSuite(t, true)

tc.malleate(suite)
suite.txBuilder.SetGasLimit(gasLimit)
suite.txBuilder.SetFeeAmount(tc.feeAmount)
suite.ctx = suite.ctx.WithMinGasPrices(tc.minGasPrice)

// Construct tx and run through mempool decorator
tx := suite.txBuilder.GetTx()
mempoolDecorator := ante.NewFeeAbstrationMempoolFeeDecorator(suite.feeabsKeeper)
antehandler := sdk.ChainAnteDecorators(mempoolDecorator)

// Run the ante handler
_, err := antehandler(suite.ctx, tx, false)

if tc.isErr {
require.Error(t, err)
require.ErrorIs(t, err, tc.expErr)
} else {
require.NoError(t, err)
}
})
}
}

func TestDeductFeeDecorator(t *testing.T) {
gasLimit := uint64(200000)
minGasPrice := sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...)
feeAmount := sdk.NewCoins(sdk.NewInt64Coin("native", 1000*int64(gasLimit)))
ibcFeeAmount := sdk.NewCoins(sdk.NewInt64Coin("ibcfee", 1000*int64(gasLimit)))
// mockHostZoneConfig is used to mock the host zone config, with ibcfee as the ibc fee denom to be used as alternative fee
mockHostZoneConfig := types.HostChainFeeAbsConfig{
IbcDenom: "ibcfee",
OsmosisPoolTokenDenomIn: "osmosis",
PoolId: 1,
Status: types.HostChainFeeAbsStatus_UPDATED,
MinSwapAmount: 0,
}
testCases := []struct {
name string
malleate func(*AnteTestSuite)
isErr bool
expErr error
}{
{
"not enough native fee in balance, should fail",
func(suite *AnteTestSuite) {
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
// suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, feeAmount).Return(sdkerrors.ErrInsufficientFee).MinTimes(1)
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), authtypes.FeeCollectorName, feeAmount).Return(sdkerrors.ErrInsufficientFee).MinTimes(1)
},
true,
sdkerrors.ErrInsufficientFunds,
},
{
"enough native fee in balance, should pass",
func(suite *AnteTestSuite) {
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), authtypes.FeeCollectorName, feeAmount).Return(nil).MinTimes(1)
},
false,
nil,
},
{
"not enough ibc fee in balance, should fail",
func(suite *AnteTestSuite) {
err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, mockHostZoneConfig)
require.NoError(t, err)
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
suite.txBuilder.SetFeeAmount(ibcFeeAmount)
suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, ibcFeeAmount).Return(sdkerrors.ErrInsufficientFunds).MinTimes(1)
},
true,
sdkerrors.ErrInsufficientFunds,
},
{
"enough ibc fee in balance, should pass",
func(suite *AnteTestSuite) {
err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, mockHostZoneConfig)
require.NoError(t, err)
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
suite.txBuilder.SetFeeAmount(ibcFeeAmount)
suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, ibcFeeAmount).Return(nil).MinTimes(1)
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), authtypes.FeeCollectorName, feeAmount).Return(nil).MinTimes(1)
},
false,
nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
suite := SetupTestSuite(t, false)
acc := suite.CreateTestAccounts(1)[0]
// default value for gasLimit, feeAmount, feePayer. Use native token fee as default
suite.txBuilder.SetGasLimit(gasLimit)
suite.txBuilder.SetFeeAmount(feeAmount)
suite.txBuilder.SetFeePayer(acc.acc.GetAddress())
suite.ctx = suite.ctx.WithMinGasPrices(minGasPrice)

// mallate the test case, e.g. setup to pay fee in IBC token
tc.malleate(suite)

// Construct tx and run through mempool decorator
tx := suite.txBuilder.GetTx()
deductFeeDecorator := ante.NewFeeAbstractionDeductFeeDecorate(suite.accountKeeper, suite.bankKeeper, suite.feeabsKeeper, suite.feeGrantKeeper)
antehandler := sdk.ChainAnteDecorators(deductFeeDecorator)
_, err := antehandler(suite.ctx, tx, false)

if tc.isErr {
require.Error(t, err)
require.ErrorIs(t, err, tc.expErr)
} else {
require.NoError(t, err)
}
})
}
}
6 changes: 3 additions & 3 deletions x/feeabs/ante/decorate.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func (famfd FeeAbstrationMempoolFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk

// After replace the feeCoinsNonZeroDenom, feeCoinsNonZeroDenom must be in denom subset of nonZeroCoinFeesReq
if !feeCoinsNonZeroDenom.DenomsSubsetOf(nonZeroCoinFeesReq) {
return ctx, sdkerrors.Wrapf(errorstypes.ErrInsufficientFee, "fee is not a subset of required fees; got %s, required: %s", feeCoins.String(), feeRequired.String())
return ctx, sdkerrors.Wrapf(errorstypes.ErrInvalidCoins, "fee is not a subset of required fees; got %s, required: %s", feeCoinsNonZeroDenom.String(), feeRequired.String())
}

// if the msg does not satisfy bypass condition and the feeCoins denoms are subset of fezeRequired,
Expand All @@ -315,8 +315,8 @@ func (famfd FeeAbstrationMempoolFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk
// Not contain zeroCoinFeesDenomReq's denoms
//
// check if the feeCoins has coins' amount higher/equal to nonZeroCoinFeesReq
if !feeCoins.IsAnyGTE(nonZeroCoinFeesReq) {
err := sdkerrors.Wrapf(errorstypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, feeRequired)
if !feeCoinsNonZeroDenom.IsAnyGTE(nonZeroCoinFeesReq) {
err := sdkerrors.Wrapf(errorstypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoinsNonZeroDenom, nonZeroCoinFeesReq)
if byPassExceedMaxGasUsage {
err = sdkerrors.Wrapf(errorstypes.ErrInsufficientFee, "Insufficient fees; bypass-min-fee-msg-types with gas consumption exceeds the maximum allowed gas value.")
}
Expand Down
120 changes: 120 additions & 0 deletions x/feeabs/ante/testutil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package ante_test

import (
"testing"

transferkeeper "github.com/cosmos/ibc-go/v7/modules/apps/transfer/keeper"
"github.com/stretchr/testify/require"
ubermock "go.uber.org/mock/gomock"

"github.com/cosmos/cosmos-sdk/client"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
"github.com/cosmos/cosmos-sdk/testutil"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
"github.com/cosmos/cosmos-sdk/x/auth"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"

feeabskeeper "github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/keeper"
feeabstestutil "github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/testutil"
feeabstypes "github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/types"
)

// TestAccount represents an account used in the tests in x/auth/ante.
type TestAccount struct {
acc authtypes.AccountI
priv cryptotypes.PrivKey
}

// AnteTestSuite is a test suite to be used with ante handler tests.
type AnteTestSuite struct {
ctx sdk.Context
clientCtx client.Context
txBuilder client.TxBuilder
accountKeeper authkeeper.AccountKeeper
bankKeeper *feeabstestutil.MockBankKeeper
feeGrantKeeper *feeabstestutil.MockFeegrantKeeper
stakingKeeper *feeabstestutil.MockStakingKeeper
feeabsKeeper feeabskeeper.Keeper
channelKeeper *feeabstestutil.MockChannelKeeper
portKeeper *feeabstestutil.MockPortKeeper
scopedKeeper *feeabstestutil.MockScopedKeeper
encCfg moduletestutil.TestEncodingConfig
}

// SetupTest setups a new test, with new app, context, and anteHandler.
func SetupTestSuite(t *testing.T, isCheckTx bool) *AnteTestSuite {
t.Helper()
suite := &AnteTestSuite{}
ctrl := ubermock.NewController(t)

// Setup mock keepers
suite.bankKeeper = feeabstestutil.NewMockBankKeeper(ctrl)
suite.stakingKeeper = feeabstestutil.NewMockStakingKeeper(ctrl)
suite.feeGrantKeeper = feeabstestutil.NewMockFeegrantKeeper(ctrl)
suite.channelKeeper = feeabstestutil.NewMockChannelKeeper(ctrl)
suite.portKeeper = feeabstestutil.NewMockPortKeeper(ctrl)
suite.scopedKeeper = feeabstestutil.NewMockScopedKeeper(ctrl)

// setup necessary params for Account Keeper
key := sdk.NewKVStoreKey(feeabstypes.StoreKey)
authKey := sdk.NewKVStoreKey(authtypes.StoreKey)
subspace := paramtypes.NewSubspace(nil, nil, nil, nil, "feeabs")
subspace = subspace.WithKeyTable(feeabstypes.ParamKeyTable())
maccPerms := map[string][]string{
"fee_collector": nil,
"mint": {"minter"},
"bonded_tokens_pool": {"burner", "staking"},
"not_bonded_tokens_pool": {"burner", "staking"},
"multiPerm": {"burner", "minter", "staking"},
"random": {"random"},
"feeabs": nil,
}

// setup context for Account Keeper
testCtx := testutil.DefaultContextWithDB(t, key, sdk.NewTransientStoreKey("transient_test"))
testCtx.CMS.MountStoreWithDB(authKey, storetypes.StoreTypeIAVL, testCtx.DB)
testCtx.CMS.MountStoreWithDB(sdk.NewTransientStoreKey("transient_test2"), storetypes.StoreTypeTransient, testCtx.DB)
err := testCtx.CMS.LoadLatestVersion()
require.NoError(t, err)
suite.ctx = testCtx.Ctx.WithIsCheckTx(isCheckTx).WithBlockHeight(1) // app.BaseApp.NewContext(isCheckTx, tmproto.Header{}).WithBlockHeight(1)

suite.encCfg = moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{})
suite.encCfg.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil)
testdata.RegisterInterfaces(suite.encCfg.InterfaceRegistry)
suite.accountKeeper = authkeeper.NewAccountKeeper(
suite.encCfg.Codec, authKey, authtypes.ProtoBaseAccount, maccPerms, sdk.Bech32MainPrefix, authtypes.NewModuleAddress("gov").String(),
)
suite.accountKeeper.SetModuleAccount(suite.ctx, authtypes.NewEmptyModuleAccount(feeabstypes.ModuleName))
// Setup feeabs keeper
suite.feeabsKeeper = feeabskeeper.NewKeeper(suite.encCfg.Codec, key, subspace, suite.stakingKeeper, suite.accountKeeper, nil, transferkeeper.Keeper{}, suite.channelKeeper, suite.portKeeper, suite.scopedKeeper)
suite.clientCtx = client.Context{}.
WithTxConfig(suite.encCfg.TxConfig)
require.NoError(t, err)

// setup txBuilder
suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder()

return suite
}

func (suite *AnteTestSuite) CreateTestAccounts(numAccs int) []TestAccount {
var accounts []TestAccount

for i := 0; i < numAccs; i++ {
priv, _, addr := testdata.KeyTestPubAddr()
acc := suite.accountKeeper.NewAccountWithAddress(suite.ctx, addr)
err := acc.SetAccountNumber(uint64(i))
if err != nil {
panic(err)
}
suite.accountKeeper.SetAccount(suite.ctx, acc)
accounts = append(accounts, TestAccount{acc, priv})
}

return accounts
}
Loading

0 comments on commit f2241e6

Please sign in to comment.