diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1d83b097..a4ad1404 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -34,7 +34,5 @@ Please add a entry below in your Pull Request for an ADR. ## ADR Table of Contents - +- [ADR 001: State](adr-001-state.md) +- [ADR 002: EVM Hooks](adr-002-evm-hooks.md) diff --git a/docs/architecture/adr-002-evm-hooks.md b/docs/architecture/adr-002-evm-hooks.md new file mode 100644 index 00000000..8b2ba53d --- /dev/null +++ b/docs/architecture/adr-002-evm-hooks.md @@ -0,0 +1,209 @@ +# 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`: + +```golang +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: + +```golang +// 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: + +```solidity +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: + +```golang +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`: + +```golang +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. + +## Further Discussions + + + +## Test Cases [optional] + + + +## References + + + +- [Hooks in staking module](https://docs.cosmos.network/v0.43/modules/staking/06_hooks.html)