diff --git a/x/evm/spec/06_hooks.md b/x/evm/spec/06_hooks.md index 7d0b52f6..2f5f95c9 100644 --- a/x/evm/spec/06_hooks.md +++ b/x/evm/spec/06_hooks.md @@ -4,7 +4,9 @@ order: 6 # Hooks -The evm module implements an `EvmHooks` interface that extend the `Tx` processing logic externally. This supports EVM contracts to call native cosmos modules by +The `x/evm` module implements an `EvmHooks` interface that extend and customize the `Tx` processing logic externally. + +This supports EVM contracts to call native cosmos modules by 1. defining a log signature and emitting the specific log from the smart contract, 2. recognizing those logs in the native tx processing code, and @@ -14,7 +16,8 @@ To do this, the interface includes a `PostTxProcessing` hook that registers cus ```go type EvmHooks interface { - PostTxProcessing(ctx sdk.Context, txHash common.Hash, logs []*ethtypes.Log) error + // Must be called after tx is processed successfully, if return an error, the whole transaction is reverted. + PostTxProcessing(ctx sdk.Context, msg core.Message, receipt *ethtypes.Receipt) error } ``` @@ -23,11 +26,11 @@ type EvmHooks interface { `PostTxProcessing` is only called after a EVM transaction finished successfully and delegates the call to underlying hooks. If no hook has been registered, this function returns with a `nil` error. ```go -func (k *Keeper) PostTxProcessing(txHash common.Hash, logs []*ethtypes.Log) error { - if k.hooks == nil { - return nil - } - return k.hooks.PostTxProcessing(k.Ctx(), txHash, logs) +func (k *Keeper) PostTxProcessing(ctx sdk.Context, msg core.Message, receipt *ethtypes.Receipt) error { + if k.hooks == nil { + return nil + } + return k.hooks.PostTxProcessing(k.Ctx(), msg, receipt) } ``` @@ -35,9 +38,9 @@ It's executed in the same cache context as the EVM transaction, if it returns an The error returned by the hooks is translated to a VM error `failed to process native logs`, the detailed error message is stored in the return value. The message is sent to native modules asynchronously, there's no way for the caller to catch and recover the error. -## Use Case: Call Native erc20 Module on Evmos +## Use Case: Call Native ERC20 Module on Evmos -Here is an example taken from the [Evmos erc20 module](https://evmos.dev/modules/erc20/) that shows how the `EVMHooks` supports a contract calling a native module to convert ERC-20 Tokens intor Cosmos native Coins. Following the steps from above. +Here is an example taken from the Evmos [erc20 module](https://evmos.dev/modules/erc20/) that shows how the `EVMHooks` supports a contract calling a native module to convert ERC-20 Tokens into Cosmos native Coins. Following the steps from above. You can define and emit a `Transfer` log signature in the smart contract like this: @@ -45,14 +48,14 @@ You can define and emit a `Transfer` log signature in the smart contract like th event Transfer(address indexed from, address indexed to, uint256 value); function _transfer(address sender, address recipient, uint256 amount) internal virtual { - require(sender != address(0), "ERC20: transfer from the zero address"); - require(recipient != address(0), "ERC20: transfer to the zero address"); + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); - _beforeTokenTransfer(sender, recipient, amount); + _beforeTokenTransfer(sender, recipient, amount); - _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); - _balances[recipient] = _balances[recipient].add(amount); - emit Transfer(sender, recipient, amount); + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); } ``` @@ -63,115 +66,125 @@ The application will register a `BankSendHook` to the `EvmKeeper`. It recognizes const ERC20EventTransfer = "Transfer" // PostTxProcessing implements EvmHooks.PostTxProcessing -func (k Keeper) PostTxProcessing(ctx sdk.Context, txHash common.Hash, logs []*ethtypes.Log) error { - params := k.GetParams(ctx) - if !params.EnableEVMHook { - return sdkerrors.Wrap(types.ErrInternalTokenPair, "EVM Hook is currently disabled") - } +func (k Keeper) PostTxProcessing( + ctx sdk.Context, + msg core.Message, + receipt *ethtypes.Receipt, +) error { + params := h.k.GetParams(ctx) + if !params.EnableErc20 || !params.EnableEVMHook { + // no error is returned to allow for other post processing txs + // to pass + return nil + } - erc20 := contracts.ERC20BurnableContract.ABI + erc20 := contracts.ERC20BurnableContract.ABI - for i, log := range logs { - if len(log.Topics) < 3 { - continue - } + for i, log := range receipt.Logs { + if len(log.Topics) < 3 { + continue + } - eventID := log.Topics[0] // event ID + eventID := log.Topics[0] // event ID - event, err := erc20.EventByID(eventID) - if err != nil { - // invalid event for ERC20 - continue - } + event, err := erc20.EventByID(eventID) + if err != nil { + // invalid event for ERC20 + continue + } - if event.Name != types.ERC20EventTransfer { - k.Logger(ctx).Info("emitted event", "name", event.Name, "signature", event.Sig) - continue - } + if event.Name != types.ERC20EventTransfer { + h.k.Logger(ctx).Info("emitted event", "name", event.Name, "signature", event.Sig) + continue + } - transferEvent, err := erc20.Unpack(event.Name, log.Data) - if err != nil { - k.Logger(ctx).Error("failed to unpack transfer event", "error", err.Error()) - continue - } + transferEvent, err := erc20.Unpack(event.Name, log.Data) + if err != nil { + h.k.Logger(ctx).Error("failed to unpack transfer event", "error", err.Error()) + continue + } - if len(transferEvent) == 0 { - continue - } + if len(transferEvent) == 0 { + continue + } - tokens, ok := transferEvent[0].(*big.Int) - // safety check and ignore if amount not positive - if !ok || tokens == nil || tokens.Sign() != 1 { - continue - } + tokens, ok := transferEvent[0].(*big.Int) + // safety check and ignore if amount not positive + if !ok || tokens == nil || tokens.Sign() != 1 { + continue + } - // check that the contract is a registered token pair - contractAddr := log.Address + // check that the contract is a registered token pair + contractAddr := log.Address - id := k.GetERC20Map(ctx, contractAddr) + id := h.k.GetERC20Map(ctx, contractAddr) - if len(id) == 0 { - // no token is registered for the caller contract - continue - } + if len(id) == 0 { + // no token is registered for the caller contract + continue + } - pair, found := k.GetTokenPair(ctx, id) - if !found { - continue - } + pair, found := h.k.GetTokenPair(ctx, id) + if !found { + continue + } - // check that relaying for the pair is enabled - if !pair.Enabled { - return fmt.Errorf("internal relaying is disabled for pair %s, please create a governance proposal", contractAddr) // convert to SDK error - } + // check that conversion for the pair is enabled + if !pair.Enabled { + // continue to allow transfers for the ERC20 in case the token pair is disabled + h.k.Logger(ctx).Debug( + "ERC20 token -> Cosmos coin conversion is disabled for pair", + "coin", pair.Denom, "contract", pair.Erc20Address, + ) + continue + } - // ignore as the burning always transfers to the zero address - to := common.BytesToAddress(log.Topics[2].Bytes()) - if !bytes.Equal(to.Bytes(), types.ModuleAddress.Bytes()) { - continue - } + // ignore as the burning always transfers to the zero address + to := common.BytesToAddress(log.Topics[2].Bytes()) + if !bytes.Equal(to.Bytes(), types.ModuleAddress.Bytes()) { + continue + } - // check that the event is Burn from the ERC20Burnable interface - // NOTE: assume that if they are burning the token that has been registered as a pair, they want to mint a Cosmos coin + // check that the event is Burn from the ERC20Burnable interface + // NOTE: assume that if they are burning the token that has been registered as a pair, they want to mint a Cosmos coin - // create the corresponding sdk.Coin that is paired with ERC20 - coins := sdk.Coins{{Denom: pair.Denom, Amount: sdk.NewIntFromBigInt(tokens)}} + // create the corresponding sdk.Coin that is paired with ERC20 + coins := sdk.Coins{{Denom: pair.Denom, Amount: sdk.NewIntFromBigInt(tokens)}} - // Mint the coin only if ERC20 is external - switch pair.ContractOwner { - case types.OWNER_MODULE: - _, err = k.CallEVM(ctx, erc20, types.ModuleAddress, contractAddr, "burn", tokens) - case types.OWNER_EXTERNAL: - err = k.bankKeeper.MintCoins(ctx, types.ModuleName, coins) - default: - err = types.ErrUndefinedOwner - } + // Mint the coin only if ERC20 is external + switch pair.ContractOwner { + case types.OWNER_MODULE: + _, err = h.k.CallEVM(ctx, erc20, types.ModuleAddress, contractAddr, true, "burn", tokens) + case types.OWNER_EXTERNAL: + err = h.k.bankKeeper.MintCoins(ctx, types.ModuleName, coins) + default: + err = types.ErrUndefinedOwner + } - if err != nil { - k.Logger(ctx).Debug( - "failed to process EVM hook for ER20 -> coin conversion", - "coin", pair.Denom, "contract", pair.Erc20Address, "error", err.Error(), - ) - continue - } + if err != nil { + h.k.Logger(ctx).Debug( + "failed to process EVM hook for ER20 -> coin conversion", + "coin", pair.Denom, "contract", pair.Erc20Address, "error", err.Error(), + ) + continue + } - // Only need last 20 bytes from log.topics - from := common.BytesToAddress(log.Topics[1].Bytes()) - recipient := sdk.AccAddress(from.Bytes()) + // Only need last 20 bytes from log.topics + from := common.BytesToAddress(log.Topics[1].Bytes()) + recipient := sdk.AccAddress(from.Bytes()) - // transfer the tokens from ModuleAccount to sender address - if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, recipient, coins); err != nil { - k.Logger(ctx).Debug( - "failed to process EVM hook for ER20 -> coin conversion", - "tx-hash", txHash.Hex(), "log-idx", i, - "coin", pair.Denom, "contract", pair.Erc20Address, "error", err.Error(), - ) - continue - } - } + // transfer the tokens from ModuleAccount to sender address + if err := h.k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, recipient, coins); err != nil { + h.k.Logger(ctx).Debug( + "failed to process EVM hook for ER20 -> coin conversion", + "tx-hash", receipt.TxHash.Hex(), "log-idx", i, + "coin", pair.Denom, "contract", pair.Erc20Address, "error", err.Error(), + ) + continue + } + } - return nil -} + return nil ``` Lastly, register the hook in `app.go`: