Skip to content

Commit f2241e6

Browse files
authored
Test: add unit tests for decorators (#182)
* 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
1 parent b879162 commit f2241e6

File tree

6 files changed

+984
-7
lines changed

6 files changed

+984
-7
lines changed

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/spf13/cast v1.5.1
2020
github.com/spf13/cobra v1.8.0
2121
github.com/stretchr/testify v1.8.4
22+
go.uber.org/mock v0.2.0
2223
golang.org/x/tools v0.6.0
2324
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0
2425
google.golang.org/grpc v1.60.1
@@ -233,7 +234,7 @@ require (
233234
github.com/gogo/googleapis v1.4.1 // indirect
234235
github.com/golang/glog v1.1.2 // indirect
235236
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
236-
github.com/golang/mock v1.6.0 // indirect
237+
github.com/golang/mock v1.6.0
237238
github.com/golang/snappy v0.0.4 // indirect
238239
github.com/google/btree v1.1.2 // indirect
239240
github.com/google/go-cmp v0.6.0 // indirect

x/feeabs/ante/ante_test.go

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package ante_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/golang/mock/gomock"
7+
"github.com/stretchr/testify/require"
8+
9+
sdk "github.com/cosmos/cosmos-sdk/types"
10+
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
11+
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
12+
13+
"github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/ante"
14+
"github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/types"
15+
)
16+
17+
func TestMempoolDecorator(t *testing.T) {
18+
gasLimit := uint64(200000)
19+
// mockHostZoneConfig is used to mock the host zone config, with ibcfee as the ibc fee denom to be used as alternative fee
20+
mockHostZoneConfig := types.HostChainFeeAbsConfig{
21+
IbcDenom: "ibcfee",
22+
OsmosisPoolTokenDenomIn: "osmosis",
23+
PoolId: 1,
24+
Status: types.HostChainFeeAbsStatus_UPDATED,
25+
MinSwapAmount: 0,
26+
}
27+
testCases := []struct {
28+
name string
29+
feeAmount sdk.Coins
30+
minGasPrice sdk.DecCoins
31+
malleate func(*AnteTestSuite)
32+
isErr bool
33+
expErr error
34+
}{
35+
{
36+
"empty fee, should fail",
37+
sdk.Coins{},
38+
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 100))...),
39+
func(suite *AnteTestSuite) {
40+
},
41+
true,
42+
sdkerrors.ErrInsufficientFee,
43+
},
44+
{
45+
"not enough native fee, should fail",
46+
sdk.NewCoins(sdk.NewInt64Coin("native", 100)),
47+
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
48+
func(suite *AnteTestSuite) {},
49+
true,
50+
sdkerrors.ErrInsufficientFee,
51+
},
52+
{
53+
"enough native fee, should pass",
54+
sdk.NewCoins(sdk.NewInt64Coin("native", 1000*int64(gasLimit))),
55+
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
56+
func(suite *AnteTestSuite) {},
57+
false,
58+
nil,
59+
},
60+
{
61+
"unknown ibc fee denom, should fail",
62+
sdk.NewCoins(sdk.NewInt64Coin("ibcfee", 1000*int64(gasLimit))),
63+
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
64+
func(suite *AnteTestSuite) {},
65+
true,
66+
sdkerrors.ErrInvalidCoins,
67+
},
68+
{
69+
"not enough ibc fee, should fail",
70+
sdk.NewCoins(sdk.NewInt64Coin("ibcfee", 999*int64(gasLimit))),
71+
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
72+
func(suite *AnteTestSuite) {
73+
err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, mockHostZoneConfig)
74+
require.NoError(t, err)
75+
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
76+
suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
77+
},
78+
true,
79+
sdkerrors.ErrInsufficientFee,
80+
},
81+
82+
{
83+
"enough ibc fee, should pass",
84+
sdk.NewCoins(sdk.NewInt64Coin("ibcfee", 1000*int64(gasLimit))),
85+
sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
86+
func(suite *AnteTestSuite) {
87+
err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, mockHostZoneConfig)
88+
require.NoError(t, err)
89+
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
90+
suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
91+
},
92+
false,
93+
nil,
94+
},
95+
// TODO: Add support for multiple denom fees(--fees 50ibc,50native)
96+
// {
97+
// "half native fee, half ibc fee, should pass",
98+
// sdk.NewCoins(sdk.NewInt64Coin("native", 500*int64(gasLimit)), sdk.NewInt64Coin("ibcfee", 500*int64(gasLimit))),
99+
// sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...),
100+
// func(suite *AnteTestSuite) {
101+
// err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, types.HostChainFeeAbsConfig{
102+
// IbcDenom: "ibcfee",
103+
// OsmosisPoolTokenDenomIn: "osmosis",
104+
// PoolId: 1,
105+
// Status: types.HostChainFeeAbsStatus_UPDATED,
106+
// MinSwapAmount: 0,
107+
// })
108+
// require.NoError(t, err)
109+
// suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
110+
// suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
111+
// },
112+
// false,
113+
// nil,
114+
// },
115+
}
116+
for _, tc := range testCases {
117+
t.Run(tc.name, func(t *testing.T) {
118+
suite := SetupTestSuite(t, true)
119+
120+
tc.malleate(suite)
121+
suite.txBuilder.SetGasLimit(gasLimit)
122+
suite.txBuilder.SetFeeAmount(tc.feeAmount)
123+
suite.ctx = suite.ctx.WithMinGasPrices(tc.minGasPrice)
124+
125+
// Construct tx and run through mempool decorator
126+
tx := suite.txBuilder.GetTx()
127+
mempoolDecorator := ante.NewFeeAbstrationMempoolFeeDecorator(suite.feeabsKeeper)
128+
antehandler := sdk.ChainAnteDecorators(mempoolDecorator)
129+
130+
// Run the ante handler
131+
_, err := antehandler(suite.ctx, tx, false)
132+
133+
if tc.isErr {
134+
require.Error(t, err)
135+
require.ErrorIs(t, err, tc.expErr)
136+
} else {
137+
require.NoError(t, err)
138+
}
139+
})
140+
}
141+
}
142+
143+
func TestDeductFeeDecorator(t *testing.T) {
144+
gasLimit := uint64(200000)
145+
minGasPrice := sdk.NewDecCoinsFromCoins(sdk.NewCoins(sdk.NewInt64Coin("native", 1000))...)
146+
feeAmount := sdk.NewCoins(sdk.NewInt64Coin("native", 1000*int64(gasLimit)))
147+
ibcFeeAmount := sdk.NewCoins(sdk.NewInt64Coin("ibcfee", 1000*int64(gasLimit)))
148+
// mockHostZoneConfig is used to mock the host zone config, with ibcfee as the ibc fee denom to be used as alternative fee
149+
mockHostZoneConfig := types.HostChainFeeAbsConfig{
150+
IbcDenom: "ibcfee",
151+
OsmosisPoolTokenDenomIn: "osmosis",
152+
PoolId: 1,
153+
Status: types.HostChainFeeAbsStatus_UPDATED,
154+
MinSwapAmount: 0,
155+
}
156+
testCases := []struct {
157+
name string
158+
malleate func(*AnteTestSuite)
159+
isErr bool
160+
expErr error
161+
}{
162+
{
163+
"not enough native fee in balance, should fail",
164+
func(suite *AnteTestSuite) {
165+
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
166+
// suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, feeAmount).Return(sdkerrors.ErrInsufficientFee).MinTimes(1)
167+
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), authtypes.FeeCollectorName, feeAmount).Return(sdkerrors.ErrInsufficientFee).MinTimes(1)
168+
},
169+
true,
170+
sdkerrors.ErrInsufficientFunds,
171+
},
172+
{
173+
"enough native fee in balance, should pass",
174+
func(suite *AnteTestSuite) {
175+
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
176+
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), authtypes.FeeCollectorName, feeAmount).Return(nil).MinTimes(1)
177+
},
178+
false,
179+
nil,
180+
},
181+
{
182+
"not enough ibc fee in balance, should fail",
183+
func(suite *AnteTestSuite) {
184+
err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, mockHostZoneConfig)
185+
require.NoError(t, err)
186+
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
187+
suite.txBuilder.SetFeeAmount(ibcFeeAmount)
188+
suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
189+
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, ibcFeeAmount).Return(sdkerrors.ErrInsufficientFunds).MinTimes(1)
190+
},
191+
true,
192+
sdkerrors.ErrInsufficientFunds,
193+
},
194+
{
195+
"enough ibc fee in balance, should pass",
196+
func(suite *AnteTestSuite) {
197+
err := suite.feeabsKeeper.SetHostZoneConfig(suite.ctx, mockHostZoneConfig)
198+
require.NoError(t, err)
199+
suite.feeabsKeeper.SetTwapRate(suite.ctx, "ibcfee", sdk.NewDec(1))
200+
suite.txBuilder.SetFeeAmount(ibcFeeAmount)
201+
suite.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("native").MinTimes(1)
202+
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, ibcFeeAmount).Return(nil).MinTimes(1)
203+
suite.bankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), authtypes.FeeCollectorName, feeAmount).Return(nil).MinTimes(1)
204+
},
205+
false,
206+
nil,
207+
},
208+
}
209+
for _, tc := range testCases {
210+
t.Run(tc.name, func(t *testing.T) {
211+
suite := SetupTestSuite(t, false)
212+
acc := suite.CreateTestAccounts(1)[0]
213+
// default value for gasLimit, feeAmount, feePayer. Use native token fee as default
214+
suite.txBuilder.SetGasLimit(gasLimit)
215+
suite.txBuilder.SetFeeAmount(feeAmount)
216+
suite.txBuilder.SetFeePayer(acc.acc.GetAddress())
217+
suite.ctx = suite.ctx.WithMinGasPrices(minGasPrice)
218+
219+
// mallate the test case, e.g. setup to pay fee in IBC token
220+
tc.malleate(suite)
221+
222+
// Construct tx and run through mempool decorator
223+
tx := suite.txBuilder.GetTx()
224+
deductFeeDecorator := ante.NewFeeAbstractionDeductFeeDecorate(suite.accountKeeper, suite.bankKeeper, suite.feeabsKeeper, suite.feeGrantKeeper)
225+
antehandler := sdk.ChainAnteDecorators(deductFeeDecorator)
226+
_, err := antehandler(suite.ctx, tx, false)
227+
228+
if tc.isErr {
229+
require.Error(t, err)
230+
require.ErrorIs(t, err, tc.expErr)
231+
} else {
232+
require.NoError(t, err)
233+
}
234+
})
235+
}
236+
}

x/feeabs/ante/decorate.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ func (famfd FeeAbstrationMempoolFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk
291291

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

297297
// if the msg does not satisfy bypass condition and the feeCoins denoms are subset of fezeRequired,
@@ -315,8 +315,8 @@ func (famfd FeeAbstrationMempoolFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk
315315
// Not contain zeroCoinFeesDenomReq's denoms
316316
//
317317
// check if the feeCoins has coins' amount higher/equal to nonZeroCoinFeesReq
318-
if !feeCoins.IsAnyGTE(nonZeroCoinFeesReq) {
319-
err := sdkerrors.Wrapf(errorstypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, feeRequired)
318+
if !feeCoinsNonZeroDenom.IsAnyGTE(nonZeroCoinFeesReq) {
319+
err := sdkerrors.Wrapf(errorstypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoinsNonZeroDenom, nonZeroCoinFeesReq)
320320
if byPassExceedMaxGasUsage {
321321
err = sdkerrors.Wrapf(errorstypes.ErrInsufficientFee, "Insufficient fees; bypass-min-fee-msg-types with gas consumption exceeds the maximum allowed gas value.")
322322
}

x/feeabs/ante/testutil_test.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package ante_test
2+
3+
import (
4+
"testing"
5+
6+
transferkeeper "github.com/cosmos/ibc-go/v7/modules/apps/transfer/keeper"
7+
"github.com/stretchr/testify/require"
8+
ubermock "go.uber.org/mock/gomock"
9+
10+
"github.com/cosmos/cosmos-sdk/client"
11+
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
12+
storetypes "github.com/cosmos/cosmos-sdk/store/types"
13+
"github.com/cosmos/cosmos-sdk/testutil"
14+
"github.com/cosmos/cosmos-sdk/testutil/testdata"
15+
sdk "github.com/cosmos/cosmos-sdk/types"
16+
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
17+
"github.com/cosmos/cosmos-sdk/x/auth"
18+
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
19+
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
20+
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
21+
22+
feeabskeeper "github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/keeper"
23+
feeabstestutil "github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/testutil"
24+
feeabstypes "github.com/osmosis-labs/fee-abstraction/v7/x/feeabs/types"
25+
)
26+
27+
// TestAccount represents an account used in the tests in x/auth/ante.
28+
type TestAccount struct {
29+
acc authtypes.AccountI
30+
priv cryptotypes.PrivKey
31+
}
32+
33+
// AnteTestSuite is a test suite to be used with ante handler tests.
34+
type AnteTestSuite struct {
35+
ctx sdk.Context
36+
clientCtx client.Context
37+
txBuilder client.TxBuilder
38+
accountKeeper authkeeper.AccountKeeper
39+
bankKeeper *feeabstestutil.MockBankKeeper
40+
feeGrantKeeper *feeabstestutil.MockFeegrantKeeper
41+
stakingKeeper *feeabstestutil.MockStakingKeeper
42+
feeabsKeeper feeabskeeper.Keeper
43+
channelKeeper *feeabstestutil.MockChannelKeeper
44+
portKeeper *feeabstestutil.MockPortKeeper
45+
scopedKeeper *feeabstestutil.MockScopedKeeper
46+
encCfg moduletestutil.TestEncodingConfig
47+
}
48+
49+
// SetupTest setups a new test, with new app, context, and anteHandler.
50+
func SetupTestSuite(t *testing.T, isCheckTx bool) *AnteTestSuite {
51+
t.Helper()
52+
suite := &AnteTestSuite{}
53+
ctrl := ubermock.NewController(t)
54+
55+
// Setup mock keepers
56+
suite.bankKeeper = feeabstestutil.NewMockBankKeeper(ctrl)
57+
suite.stakingKeeper = feeabstestutil.NewMockStakingKeeper(ctrl)
58+
suite.feeGrantKeeper = feeabstestutil.NewMockFeegrantKeeper(ctrl)
59+
suite.channelKeeper = feeabstestutil.NewMockChannelKeeper(ctrl)
60+
suite.portKeeper = feeabstestutil.NewMockPortKeeper(ctrl)
61+
suite.scopedKeeper = feeabstestutil.NewMockScopedKeeper(ctrl)
62+
63+
// setup necessary params for Account Keeper
64+
key := sdk.NewKVStoreKey(feeabstypes.StoreKey)
65+
authKey := sdk.NewKVStoreKey(authtypes.StoreKey)
66+
subspace := paramtypes.NewSubspace(nil, nil, nil, nil, "feeabs")
67+
subspace = subspace.WithKeyTable(feeabstypes.ParamKeyTable())
68+
maccPerms := map[string][]string{
69+
"fee_collector": nil,
70+
"mint": {"minter"},
71+
"bonded_tokens_pool": {"burner", "staking"},
72+
"not_bonded_tokens_pool": {"burner", "staking"},
73+
"multiPerm": {"burner", "minter", "staking"},
74+
"random": {"random"},
75+
"feeabs": nil,
76+
}
77+
78+
// setup context for Account Keeper
79+
testCtx := testutil.DefaultContextWithDB(t, key, sdk.NewTransientStoreKey("transient_test"))
80+
testCtx.CMS.MountStoreWithDB(authKey, storetypes.StoreTypeIAVL, testCtx.DB)
81+
testCtx.CMS.MountStoreWithDB(sdk.NewTransientStoreKey("transient_test2"), storetypes.StoreTypeTransient, testCtx.DB)
82+
err := testCtx.CMS.LoadLatestVersion()
83+
require.NoError(t, err)
84+
suite.ctx = testCtx.Ctx.WithIsCheckTx(isCheckTx).WithBlockHeight(1) // app.BaseApp.NewContext(isCheckTx, tmproto.Header{}).WithBlockHeight(1)
85+
86+
suite.encCfg = moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{})
87+
suite.encCfg.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil)
88+
testdata.RegisterInterfaces(suite.encCfg.InterfaceRegistry)
89+
suite.accountKeeper = authkeeper.NewAccountKeeper(
90+
suite.encCfg.Codec, authKey, authtypes.ProtoBaseAccount, maccPerms, sdk.Bech32MainPrefix, authtypes.NewModuleAddress("gov").String(),
91+
)
92+
suite.accountKeeper.SetModuleAccount(suite.ctx, authtypes.NewEmptyModuleAccount(feeabstypes.ModuleName))
93+
// Setup feeabs keeper
94+
suite.feeabsKeeper = feeabskeeper.NewKeeper(suite.encCfg.Codec, key, subspace, suite.stakingKeeper, suite.accountKeeper, nil, transferkeeper.Keeper{}, suite.channelKeeper, suite.portKeeper, suite.scopedKeeper)
95+
suite.clientCtx = client.Context{}.
96+
WithTxConfig(suite.encCfg.TxConfig)
97+
require.NoError(t, err)
98+
99+
// setup txBuilder
100+
suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder()
101+
102+
return suite
103+
}
104+
105+
func (suite *AnteTestSuite) CreateTestAccounts(numAccs int) []TestAccount {
106+
var accounts []TestAccount
107+
108+
for i := 0; i < numAccs; i++ {
109+
priv, _, addr := testdata.KeyTestPubAddr()
110+
acc := suite.accountKeeper.NewAccountWithAddress(suite.ctx, addr)
111+
err := acc.SetAccountNumber(uint64(i))
112+
if err != nil {
113+
panic(err)
114+
}
115+
suite.accountKeeper.SetAccount(suite.ctx, acc)
116+
accounts = append(accounts, TestAccount{acc, priv})
117+
}
118+
119+
return accounts
120+
}

0 commit comments

Comments
 (0)