docs: update EVM spec
This commit is contained in:
parent
0543a28941
commit
a121224dbc
@ -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`:
|
||||
|
Loading…
Reference in New Issue
Block a user