diff --git a/x/vm/keeper/gas.go b/x/vm/keeper/gas.go index a83695144..da2c72f36 100644 --- a/x/vm/keeper/gas.go +++ b/x/vm/keeper/gas.go @@ -25,7 +25,7 @@ func (k *Keeper) GetEthIntrinsicGas(ctx sdk.Context, msg core.Message, cfg *para return core.IntrinsicGas(msg.Data(), msg.AccessList(), isContractCreation, homestead, istanbul) } -// RefundGas transfers the leftover gas to the sender of the message, caped to half of the total gas +// RefundGas transfers the leftover gas to the sender of the message, capped to half of the total gas // consumed in the transaction. Additionally, the function sets the total gas consumed to the value // returned by the EVM execution, thus ignoring the previous intrinsic gas consumed during in the // AnteHandler. diff --git a/x/vm/keeper/hooks.go b/x/vm/keeper/hooks.go new file mode 100644 index 000000000..12be89074 --- /dev/null +++ b/x/vm/keeper/hooks.go @@ -0,0 +1,36 @@ +package keeper + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/cosmos/evm/x/vm/types" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Event Hooks +// These can be utilized to customize evm transaction processing. + +var _ types.EvmHooks = MultiEvmHooks{} + +// MultiEvmHooks combine multiple evm hooks, all hook functions are run in array sequence +type MultiEvmHooks []types.EvmHooks + +// NewMultiEvmHooks combine multiple evm hooks +func NewMultiEvmHooks(hooks ...types.EvmHooks) MultiEvmHooks { + return hooks +} + +// PostTxProcessing delegate the call to underlying hooks +func (mh MultiEvmHooks) PostTxProcessing(ctx sdk.Context, sender common.Address, msg core.Message, receipt *ethtypes.Receipt) error { + for i := range mh { + if err := mh[i].PostTxProcessing(ctx, sender, msg, receipt); err != nil { + return errorsmod.Wrapf(err, "EVM hook %T failed", mh[i]) + } + } + return nil +} diff --git a/x/vm/keeper/hooks_test.go b/x/vm/keeper/hooks_test.go new file mode 100644 index 000000000..e82b6fb3a --- /dev/null +++ b/x/vm/keeper/hooks_test.go @@ -0,0 +1,90 @@ +package keeper_test + +import ( + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/cosmos/evm/x/vm/keeper" + "github.com/cosmos/evm/x/vm/statedb" + "github.com/cosmos/evm/x/vm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// LogRecordHook records all the logs +type LogRecordHook struct { + Logs []*ethtypes.Log +} + +func (dh *LogRecordHook) PostTxProcessing(ctx sdk.Context, sender common.Address, msg core.Message, receipt *ethtypes.Receipt) error { + dh.Logs = receipt.Logs + return nil +} + +// FailureHook always fail +type FailureHook struct{} + +func (dh FailureHook) PostTxProcessing(ctx sdk.Context, sender common.Address, msg core.Message, receipt *ethtypes.Receipt) error { + return errors.New("post tx processing failed") +} + +func (suite *KeeperTestSuite) TestEvmHooks() { + testCases := []struct { + msg string + setupHook func() types.EvmHooks + expFunc func(hook types.EvmHooks, result error) + }{ + { + "log collect hook", + func() types.EvmHooks { + return &LogRecordHook{} + }, + func(hook types.EvmHooks, result error) { + suite.Require().NoError(result) + suite.Require().Equal(1, len((hook.(*LogRecordHook).Logs))) + }, + }, + { + "always fail hook", + func() types.EvmHooks { + return &FailureHook{} + }, + func(hook types.EvmHooks, result error) { + suite.Require().Error(result) + }, + }, + } + + for _, tc := range testCases { + suite.SetupTest() + hook := tc.setupHook() + suite.network.App.EVMKeeper.SetHooks(keeper.NewMultiEvmHooks(hook)) + + k := suite.network.App.EVMKeeper + ctx := suite.network.GetContext() + txHash := common.BigToHash(big.NewInt(1)) + vmdb := statedb.New(ctx, k, statedb.NewTxConfig( + common.BytesToHash(ctx.HeaderHash()), + txHash, + 0, + 0, + )) + + vmdb.AddLog(ðtypes.Log{ + Topics: []common.Hash{}, + Address: suite.keyring.GetAddr(0), + }) + logs := vmdb.Logs() + receipt := ðtypes.Receipt{ + TxHash: txHash, + Logs: logs, + } + result := k.PostTxProcessing(ctx, suite.keyring.GetAddr(0), ethtypes.Message{}, receipt) + + tc.expFunc(hook, result) + } +} diff --git a/x/vm/keeper/keeper.go b/x/vm/keeper/keeper.go index 3ec3ef1e9..bfb81aad1 100644 --- a/x/vm/keeper/keeper.go +++ b/x/vm/keeper/keeper.go @@ -57,6 +57,9 @@ type Keeper struct { // Tracer used to collect execution traces from the EVM transaction execution tracer string + hooks types.EvmHooks + // EVM Hooks for tx post-processing + // precompiles defines the map of all available precompiled smart contracts. // Some of these precompiled contracts might not be active depending on the EVM // parameters. @@ -164,6 +167,30 @@ func (k Keeper) GetTxIndexTransient(ctx sdk.Context) uint64 { return sdk.BigEndianToUint64(store.Get(types.KeyPrefixTransientTxIndex)) } +// ---------------------------------------------------------------------------- +// Hooks +// ---------------------------------------------------------------------------- + +// SetHooks sets the hooks for the EVM module +// Called only once during initialization, panics if called more than once. +func (k *Keeper) SetHooks(eh types.EvmHooks) *Keeper { + if k.hooks != nil { + panic("cannot set evm hooks twice") + } + + k.hooks = eh + return k +} + +// PostTxProcessing delegates the call to the hooks. +// If no hook has been registered, this function returns with a `nil` error +func (k *Keeper) PostTxProcessing(ctx sdk.Context, sender common.Address, msg core.Message, receipt *ethtypes.Receipt) error { + if k.hooks == nil { + return nil + } + return k.hooks.PostTxProcessing(ctx, sender, msg, receipt) +} + // ---------------------------------------------------------------------------- // Log // ---------------------------------------------------------------------------- diff --git a/x/vm/keeper/state_transition.go b/x/vm/keeper/state_transition.go index f447c82c7..665429a62 100644 --- a/x/vm/keeper/state_transition.go +++ b/x/vm/keeper/state_transition.go @@ -7,6 +7,7 @@ import ( evmcore "github.com/ethereum/go-ethereum/core" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" cmttypes "github.com/cometbft/cometbft/types" @@ -144,7 +145,10 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { // // For relevant discussion see: https://github.com/cosmos/cosmos-sdk/discussions/9072 func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*types.MsgEthereumTxResponse, error) { - var bloom *big.Int + var ( + bloom *big.Int + bloomReceipt ethtypes.Bloom + ) cfg, err := k.EVMConfig(ctx, sdk.ConsAddress(ctx.BlockHeader().ProposerAddress)) if err != nil { @@ -179,10 +183,58 @@ func (k *Keeper) ApplyTransaction(ctx sdk.Context, tx *ethtypes.Transaction) (*t if len(logs) > 0 { bloom = k.GetBlockBloomTransient(ctx) bloom.Or(bloom, big.NewInt(0).SetBytes(ethtypes.LogsBloom(logs))) + bloomReceipt = ethtypes.BytesToBloom(bloom.Bytes()) } if !res.Failed() { - commit() + var contractAddr common.Address + if msg.To() == nil { + contractAddr = crypto.CreateAddress(msg.From(), msg.Nonce()) + } + + cumulativeGasUsed := res.GasUsed + if ctx.BlockGasMeter() != nil { + limit := ctx.BlockGasMeter().Limit() + cumulativeGasUsed += ctx.BlockGasMeter().GasConsumed() + if cumulativeGasUsed > limit { + cumulativeGasUsed = limit + } + } + + receipt := ðtypes.Receipt{ + Type: tx.Type(), + PostState: nil, + CumulativeGasUsed: cumulativeGasUsed, + Bloom: bloomReceipt, + Logs: logs, + TxHash: txConfig.TxHash, + ContractAddress: contractAddr, + GasUsed: res.GasUsed, + BlockHash: txConfig.BlockHash, + BlockNumber: big.NewInt(ctx.BlockHeight()), + TransactionIndex: txConfig.TxIndex, + } + + signerAddr, err := signer.Sender(tx) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to extract sender address from ethereum transaction") + } + + // Note: PostTxProcessing hooks currently do not charge for gas + // and function similar to EndBlockers in abci, but for EVM transactions + if err = k.PostTxProcessing(tmpCtx, signerAddr, msg, receipt); err != nil { + // If hooks returns an error, revert the whole tx. + res.VmError = errorsmod.Wrap(err, "failed to execute post transaction processing").Error() + k.Logger(ctx).Error("tx post processing failed", "error", err) + // If the tx failed in post processing hooks, we should clear the logs + res.Logs = nil + } else if commit != nil { + commit() + + // Since the post-processing can alter the log, we need to update the result + res.Logs = types.NewLogsFromEth(receipt.Logs) + ctx.EventManager().EmitEvents(tmpCtx.EventManager().Events()) + } } evmDenom := types.GetEVMCoinDenom() diff --git a/x/vm/keeper/state_transition_test.go b/x/vm/keeper/state_transition_test.go index 416f08f02..e63fd1d46 100644 --- a/x/vm/keeper/state_transition_test.go +++ b/x/vm/keeper/state_transition_test.go @@ -556,6 +556,56 @@ func (suite *KeeperTestSuite) TestEVMConfig() { suite.Require().Equal(proposerHextAddress, cfg.CoinBase) } +func (suite *KeeperTestSuite) TestApplyTransaction() { + suite.enableFeemarket = true + defer func() { suite.enableFeemarket = false }() + // FeeCollector account is pre-funded with enough tokens + // for refund to work + // NOTE: everything should happen within the same block for + // feecollector account to remain funded + suite.SetupTest() + // set bounded cosmos block gas limit + ctx := suite.network.GetContext().WithBlockGasMeter(storetypes.NewGasMeter(1e6)) + err := suite.network.App.BankKeeper.MintCoins(ctx, "mint", sdk.NewCoins(sdk.NewCoin("aatom", sdkmath.NewInt(3e18)))) + suite.Require().NoError(err) + err = suite.network.App.BankKeeper.SendCoinsFromModuleToModule(ctx, "mint", "fee_collector", sdk.NewCoins(sdk.NewCoin("aatom", sdkmath.NewInt(3e18)))) + suite.Require().NoError(err) + testCases := []struct { + name string + gasLimit uint64 + requireErr bool + errorMsg string + }{ + { + "pass - set evm limit above cosmos block gas limit and refund", + 6e6, + false, + "", + }, + } + + for _, tc := range testCases { + suite.Run(fmt.Sprintf("Case %s", tc.name), func() { + tx, err := suite.factory.GenerateSignedEthTx(suite.keyring.GetPrivKey(0), types.EvmTxArgs{ + GasLimit: tc.gasLimit, + }) + suite.Require().NoError(err) + initialBalance := suite.network.App.BankKeeper.GetBalance(ctx, suite.keyring.GetAccAddr(0), "aatom") + + ethTx := tx.GetMsgs()[0].(*types.MsgEthereumTx).AsTransaction() + res, err := suite.network.App.EVMKeeper.ApplyTransaction(ctx, ethTx) + suite.Require().NoError(err) + suite.Require().Equal(res.GasUsed, uint64(3e6)) + // Half of the gas should be refunded based on the protocol refund cap. + // Note that the balance should only increment by the refunded amount + // because ApplyTransaction does not consume and take the gas from the user. + balanceAfterRefund := suite.network.App.BankKeeper.GetBalance(ctx, suite.keyring.GetAccAddr(0), "aatom") + expectedRefund := new(big.Int).Mul(new(big.Int).SetUint64(6e6/2), suite.network.App.EVMKeeper.GetBaseFee(ctx)) + suite.Require().Equal(balanceAfterRefund.Sub(initialBalance).Amount, sdkmath.NewIntFromBigInt(expectedRefund)) + }) + } +} + func (suite *KeeperTestSuite) TestApplyMessage() { suite.enableFeemarket = true defer func() { suite.enableFeemarket = false }() diff --git a/x/vm/types/interfaces.go b/x/vm/types/interfaces.go index 3202585b3..0ce185537 100644 --- a/x/vm/types/interfaces.go +++ b/x/vm/types/interfaces.go @@ -5,6 +5,8 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" feemarkettypes "github.com/cosmos/evm/x/feemarket/types" @@ -57,6 +59,12 @@ type Erc20Keeper interface { GetERC20PrecompileInstance(ctx sdk.Context, address common.Address) (contract vm.PrecompiledContract, found bool, err error) } +// EvmHooks event hooks for evm tx processing +type EvmHooks interface { + // Must be called after tx is processed successfully, if return an error, the whole transaction is reverted. + PostTxProcessing(ctx sdk.Context, sender common.Address, msg core.Message, receipt *ethtypes.Receipt) error +} + // BankWrapper defines the methods required by the wrapper around // the Cosmos SDK x/bank keeper that is used to manage an EVM coin // with a configurable value for decimals.