Skip to content

Commit

Permalink
test(evm): unit tests for evm_ante (#1912)
Browse files Browse the repository at this point in the history
* test(evm): unit tests for evm_ante_sigverify

* chore: changelog update

* test(evm): ante gas wanted full coverage

* test(evm): ante handler setup ctx coverage

* test(evm): ante handler emit event coverage

* test(evm): evmante hanlers emit_event, setup_ctx and validate_basic

* fix: lint

* test(evm): evmante fee checker coverage

* test(evm): evmante fees test coverage

* test(evm): split evmante handlers 1 handler 1 file

* test(evm): evmante tests for can_transfer, fee_checker, gas_comsume

* test(evm): evmante increment_sender_seq, validate_basic, gas_consume, reject_msgs, can_transfer

* test: tried -v flag for integration tests
  • Loading branch information
onikonychev authored Jun 11, 2024
1 parent 42a8b65 commit 6511f86
Show file tree
Hide file tree
Showing 28 changed files with 1,969 additions and 620 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#1907](https://github.com/NibiruChain/nibiru/pull/1907) - test(evm): grpc_query full coverage
- [#1909](https://github.com/NibiruChain/nibiru/pull/1909) - chore(evm): set is_london true by default and removed from config
- [#1911](https://github.com/NibiruChain/nibiru/pull/1911) - chore(evm): simplified config by removing old eth forks
- [#1914](https://github.com/NibiruChain/nibiru/pull/1914) - refactor(evm): Remove dead code and document non-EVM ante handler
- [#1912](https://github.com/NibiruChain/nibiru/pull/1912) - test(evm): unit tests for evm_ante
- [#1914](https://github.com/NibiruChain/nibiru/pull/1914) - refactor(evm): Remove dead code and document non-EVM ante handler- [#1917](https://github.com/NibiruChain/nibiru/pull/1917) - test(e2e-evm): TypeScript support. Type generation from compiled contracts. Formatter for TS code.
- [#1917](https://github.com/NibiruChain/nibiru/pull/1917) - test(e2e-evm): TypeScript support. Type generation from compiled contracts. Formatter for TS code.

#### Dapp modules: perp, spot, oracle, etc
Expand Down
101 changes: 101 additions & 0 deletions app/evmante_can_transfer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) 2023-2024 Nibi, Inc.
package app

import (
"math/big"

"cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/NibiruChain/nibiru/x/evm"
"github.com/NibiruChain/nibiru/x/evm/statedb"

gethcommon "github.com/ethereum/go-ethereum/common"
gethcore "github.com/ethereum/go-ethereum/core/types"
)

// CanTransferDecorator checks if the sender is allowed to transfer funds according to the EVM block
// context rules.
type CanTransferDecorator struct {
AppKeepers
}

// NewCanTransferDecorator creates a new CanTransferDecorator instance.
func NewCanTransferDecorator(k AppKeepers) CanTransferDecorator {
return CanTransferDecorator{
AppKeepers: k,
}
}

// AnteHandle creates an EVM from the message and calls the BlockContext CanTransfer function to
// see if the address can execute the transaction.
func (ctd CanTransferDecorator) AnteHandle(
ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler,
) (sdk.Context, error) {
params := ctd.EvmKeeper.GetParams(ctx)
ethCfg := evm.EthereumConfig(ctd.EvmKeeper.EthChainID(ctx))
signer := gethcore.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight()))

for _, msg := range tx.GetMsgs() {
msgEthTx, ok := msg.(*evm.MsgEthereumTx)
if !ok {
return ctx, errors.Wrapf(
errortypes.ErrUnknownRequest,
"invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil),
)
}
baseFee := ctd.EvmKeeper.GetBaseFee(ctx)

coreMsg, err := msgEthTx.AsMessage(signer, baseFee)
if err != nil {
return ctx, errors.Wrapf(
err,
"failed to create an ethereum core.Message from signer %T", signer,
)
}

if baseFee == nil {
return ctx, errors.Wrap(
evm.ErrInvalidBaseFee,
"base fee is supported but evm block context value is nil",
)
}
if coreMsg.GasFeeCap().Cmp(baseFee) < 0 {
return ctx, errors.Wrapf(
errortypes.ErrInsufficientFee,
"max fee per gas less than block base fee (%s < %s)",
coreMsg.GasFeeCap(), baseFee,
)
}

// NOTE: pass in an empty coinbase address and nil tracer as we don't need them for the check below
cfg := &statedb.EVMConfig{
ChainConfig: ethCfg,
Params: params,
CoinBase: gethcommon.Address{},
BaseFee: baseFee,
}

stateDB := statedb.New(
ctx,
&ctd.EvmKeeper,
statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())),
)
evmInstance := ctd.EvmKeeper.NewEVM(ctx, coreMsg, cfg, evm.NewNoOpTracer(), stateDB)

// check that caller has enough balance to cover asset transfer for **topmost** call
// NOTE: here the gas consumed is from the context with the infinite gas meter
if coreMsg.Value().Sign() > 0 &&
!evmInstance.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) {
return ctx, errors.Wrapf(
errortypes.ErrInsufficientFunds,
"failed to transfer %s from address %s using the EVM block context transfer function",
coreMsg.Value(),
coreMsg.From(),
)
}
}

return next(ctx, tx, simulate)
}
109 changes: 109 additions & 0 deletions app/evmante_can_transfer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package app_test

import (
"math/big"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/NibiruChain/nibiru/app"
"github.com/NibiruChain/nibiru/eth"
"github.com/NibiruChain/nibiru/x/evm/evmtest"
"github.com/NibiruChain/nibiru/x/evm/statedb"
)

func (s *TestSuite) TestCanTransferDecorator() {
testCases := []struct {
name string
txSetup func(deps *evmtest.TestDeps) sdk.FeeTx
ctxSetup func(deps *evmtest.TestDeps)
beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB)
wantErr string
}{
{
name: "happy: signed tx, sufficient funds",
beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) {
sdb.AddBalance(deps.Sender.EthAddr, big.NewInt(100))
},
txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx {
txMsg := happyTransfertTx(deps, 0)
txBuilder := deps.EncCfg.TxConfig.NewTxBuilder()

gethSigner := deps.Sender.GethSigner(deps.Chain.EvmKeeper.EthChainID(deps.Ctx))
keyringSigner := deps.Sender.KeyringSigner
err := txMsg.Sign(gethSigner, keyringSigner)
s.Require().NoError(err)

tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom)
s.Require().NoError(err)

return tx
},
wantErr: "",
},
{
name: "sad: signed tx, insufficient funds",
txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx {
txMsg := happyTransfertTx(deps, 0)
txBuilder := deps.EncCfg.TxConfig.NewTxBuilder()

gethSigner := deps.Sender.GethSigner(deps.Chain.EvmKeeper.EthChainID(deps.Ctx))
keyringSigner := deps.Sender.KeyringSigner
err := txMsg.Sign(gethSigner, keyringSigner)
s.Require().NoError(err)

tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom)
s.Require().NoError(err)

return tx
},
wantErr: "insufficient funds",
},
{
name: "sad: unsigned tx",
txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx {
txMsg := happyTransfertTx(deps, 0)
txBuilder := deps.EncCfg.TxConfig.NewTxBuilder()

tx, err := txMsg.BuildTx(txBuilder, eth.EthBaseDenom)
s.Require().NoError(err)

return tx
},
wantErr: "invalid transaction",
},
{
name: "sad: tx with non evm message",
txSetup: func(deps *evmtest.TestDeps) sdk.FeeTx {
return nonEvmMsgTx(deps).(sdk.FeeTx)
},
wantErr: "invalid message",
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
deps := evmtest.NewTestDeps()
stateDB := deps.StateDB()
anteDec := app.NewCanTransferDecorator(deps.Chain.AppKeepers)
tx := tc.txSetup(&deps)

if tc.ctxSetup != nil {
tc.ctxSetup(&deps)
}
if tc.beforeTxSetup != nil {
tc.beforeTxSetup(&deps, stateDB)
err := stateDB.Commit()
s.Require().NoError(err)
}

_, err := anteDec.AnteHandle(
deps.Ctx, tx, false, NextNoOpAnteHandler,
)
if tc.wantErr != "" {
s.Require().ErrorContains(err, tc.wantErr)
return
}
s.Require().NoError(err)
})
}
}
59 changes: 59 additions & 0 deletions app/evmante_emit_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2023-2024 Nibi, Inc.
package app

import (
"strconv"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/NibiruChain/nibiru/x/evm"
)

// EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit).
type EthEmitEventDecorator struct {
AppKeepers
}

// NewEthEmitEventDecorator creates a new EthEmitEventDecorator
func NewEthEmitEventDecorator(k AppKeepers) EthEmitEventDecorator {
return EthEmitEventDecorator{AppKeepers: k}
}

// AnteHandle emits some basic events for the eth messages
func (eeed EthEmitEventDecorator) AnteHandle(
ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler,
) (newCtx sdk.Context, err error) {
// After eth tx passed ante handler, the fee is deducted and nonce increased,
// it shouldn't be ignored by json-rpc. We need to emit some events at the
// very end of ante handler to be indexed by the consensus engine.
txIndex := eeed.EvmKeeper.EVMState().BlockTxIndex.GetOr(ctx, 0)

for i, msg := range tx.GetMsgs() {
msgEthTx, ok := msg.(*evm.MsgEthereumTx)
if !ok {
return ctx, errorsmod.Wrapf(
errortypes.ErrUnknownRequest,
"invalid message type %T, expected %T",
msg, (*evm.MsgEthereumTx)(nil),
)
}

// emit ethereum tx hash as an event so that it can be indexed by
// Tendermint for query purposes it's emitted in ante handler, so we can
// query failed transaction (out of block gas limit).
ctx.EventManager().EmitEvent(
sdk.NewEvent(
evm.EventTypeEthereumTx,
sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash),
sdk.NewAttribute(
evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i),
10,
),
), // #nosec G701
))
}

return next(ctx, tx, simulate)
}
79 changes: 79 additions & 0 deletions app/evmante_emit_event_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package app_test

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx"

"github.com/NibiruChain/nibiru/x/evm"

"github.com/NibiruChain/nibiru/app"
"github.com/NibiruChain/nibiru/x/evm/evmtest"
tf "github.com/NibiruChain/nibiru/x/tokenfactory/types"
)

func (s *TestSuite) TestEthEmitEventDecorator() {
testCases := []struct {
name string
txSetup func(deps *evmtest.TestDeps) sdk.Tx
wantErr string
}{
{
name: "sad: non ethereum tx",
txSetup: func(deps *evmtest.TestDeps) sdk.Tx {
return legacytx.StdTx{
Msgs: []sdk.Msg{
&tf.MsgMint{},
},
}
},
wantErr: "invalid message",
},
{
name: "happy: eth tx emitted event",
txSetup: func(deps *evmtest.TestDeps) sdk.Tx {
tx := happyCreateContractTx(deps)
return tx
},
wantErr: "",
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
deps := evmtest.NewTestDeps()
stateDB := deps.StateDB()
anteDec := app.NewEthEmitEventDecorator(deps.Chain.AppKeepers)

tx := tc.txSetup(&deps)
s.Require().NoError(stateDB.Commit())

_, err := anteDec.AnteHandle(
deps.Ctx, tx, false, NextNoOpAnteHandler,
)
if tc.wantErr != "" {
s.Require().ErrorContains(err, tc.wantErr)
return
}
s.Require().NoError(err)
events := deps.Ctx.EventManager().Events()

s.Require().Greater(len(events), 0)
event := events[len(events)-1]
s.Require().Equal(evm.EventTypeEthereumTx, event.Type)

// Convert tx to msg to get hash
txMsg, ok := tx.GetMsgs()[0].(*evm.MsgEthereumTx)
s.Require().True(ok)

// TX hash attr must present
attr, ok := event.GetAttribute(evm.AttributeKeyEthereumTxHash)
s.Require().True(ok, "tx hash attribute not found")
s.Require().Equal(txMsg.Hash, attr.Value)

// TX index attr must present
attr, ok = event.GetAttribute(evm.AttributeKeyTxIndex)
s.Require().True(ok, "tx index attribute not found")
s.Require().Equal("0", attr.Value)
})
}
}
Loading

0 comments on commit 6511f86

Please sign in to comment.