From 2a6c50f899b8cd59116ee48fd30704f17386c93b Mon Sep 17 00:00:00 2001 From: Teddy Ding Date: Fri, 23 Aug 2024 11:15:20 -0400 Subject: [PATCH] Implement gas-free WasmExecution for accounts with sufficient balance --- x/auth/ante/fee.go | 5 ++ x/auth/ante/wasm_execution.go | 41 ++++++++++++ x/auth/ante/wasm_execution_test.go | 82 +++++++++++++++++++++++ x/auth/testutil/expected_keepers_mocks.go | 14 ++++ x/auth/types/expected_keepers.go | 1 + 5 files changed, 143 insertions(+) create mode 100644 x/auth/ante/wasm_execution.go create mode 100644 x/auth/ante/wasm_execution_test.go diff --git a/x/auth/ante/fee.go b/x/auth/ante/fee.go index 7581765ecf7d..a25f2f3f7b31 100644 --- a/x/auth/ante/fee.go +++ b/x/auth/ante/fee.go @@ -106,6 +106,11 @@ func (dfd DeductFeeDecorator) checkDeductFee(ctx sdk.Context, sdkTx sdk.Tx, fee return sdkerrors.ErrUnknownAddress.Wrapf("fee payer address: %s does not exist", deductFeesFrom) } + // Skip deducting fees it's a qualified Wasm Execution transaction. + if IsSingleWasmExecTx(sdkTx) && WasmExecExemptFromGas(dfd.bankKeeper, ctx, deductFeesFromAcc) { + return nil + } + // deduct the fees if !fee.IsZero() { err := DeductFees(dfd.bankKeeper, ctx, deductFeesFromAcc, fee) diff --git a/x/auth/ante/wasm_execution.go b/x/auth/ante/wasm_execution.go new file mode 100644 index 000000000000..f0262c671387 --- /dev/null +++ b/x/auth/ante/wasm_execution.go @@ -0,0 +1,41 @@ +package ante + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +const ( + // The IBC denom of `uusdc` (atomic unit of Noble USDC) + UusdcDenom = "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5" + UusdcBalanceForGasExemption = 2_000_000_000 // 2_000 Noble USDC +) + +// Returns true if the transaction includes a single MsgExecuteContract message. +// False otherwise. +func IsSingleWasmExecTx( + sdkTx sdk.Tx, +) bool { + msgs := sdkTx.GetMsgs() + if len(msgs) > 1 { + return false + } + + for _, msg := range msgs { + if sdk.MsgTypeURL(msg) == "/cosmwasm.wasm.v1.MsgExecuteContract" { + return true + } + } + return false +} + +// Returns true if USDC balance is sufficient for gas exemption. +func WasmExecExemptFromGas( + bankKeeper types.BankKeeper, + ctx sdk.Context, + deductFeeFromAcc sdk.AccountI, +) bool { + balance := bankKeeper.GetBalance(ctx, deductFeeFromAcc.GetAddress(), UusdcDenom) + return balance.Amount.GTE(math.NewInt(UusdcBalanceForGasExemption)) +} diff --git a/x/auth/ante/wasm_execution_test.go b/x/auth/ante/wasm_execution_test.go new file mode 100644 index 000000000000..4c2fc3a21e31 --- /dev/null +++ b/x/auth/ante/wasm_execution_test.go @@ -0,0 +1,82 @@ +package ante_test + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + "github.com/golang/mock/gomock" + + "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/stretchr/testify/require" +) + +// MockMsgExecuteContract is a mock message for testing purposes. +type MockMsgExecuteContract struct{} + +func (m MockMsgExecuteContract) Reset() {} +func (m MockMsgExecuteContract) String() string { return "MockMsgExecuteContract" } +func (m MockMsgExecuteContract) ProtoMessage() {} + +func (m MockMsgExecuteContract) XXX_MessageName() string { + return "cosmwasm.wasm.v1.MsgExecuteContract" +} + +func TestIsSingleWasmExecTx(t *testing.T) { + // Create a mock message with the desired TypeURL + msg := &MockMsgExecuteContract{} + + suite := SetupTestSuite(t, true) + suite.txBuilder = suite.clientCtx.TxConfig.NewTxBuilder() + + err := suite.txBuilder.SetMsgs(msg) + require.NoError(t, err) + + // Create the transaction + sdkTx := suite.txBuilder.GetTx() + + // Test the function + result := ante.IsSingleWasmExecTx(sdkTx) + require.True(t, result, "Expected IsSingleWasmExecTx to return true for a single MsgExecuteContract message") + + // Test with multiple messages + err = suite.txBuilder.SetMsgs(msg, msg) + require.NoError(t, err) + sdkTx = suite.txBuilder.GetTx() + result = ante.IsSingleWasmExecTx(sdkTx) + require.False(t, result, "Expected IsSingleWasmExecTx to return false for multiple messages") +} + +func TestWasmExecExemptFromGas(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Setup the test suite + suite := SetupTestSuite(t, false) + + // Create a mock account + acc := &types.BaseAccount{} + acc.SetAddress(sdk.AccAddress([]byte("test_address"))) + + // Set up the mock bank keeper to return a specific balance + suite.bankKeeper.EXPECT(). + GetBalance(gomock.Any(), acc.GetAddress(), ante.UusdcDenom). + Return(sdk.Coin{Denom: ante.UusdcDenom, Amount: sdkmath.NewInt(ante.UusdcBalanceForGasExemption)}). + Times(1) + + // Test the function with sufficient balance + result := ante.WasmExecExemptFromGas(suite.bankKeeper, suite.ctx, acc) + require.True(t, result, "Expected WasmExecExemptFromGas to return true for sufficient balance") + + // Set up the mock bank keeper to return an insufficient balance + suite.bankKeeper.EXPECT(). + GetBalance(gomock.Any(), acc.GetAddress(), ante.UusdcDenom). + Return(sdk.Coin{Denom: ante.UusdcDenom, Amount: sdkmath.NewInt(ante.UusdcBalanceForGasExemption - 1)}). + Times(1) + + // Test the function with insufficient balance + result = ante.WasmExecExemptFromGas(suite.bankKeeper, suite.ctx, acc) + require.False(t, result, "Expected WasmExecExemptFromGas to return false for insufficient balance") +} diff --git a/x/auth/testutil/expected_keepers_mocks.go b/x/auth/testutil/expected_keepers_mocks.go index 2d0e602a8cbc..37ec7cb15aa1 100644 --- a/x/auth/testutil/expected_keepers_mocks.go +++ b/x/auth/testutil/expected_keepers_mocks.go @@ -35,6 +35,20 @@ func (m *MockBankKeeper) EXPECT() *MockBankKeeperMockRecorder { return m.recorder } +// GetBalance mocks base method. +func (m *MockBankKeeper) GetBalance(ctx context.Context, addr types.AccAddress, denom string) types.Coin { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBalance", ctx, addr, denom) + ret0, _ := ret[0].(types.Coin) + return ret0 +} + +// GetBalance indicates an expected call of GetBalance. +func (mr *MockBankKeeperMockRecorder) GetBalance(ctx, addr, denom interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBalance", reflect.TypeOf((*MockBankKeeper)(nil).GetBalance), ctx, addr, denom) +} + // IsSendEnabledCoins mocks base method. func (m *MockBankKeeper) IsSendEnabledCoins(ctx context.Context, coins ...types.Coin) error { m.ctrl.T.Helper() diff --git a/x/auth/types/expected_keepers.go b/x/auth/types/expected_keepers.go index ffcfe49aa10d..898ccc10204c 100644 --- a/x/auth/types/expected_keepers.go +++ b/x/auth/types/expected_keepers.go @@ -11,4 +11,5 @@ type BankKeeper interface { IsSendEnabledCoins(ctx context.Context, coins ...sdk.Coin) error SendCoins(ctx context.Context, from, to sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + GetBalance(ctx context.Context, addr sdk.AccAddress, denom string) sdk.Coin }