diff --git a/app/app.go b/app/app.go index 8ff242e..ab582fe 100644 --- a/app/app.go +++ b/app/app.go @@ -104,6 +104,9 @@ import ( initiaapplanes "github.com/initia-labs/initia/app/lanes" initiaappparams "github.com/initia-labs/initia/app/params" + ibchooks "github.com/initia-labs/initia/x/ibc-hooks" + ibchookskeeper "github.com/initia-labs/initia/x/ibc-hooks/keeper" + ibchookstypes "github.com/initia-labs/initia/x/ibc-hooks/types" "github.com/initia-labs/initia/x/ibc/fetchprice" fetchpricekeeper "github.com/initia-labs/initia/x/ibc/fetchprice/keeper" fetchpricetypes "github.com/initia-labs/initia/x/ibc/fetchprice/types" @@ -134,7 +137,7 @@ import ( // local imports appante "github.com/initia-labs/minievm/app/ante" apphook "github.com/initia-labs/minievm/app/hook" - evmibcmiddleware "github.com/initia-labs/minievm/app/ibc-middleware" + ibcevmhooks "github.com/initia-labs/minievm/app/ibc-hooks" appkeepers "github.com/initia-labs/minievm/app/keepers" applanes "github.com/initia-labs/minievm/app/lanes" @@ -223,6 +226,7 @@ type MinitiaApp struct { ICQKeeper *icqkeeper.Keeper OracleKeeper *oraclekeeper.Keeper // x/oracle keeper used for the slinky oracle FetchPriceKeeper *fetchpricekeeper.Keeper + IBCHooksKeeper *ibchookskeeper.Keeper // make scoped keepers public for test purposes ScopedIBCKeeper capabilitykeeper.ScopedKeeper @@ -276,7 +280,7 @@ func NewMinitiaApp( icahosttypes.StoreKey, icacontrollertypes.StoreKey, icaauthtypes.StoreKey, ibcfeetypes.StoreKey, evmtypes.StoreKey, opchildtypes.StoreKey, auctiontypes.StoreKey, packetforwardtypes.StoreKey, icqtypes.StoreKey, - oracletypes.StoreKey, fetchpricetypes.StoreKey, + oracletypes.StoreKey, fetchpricetypes.StoreKey, ibchookstypes.StoreKey, ) tkeys := storetypes.NewTransientStoreKeys() memKeys := storetypes.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) @@ -437,6 +441,13 @@ func NewMinitiaApp( ) app.IBCFeeKeeper = &ibcFeeKeeper + app.IBCHooksKeeper = ibchookskeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[ibchookstypes.StoreKey]), + authorityAddr, + ac, + ) + //////////////////////////// // Transfer configuration // //////////////////////////// @@ -446,7 +457,6 @@ func NewMinitiaApp( var transferStack porttypes.IBCModule { packetForwardKeeper := &packetforwardkeeper.Keeper{} - evmMiddleware := &evmibcmiddleware.IBCMiddleware{} // Create Transfer Keepers transferKeeper := ibctransferkeeper.NewKeeper( @@ -473,8 +483,8 @@ func NewMinitiaApp( app.IBCKeeper.ChannelKeeper, communityPoolKeeper, app.BankKeeper, - // ics4wrapper: transfer -> packet forward -> evm - evmMiddleware, + // ics4wrapper: transfer -> packet forward -> fee + app.IBCFeeKeeper, authorityAddr, ) app.PacketForwardKeeper = packetForwardKeeper @@ -487,19 +497,21 @@ func NewMinitiaApp( packetforwardkeeper.DefaultRefundTransferPacketTimeoutTimestamp, ) - // create move middleware for transfer - *evmMiddleware = evmibcmiddleware.NewIBCMiddleware( - // receive: evm -> packet forward -> transfer + // create wasm middleware for transfer + hookMiddleware := ibchooks.NewIBCMiddleware( + // receive: wasm -> packet forward -> transfer packetForwardMiddleware, - // ics4wrapper: transfer -> packet forward -> evm -> fee - app.IBCFeeKeeper, - app.EVMKeeper, + ibchooks.NewICS4Middleware( + nil, /* ics4wrapper: not used */ + ibcevmhooks.NewEVMHooks(app.EVMKeeper, ac), + ), + app.IBCHooksKeeper, ) // create ibcfee middleware for transfer transferStack = ibcfee.NewIBCMiddleware( // receive: fee -> evm -> packet forward -> transfer - evmMiddleware, + hookMiddleware, // ics4wrapper: transfer -> packet forward -> evm -> fee -> channel *app.IBCFeeKeeper, ) diff --git a/app/ibc-hooks/common_test.go b/app/ibc-hooks/common_test.go index c5b970a..44f1e42 100644 --- a/app/ibc-hooks/common_test.go +++ b/app/ibc-hooks/common_test.go @@ -299,8 +299,7 @@ func _createTestInput( // ibc middleware setup mockIBCMiddleware := mockIBCMiddleware{} - evmHooks, err := evmhooks.NewEVMHooks(evmKeeper, ac) - require.NoError(t, err) + evmHooks := evmhooks.NewEVMHooks(evmKeeper, ac) middleware := ibchooks.NewICS4Middleware(mockIBCMiddleware, evmHooks) ibcHookMiddleware := ibchooks.NewIBCMiddleware(mockIBCMiddleware, middleware, ibcHooksKeeper) diff --git a/app/ibc-hooks/hooks.go b/app/ibc-hooks/hooks.go index 1219312..3a5eb92 100644 --- a/app/ibc-hooks/hooks.go +++ b/app/ibc-hooks/hooks.go @@ -25,17 +25,17 @@ type EVMHooks struct { asyncCallbackABI *abi.ABI } -func NewEVMHooks(evmKeeper *evmkeeper.Keeper, ac address.Codec) (*EVMHooks, error) { +func NewEVMHooks(evmKeeper *evmkeeper.Keeper, ac address.Codec) *EVMHooks { abi, err := i_ibc_async_callback.IIbcAsyncCallbackMetaData.GetAbi() if err != nil { - return nil, err + panic(err) } return &EVMHooks{ evmKeeper: evmKeeper, ac: ac, asyncCallbackABI: abi, - }, nil + } } func (h EVMHooks) OnRecvPacketOverride(im ibchooks.IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { diff --git a/app/ibc-middleware/README.md b/app/ibc-middleware/README.md deleted file mode 100644 index 71e3e94..0000000 --- a/app/ibc-middleware/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# IBC-hooks - -This module is copied from [osmosis](https://github.com/osmosis-labs/osmosis) and changed to execute evm contract with ICS-20 token transfer calls. - -## EVM Hooks - -The evm hook is an IBC middleware which is used to allow ICS-20 token transfers to initiate contract calls. -This allows cross-chain contract calls, that involve token evmment. -This is useful for a variety of usecases. -One of primary importance is cross-chain swaps, which is an extremely powerful primitive. - -The mechanism enabling this is a `memo` field on every ICS20 transfer packet as of [IBC v3.4.0](https://medium.com/the-interchain-foundation/moving-beyond-simple-token-transfers-d42b2b1dc29b). -EVM hooks is an IBC middleware that parses an ICS20 transfer, and if the `memo` field is of a particular form, executes a evm contract call. We now detail the `memo` format for `evm` contract calls, and the execution guarantees provided. - -### EVM Contract Execution Format - -Before we dive into the IBC metadata format, we show the evm execute message format, so the reader has a sense of what are the fields we need to be setting in. - -```go -// MsgCall is a message to call an Ethereum contract. -type MsgCall struct { - // Sender is the that actor that signed the messages - Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` - // ContractAddr is the contract address to be executed. - // It can be cosmos address or hex encoded address. - ContractAddr string `protobuf:"bytes,2,opt,name=contract_addr,json=contractAddr,proto3" json:"contract_addr,omitempty"` - // Execution input bytes. - Input []byte `protobuf:"bytes,3,opt,name=input,proto3" json:"input,omitempty"` -} -``` - -So we detail where we want to get each of these fields from: - -- Sender: We cannot trust the sender of an IBC packet, the counter-party chain has full ability to lie about it. - We cannot risk this sender being confused for a particular user or module address on Initia. - So we replace the sender with an account to represent the sender prefixed by the channel and a evm module prefix. - This is done by setting the sender to `Bech32(Hash(Hash("ibc-hook-intermediary") + channelID/sender))`, where the channelId is the channel id on the local chain. -- ModuleAddress: This field should be directly obtained from the ICS-20 packet metadata -- ModuleName: This field should be directly obtained from the ICS-20 packet metadata -- FunctionName: This field should be directly obtained from the ICS-20 packet metadata -- TypeArgs: This field should be directly obtained from the ICS-20 packet metadata -- Args: This field should be directly obtained from the ICS-20 packet metadata. - -So our constructed evm message that we execute will look like: - -```go -msg := MsgCall{ - // Sender is the that actor that signed the messages - Sender: "init1-hash-of-channel-and-sender", - // ContractAddr is the address of the contract - ContractAddr: packet.data.memo["evm"]["contract_addr"], - // Input is the input bytes - Input: packet.data.memo["evm"]["input"], -} -``` - -### ICS20 packet structure - -So given the details above, we propogate the implied ICS20 packet data structure. -ICS20 is JSON native, so we use JSON for the memo format. - -```json -{ - //... other ibc fields that we don't care about - "data": { - "denom": "denom on counterparty chain (e.g. uatom)", // will be transformed to the local denom (ibc/...) - "amount": "1000", - "sender": "addr on counterparty chain", // will be transformed - "receiver": "ModuleAddr::ModuleName::FunctionName", - "memo": { - "evm": { - "contract_addr": "0x1", - "input": "base64 encoded bytes", - } - } - } -} -``` - -An ICS20 packet is formatted correctly for evmhooks iff the following all hold: - -- `memo` is not blank -- `memo` is valid JSON -- `memo` has at least one key, with value `"evm"` -- `memo["evm"]` has exactly two entries, `"contract_addr"` and `"input"` -- `receiver` == "" || `receiver` == "contract_addr" - -We consider an ICS20 packet as directed towards evmhooks iff all of the following hold: - -- `memo` is not blank -- `memo` is valid JSON -- `memo` has at least one key, with name `"evm"` - -If an ICS20 packet is not directed towards evmhooks, evmhooks doesn't do anything. -If an ICS20 packet is directed towards evmhooks, and is formatted incorrectly, then evmhooks returns an error. - -### Execution flow - -Pre evm hooks: - -- Ensure the incoming IBC packet is cryptogaphically valid -- Ensure the incoming IBC packet is not timed out. - -In evm hooks, pre packet execution: - -- Ensure the packet is correctly formatted (as defined above) -- Edit the receiver to be the hardcoded IBC module account - -In evm hooks, post packet execution: - -- Construct evm message as defined before -- Execute evm message -- if evm message has error, return ErrAck -- otherwise continue through middleware - -# Testing strategy - -See go tests. diff --git a/app/ibc-middleware/ibc_middleware.go b/app/ibc-middleware/ibc_middleware.go deleted file mode 100644 index 99748ce..0000000 --- a/app/ibc-middleware/ibc_middleware.go +++ /dev/null @@ -1,202 +0,0 @@ -package ibc_middleware - -import ( - "encoding/json" - - sdk "github.com/cosmos/cosmos-sdk/types" - capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" - - clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" - channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" - porttypes "github.com/cosmos/ibc-go/v8/modules/core/05-port/types" - ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" - - evmkeeper "github.com/initia-labs/minievm/x/evm/keeper" - evmtypes "github.com/initia-labs/minievm/x/evm/types" -) - -var _ porttypes.Middleware = &IBCMiddleware{} - -type IBCMiddleware struct { - app porttypes.IBCModule - ics4Wrapper porttypes.ICS4Wrapper - evmKeeper *evmkeeper.Keeper -} - -func NewIBCMiddleware( - app porttypes.IBCModule, - ics4Wrapper porttypes.ICS4Wrapper, - evmKeeper *evmkeeper.Keeper, -) IBCMiddleware { - return IBCMiddleware{ - app: app, - ics4Wrapper: ics4Wrapper, - evmKeeper: evmKeeper, - } -} - -// OnChanOpenInit implements the IBCMiddleware interface -func (im IBCMiddleware) OnChanOpenInit( - ctx sdk.Context, - order channeltypes.Order, - connectionHops []string, - portID string, - channelID string, - channelCap *capabilitytypes.Capability, - counterparty channeltypes.Counterparty, - version string, -) (string, error) { - return im.app.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, version) -} - -// OnChanOpenTry implements the IBCMiddleware interface -func (im IBCMiddleware) OnChanOpenTry( - ctx sdk.Context, - order channeltypes.Order, - connectionHops []string, - portID, - channelID string, - channelCap *capabilitytypes.Capability, - counterparty channeltypes.Counterparty, - counterpartyVersion string, -) (string, error) { - return im.app.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, channelCap, counterparty, counterpartyVersion) -} - -// OnChanOpenAck implements the IBCMiddleware interface -func (im IBCMiddleware) OnChanOpenAck( - ctx sdk.Context, - portID, - channelID string, - counterpartyChannelID string, - counterpartyVersion string, -) error { - return im.app.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) -} - -// OnChanOpenConfirm implements the IBCMiddleware interface -func (im IBCMiddleware) OnChanOpenConfirm( - ctx sdk.Context, - portID, - channelID string, -) error { - return im.app.OnChanOpenConfirm(ctx, portID, channelID) -} - -// OnChanCloseInit implements the IBCMiddleware interface -func (im IBCMiddleware) OnChanCloseInit( - ctx sdk.Context, - portID, - channelID string, -) error { - return im.app.OnChanCloseInit(ctx, portID, channelID) -} - -// OnChanCloseConfirm implements the IBCMiddleware interface -func (im IBCMiddleware) OnChanCloseConfirm( - ctx sdk.Context, - portID, - channelID string, -) error { - return im.app.OnChanCloseConfirm(ctx, portID, channelID) -} - -// OnAcknowledgementPacket implements the IBCMiddleware interface -func (im IBCMiddleware) OnAcknowledgementPacket( - ctx sdk.Context, - packet channeltypes.Packet, - acknowledgement []byte, - relayer sdk.AccAddress, -) error { - return im.app.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) -} - -// OnTimeoutPacket implements the IBCMiddleware interface -func (im IBCMiddleware) OnTimeoutPacket( - ctx sdk.Context, - packet channeltypes.Packet, - relayer sdk.AccAddress, -) error { - return im.app.OnTimeoutPacket(ctx, packet, relayer) -} - -// SendPacket implements the ICS4 Wrapper interface -func (im IBCMiddleware) SendPacket( - ctx sdk.Context, - chanCap *capabilitytypes.Capability, - sourcePort string, - sourceChannel string, - timeoutHeight clienttypes.Height, - timeoutTimestamp uint64, - data []byte, -) (sequence uint64, err error) { - return im.ics4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) -} - -// WriteAcknowledgement implements the ICS4 Wrapper interface -func (im IBCMiddleware) WriteAcknowledgement( - ctx sdk.Context, - chanCap *capabilitytypes.Capability, - packet ibcexported.PacketI, - ack ibcexported.Acknowledgement, -) error { - return im.ics4Wrapper.WriteAcknowledgement(ctx, chanCap, packet, ack) -} - -func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { - return im.ics4Wrapper.GetAppVersion(ctx, portID, channelID) -} - -// OnRecvPacket implements the IBCMiddleware interface -func (im IBCMiddleware) OnRecvPacket( - ctx sdk.Context, - packet channeltypes.Packet, - relayer sdk.AccAddress, -) ibcexported.Acknowledgement { - isIcs20, data := isIcs20Packet(packet) - if !isIcs20 { - return im.app.OnRecvPacket(ctx, packet, relayer) - } - - // Validate the memo - isEVMRouted, msg, err := validateAndParseMemo(data.GetMemo(), data.Receiver) - if !isEVMRouted { - return im.app.OnRecvPacket(ctx, packet, relayer) - } else if err != nil { - return newEmitErrorAcknowledgement(ctx, err) - } - - // Calculate the receiver / contract caller based on the packet's channel and sender - intermediateSender := deriveIntermediateSender(packet.GetDestChannel(), data.GetSender()) - - // The funds sent on this packet need to be transferred to the intermediary account for the sender. - // For this, we override the ICS20 packet's Receiver (essentially hijacking the funds to this new address) - // and execute the underlying OnRecvPacket() call (which should eventually land on the transfer app's - // relay.go and send the funds to the intermediary account. - // - // If that succeeds, we make the contract call - data.Receiver = intermediateSender - bz, err := json.Marshal(data) - if err != nil { - return newEmitErrorAcknowledgement(ctx, err) - } - packet.Data = bz - - ack := im.app.OnRecvPacket(ctx, packet, relayer) - if !ack.Success() { - return ack - } - - msg.Sender = intermediateSender - _, err = im.execMsg(ctx, &msg) - if err != nil { - return newEmitErrorAcknowledgement(ctx, err) - } - - return ack -} - -func (im IBCMiddleware) execMsg(ctx sdk.Context, msg *evmtypes.MsgCall) (*evmtypes.MsgCallResponse, error) { - moveMsgServer := evmkeeper.NewMsgServerImpl(im.evmKeeper) - return moveMsgServer.Call(ctx, msg) -} diff --git a/app/ibc-middleware/util.go b/app/ibc-middleware/util.go deleted file mode 100644 index 5be2aac..0000000 --- a/app/ibc-middleware/util.go +++ /dev/null @@ -1,96 +0,0 @@ -package ibc_middleware - -import ( - "encoding/json" - "fmt" - - "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/address" - - transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" - channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" - - evmtypes "github.com/initia-labs/minievm/x/evm/types" -) - -const senderPrefix = "ibc-move-evm-intermediary" - -// deriveIntermediateSender compute intermediate sender address -// Bech32(Hash(Hash("ibc-hook-intermediary") + channelID/sender)) -func deriveIntermediateSender(channel, originalSender string) string { - senderStr := fmt.Sprintf("%s/%s", channel, originalSender) - senderAddr := sdk.AccAddress(address.Hash(senderPrefix, []byte(senderStr))) - return senderAddr.String() -} - -func isIcs20Packet(packet channeltypes.Packet) (isIcs20 bool, ics20data transfertypes.FungibleTokenPacketData) { - var data transfertypes.FungibleTokenPacketData - if err := json.Unmarshal(packet.GetData(), &data); err != nil { - return false, data - } - return true, data -} - -func validateAndParseMemo(memo, receiver string) (isEVMRouted bool, msg evmtypes.MsgCall, err error) { - isEVMRouted, metadata := jsonStringHasKey(memo, "evm") - if !isEVMRouted { - return - } - - evmRaw := metadata["evm"] - bz, err := json.Marshal(evmRaw) - if err != nil { - err = errors.Wrap(channeltypes.ErrInvalidPacket, err.Error()) - return - } - - err = json.Unmarshal(bz, &msg) - if err != nil { - err = errors.Wrap(channeltypes.ErrInvalidPacket, err.Error()) - return - } - - if receiver != msg.ContractAddr { - err = errors.Wrap(channeltypes.ErrInvalidPacket, "receiver is not properly set") - return - } - - return -} - -// jsonStringHasKey parses the memo as a json object and checks if it contains the key. -func jsonStringHasKey(memo, key string) (found bool, jsonObject map[string]interface{}) { - jsonObject = make(map[string]interface{}) - - // If there is no memo, the packet was either sent with an earlier version of IBC, or the memo was - // intentionally left blank. Nothing to do here. Ignore the packet and pass it down the stack. - if len(memo) == 0 { - return false, jsonObject - } - - // the jsonObject must be a valid JSON object - err := json.Unmarshal([]byte(memo), &jsonObject) - if err != nil { - return false, jsonObject - } - - // If the key doesn't exist, there's nothing to do on this hook. Continue by passing the packet - // down the stack - _, ok := jsonObject[key] - if !ok { - return false, jsonObject - } - - return true, jsonObject -} - -// newEmitErrorAcknowledgement creates a new error acknowledgement after having emitted an event with the -// details of the error. -func newEmitErrorAcknowledgement(ctx sdk.Context, err error) channeltypes.Acknowledgement { - return channeltypes.Acknowledgement{ - Response: &channeltypes.Acknowledgement_Error{ - Error: fmt.Sprintf("ibc evm hook error: %s", err.Error()), - }, - } -} diff --git a/app/ibc-middleware/util_test.go b/app/ibc-middleware/util_test.go deleted file mode 100644 index 25605b0..0000000 --- a/app/ibc-middleware/util_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package ibc_middleware - -import ( - "testing" - - "github.com/stretchr/testify/require" - - evmtypes "github.com/initia-labs/minievm/x/evm/types" -) - -func Test_validateAndParseMemo(t *testing.T) { - memo := ` - { - "evm" : { - "sender": "init_addr", - "contract_addr": "contract_addr", - "input": "" - } - }` - isEVMRouted, msg, err := validateAndParseMemo(memo, "contract_addr") - require.True(t, isEVMRouted) - require.NoError(t, err) - require.Equal(t, evmtypes.MsgCall{ - Sender: "init_addr", - ContractAddr: "contract_addr", - Input: "", - }, msg) - - // invalid receiver - isEVMRouted, _, err = validateAndParseMemo(memo, "invalid_addr") - require.True(t, isEVMRouted) - require.Error(t, err) - - isEVMRouted, _, err = validateAndParseMemo("hihi", "invalid_addr") - require.False(t, isEVMRouted) - require.NoError(t, err) -}