* Add adr-002 evm hooks proposal * add bank send hook example * fix typo * add more details to the adr * polishes * add comments and missing sections * explain the error translation * update working, add consequences * add reference to staking module hooks * add ApplyTransaction snippet
7.3 KiB
ADR 002: EVM Hooks
Changelog
- 2021-08-11: first draft
Status
PROPOSED
Abstract
The current ADR proposes a hook interface to the EVM module, to extend the tx processing logic externally, specifically to support EVM contract calling native modules through logs.
Context
Currently, there are no way for EVM smart contracts to call cosmos native modules, one way to do this is by emitting specific logs from the contract, and recognize those logs in tx processing code and convert them to native module calls.
To do this in an extensible way, we can add a post tx processing hook into the EVM module, which allows third-party to add custom logic to process transaction logs.
Decision
This ADR proposes to add an EvmHooks
interface and a method to register hooks in the EvmKeeper
:
type EvmHooks interface {
PostTxProcessing(ctx sdk.Context, txHash ethcmn.Hash, logs []*ethtypes.Log) error
}
func (k *EvmKeeper) SetHooks(eh types.EvmHooks) *Keeper;
-
PostTxProcessing
is only called after the EVM transaction finished successfully, it's executed in the same cache context as the EVM transaction, if it returns an error, the whole EVM transaction is reverted, if the hook implementor doesn't want to revert the tx, he can always return nil instead.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.
The EVM state transition method ApplyTransaction
should be changed like this:
// Create cached context which convers both the tx processing and post processing
revision := k.Snapshot()
res, err := k.ApplyMessage(evm, msg, ethCfg, false)
if err != nil {
return err
}
...
if !res.Failed() {
// Only call hooks if tx executed successfully.
err = k.hooks.PostTxProcessing(k.ctx, txHash, logs)
if err != nil {
// If hooks return error, revert the whole tx.
k.RevertToSnapshot(revision)
res.VmError = "failed to process native logs"
res.Ret = []byte(err.Error())
}
}
There are no default hooks implemented in the EVM module, so the proposal is backward compatible, only opens extra extensibility for certain use cases.
Use Case: Call Native Module
To support contract calling native module with this proposal, one can define a log signature and emits the specific log
from the smart contract, native logic registers a PostTxProcessing
hook which recognizes the log and does the native module
call.
For example, to support smart contract to transfer native tokens, one can define and emit a __CosmosNativeBankSend
log
signature in the smart contract like this:
event __CosmosNativeBankSend(address recipient, uint256 amount, string denom);
function withdraw_to_native_token(amount uint256) public {
_balances[msg.sender] -= amount;
// send native tokens from contract address to msg.sender.
emit __CosmosNativeBankSend(msg.sender, amount, "native_denom");
}
And the application registers a BankSendHook
to EvmKeeper
, it recognizes the log and converts it to a call to the bank
module's SendCoinsFromAccountToAccount
method:
var (
// BankSendEvent represent the signature of
// `event __CosmosNativeBankSend(address recipient, uint256 amount, string denom)`
BankSendEvent abi.Event
)
func init() {
addressType, _ := abi.NewType("address", "", nil)
uint256Type, _ := abi.NewType("uint256", "", nil)
stringType, _ := abi.NewType("string", "", nil)
BankSendEvent = abi.NewEvent(
"__CosmosNativeBankSend",
"__CosmosNativeBankSend",
false,
abi.Arguments{abi.Argument{
Name: "recipient",
Type: addressType,
Indexed: false,
}, abi.Argument{
Name: "amount",
Type: uint256Type,
Indexed: false,
}, abi.Argument{
Name: "denom",
Type: stringType,
Indexed: false,
}},
)
}
type BankSendHook struct {
bankKeeper bankkeeper.Keeper
}
func NewBankSendHook(bankKeeper bankkeeper.Keeper) *BankSendHook {
return &BankSendHook{
bankKeeper: bankKeeper,
}
}
func (h BankSendHook) PostTxProcessing(ctx sdk.Context, txHash ethcmn.Hash, logs []*ethtypes.Log) error {
for _, log := range logs {
if len(log.Topics) == 0 || log.Topics[0] != BankSendEvent.ID {
continue
}
if !ContractAllowed(log.Address) {
// Check the contract whitelist to prevent accidental native call.
continue
}
unpacked, err := BankSendEvent.Inputs.Unpack(log.Data)
if err != nil {
log.Warn("log signature matches but failed to decode")
continue
}
contract := sdk.AccAddress(log.Address.Bytes())
recipient := sdk.AccAddress(unpacked[0].(ethcmn.Address).Bytes())
coins := sdk.NewCoins(sdk.NewCoin(unpacked[2].(string), sdk.NewIntFromBigInt(unpacked[1].(*big.Int))))
err = h.bankKeeper.SendCoins(ctx, contract, recipient, coins)
if err != nil {
return err
}
}
}
return nil
}
Register the hook in app.go
:
evmKeeper.SetHooks(NewBankSendHook(bankKeeper));
Consequences
Backwards Compatibility
The proposed ADR is backward compatible.
Positive
- Improve extensibility of EVM module
Negative
- It's possible that some contracts accidentally define a log with the same signature and cause an unintentional result. To mitigate this, the implementor could whitelist contracts that are allowed to invoke native calls.
Neutral
- The contract can only call native modules asynchronously, which means it can neither get the result nor handle the error.