450 lines
16 KiB
Go
450 lines
16 KiB
Go
package ibc_hooks
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
|
|
|
|
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
|
|
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
|
|
channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types"
|
|
ibcexported "github.com/cosmos/ibc-go/v4/modules/core/exported"
|
|
|
|
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/keeper"
|
|
"github.com/wormhole-foundation/wormchain/x/ibc-hooks/types"
|
|
)
|
|
|
|
type ContractAck struct {
|
|
ContractResult []byte `json:"contract_result"`
|
|
IbcAck []byte `json:"ibc_ack"`
|
|
}
|
|
|
|
type WasmHooks struct {
|
|
ContractKeeper *wasmkeeper.PermissionedKeeper
|
|
ibcHooksKeeper *keeper.Keeper
|
|
bech32PrefixAccAddr string
|
|
}
|
|
|
|
func NewWasmHooks(ibcHooksKeeper *keeper.Keeper, contractKeeper *wasmkeeper.PermissionedKeeper, bech32PrefixAccAddr string) WasmHooks {
|
|
return WasmHooks{
|
|
ContractKeeper: contractKeeper,
|
|
ibcHooksKeeper: ibcHooksKeeper,
|
|
bech32PrefixAccAddr: bech32PrefixAccAddr,
|
|
}
|
|
}
|
|
|
|
func (h WasmHooks) ProperlyConfigured() bool {
|
|
return h.ContractKeeper != nil && h.ibcHooksKeeper != nil
|
|
}
|
|
|
|
func (h WasmHooks) OnRecvPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement {
|
|
if !h.ProperlyConfigured() {
|
|
// Not configured
|
|
return im.App.OnRecvPacket(ctx, packet, relayer)
|
|
}
|
|
isIcs20, data := isIcs20Packet(packet)
|
|
if !isIcs20 {
|
|
return im.App.OnRecvPacket(ctx, packet, relayer)
|
|
}
|
|
|
|
// Validate the memo
|
|
isWasmRouted, contractAddr, msgBytes, err := ValidateAndParseMemo(data.GetMemo(), data.Receiver)
|
|
if !isWasmRouted {
|
|
return im.App.OnRecvPacket(ctx, packet, relayer)
|
|
}
|
|
if err != nil {
|
|
return NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation, err.Error())
|
|
}
|
|
if msgBytes == nil || contractAddr == nil { // This should never happen
|
|
return NewEmitErrorAcknowledgement(ctx, types.ErrMsgValidation)
|
|
}
|
|
|
|
// Calculate the receiver / contract caller based on the packet's channel and sender
|
|
channel := packet.GetDestChannel()
|
|
sender := data.GetSender()
|
|
senderBech32, err := keeper.DeriveIntermediateSender(channel, sender, h.bech32PrefixAccAddr)
|
|
if err != nil {
|
|
return NewEmitErrorAcknowledgement(ctx, types.ErrBadSender, fmt.Sprintf("cannot convert sender address %s/%s to bech32: %s", channel, sender, err.Error()))
|
|
}
|
|
|
|
// 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 sunds to the intermediary account.
|
|
//
|
|
// If that succeeds, we make the contract call
|
|
data.Receiver = senderBech32
|
|
bz, err := json.Marshal(data)
|
|
if err != nil {
|
|
return NewEmitErrorAcknowledgement(ctx, types.ErrMarshaling, err.Error())
|
|
}
|
|
packet.Data = bz
|
|
|
|
// Execute the receive
|
|
ack := im.App.OnRecvPacket(ctx, packet, relayer)
|
|
if !ack.Success() {
|
|
return ack
|
|
}
|
|
|
|
amount, ok := sdk.NewIntFromString(data.GetAmount())
|
|
if !ok {
|
|
// This should never happen, as it should've been caught in the underlaying call to OnRecvPacket,
|
|
// but returning here for completeness
|
|
return NewEmitErrorAcknowledgement(ctx, types.ErrInvalidPacket, "Amount is not an int")
|
|
}
|
|
|
|
// The packet's denom is the denom in the sender chain. This needs to be converted to the local denom.
|
|
denom := MustExtractDenomFromPacketOnRecv(packet)
|
|
funds := sdk.NewCoins(sdk.NewCoin(denom, amount))
|
|
|
|
// Execute the contract
|
|
execMsg := wasmtypes.MsgExecuteContract{
|
|
Sender: senderBech32,
|
|
Contract: contractAddr.String(),
|
|
Msg: msgBytes,
|
|
Funds: funds,
|
|
}
|
|
response, err := h.execWasmMsg(ctx, &execMsg)
|
|
if err != nil {
|
|
return NewEmitErrorAcknowledgement(ctx, types.ErrWasmError, err.Error())
|
|
}
|
|
|
|
fullAck := ContractAck{ContractResult: response.Data, IbcAck: ack.Acknowledgement()}
|
|
bz, err = json.Marshal(fullAck)
|
|
if err != nil {
|
|
return NewEmitErrorAcknowledgement(ctx, types.ErrBadResponse, err.Error())
|
|
}
|
|
|
|
return channeltypes.NewResultAcknowledgement(bz)
|
|
}
|
|
|
|
func (h WasmHooks) execWasmMsg(ctx sdk.Context, execMsg *wasmtypes.MsgExecuteContract) (*wasmtypes.MsgExecuteContractResponse, error) {
|
|
if err := execMsg.ValidateBasic(); err != nil {
|
|
return nil, fmt.Errorf(types.ErrBadExecutionMsg, err.Error())
|
|
}
|
|
wasmMsgServer := wasmkeeper.NewMsgServerImpl(h.ContractKeeper)
|
|
return wasmMsgServer.ExecuteContract(sdk.WrapSDKContext(ctx), execMsg)
|
|
}
|
|
|
|
func isIcs20Packet(packet channeltypes.Packet) (isIcs20 bool, ics20data transfertypes.FungibleTokenPacketData) {
|
|
var data transfertypes.FungibleTokenPacketData
|
|
if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
|
|
return false, data
|
|
}
|
|
return true, data
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func ValidateAndParseMemo(memo string, receiver string) (isWasmRouted bool, contractAddr sdk.AccAddress, msgBytes []byte, err error) {
|
|
isWasmRouted, metadata := jsonStringHasKey(memo, "wasm")
|
|
if !isWasmRouted {
|
|
return isWasmRouted, sdk.AccAddress{}, nil, nil
|
|
}
|
|
|
|
wasmRaw := metadata["wasm"]
|
|
|
|
// Make sure the wasm key is a map. If it isn't, ignore this packet
|
|
wasm, ok := wasmRaw.(map[string]interface{})
|
|
if !ok {
|
|
return isWasmRouted, sdk.AccAddress{}, nil,
|
|
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, "wasm metadata is not a valid JSON map object")
|
|
}
|
|
|
|
// Get the contract
|
|
contract, ok := wasm["contract"].(string)
|
|
if !ok {
|
|
// The tokens will be returned
|
|
return isWasmRouted, sdk.AccAddress{}, nil,
|
|
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["contract"]`)
|
|
}
|
|
|
|
contractAddr, err = sdk.AccAddressFromBech32(contract)
|
|
if err != nil {
|
|
return isWasmRouted, sdk.AccAddress{}, nil,
|
|
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] is not a valid bech32 address`)
|
|
}
|
|
|
|
// The contract and the receiver should be the same for the packet to be valid
|
|
if contract != receiver {
|
|
return isWasmRouted, sdk.AccAddress{}, nil,
|
|
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] should be the same as the receiver of the packet`)
|
|
}
|
|
|
|
// Ensure the message key is provided
|
|
if wasm["msg"] == nil {
|
|
return isWasmRouted, sdk.AccAddress{}, nil,
|
|
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["msg"]`)
|
|
}
|
|
|
|
// Make sure the msg key is a map. If it isn't, return an error
|
|
_, ok = wasm["msg"].(map[string]interface{})
|
|
if !ok {
|
|
return isWasmRouted, sdk.AccAddress{}, nil,
|
|
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["msg"] is not a map object`)
|
|
}
|
|
|
|
// Get the message string by serializing the map
|
|
msgBytes, err = json.Marshal(wasm["msg"])
|
|
if err != nil {
|
|
// The tokens will be returned
|
|
return isWasmRouted, sdk.AccAddress{}, nil,
|
|
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, err.Error())
|
|
}
|
|
|
|
return isWasmRouted, contractAddr, msgBytes, nil
|
|
}
|
|
|
|
func (h WasmHooks) SendPacketOverride(i ICS4Middleware, ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI) error {
|
|
concretePacket, ok := packet.(channeltypes.Packet)
|
|
if !ok {
|
|
return i.channel.SendPacket(ctx, chanCap, packet) // continue
|
|
}
|
|
|
|
isIcs20, data := isIcs20Packet(concretePacket)
|
|
if !isIcs20 {
|
|
return i.channel.SendPacket(ctx, chanCap, packet) // continue
|
|
}
|
|
|
|
isCallbackRouted, metadata := jsonStringHasKey(data.GetMemo(), types.IBCCallbackKey)
|
|
if !isCallbackRouted {
|
|
return i.channel.SendPacket(ctx, chanCap, packet) // continue
|
|
}
|
|
|
|
// We remove the callback metadata from the memo as it has already been processed.
|
|
|
|
// If the only available key in the memo is the callback, we should remove the memo
|
|
// from the data completely so the packet is sent without it.
|
|
// This way receiver chains that are on old versions of IBC will be able to process the packet
|
|
|
|
callbackRaw := metadata[types.IBCCallbackKey] // This will be used later.
|
|
delete(metadata, types.IBCCallbackKey)
|
|
bzMetadata, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return sdkerrors.Wrap(err, "Send packet with callback error")
|
|
}
|
|
stringMetadata := string(bzMetadata)
|
|
if stringMetadata == "{}" {
|
|
data.Memo = ""
|
|
} else {
|
|
data.Memo = stringMetadata
|
|
}
|
|
dataBytes, err := json.Marshal(data)
|
|
if err != nil {
|
|
return sdkerrors.Wrap(err, "Send packet with callback error")
|
|
}
|
|
|
|
packetWithoutCallbackMemo := channeltypes.Packet{
|
|
Sequence: concretePacket.Sequence,
|
|
SourcePort: concretePacket.SourcePort,
|
|
SourceChannel: concretePacket.SourceChannel,
|
|
DestinationPort: concretePacket.DestinationPort,
|
|
DestinationChannel: concretePacket.DestinationChannel,
|
|
Data: dataBytes,
|
|
TimeoutTimestamp: concretePacket.TimeoutTimestamp,
|
|
TimeoutHeight: concretePacket.TimeoutHeight,
|
|
}
|
|
|
|
err = i.channel.SendPacket(ctx, chanCap, packetWithoutCallbackMemo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make sure the callback contract is a string and a valid bech32 addr. If it isn't, ignore this packet
|
|
contract, ok := callbackRaw.(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
_, err = sdk.AccAddressFromBech32(contract)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
h.ibcHooksKeeper.StorePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence(), contract)
|
|
return nil
|
|
}
|
|
|
|
func (h WasmHooks) OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error {
|
|
err := im.App.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !h.ProperlyConfigured() {
|
|
// Not configured. Return from the underlying implementation
|
|
return nil
|
|
}
|
|
|
|
contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
|
|
if contract == "" {
|
|
// No callback configured
|
|
return nil
|
|
}
|
|
|
|
contractAddr, err := sdk.AccAddressFromBech32(contract)
|
|
if err != nil {
|
|
return sdkerrors.Wrap(err, "Ack callback error") // The callback configured is not a bech32. Error out
|
|
}
|
|
|
|
success := "false"
|
|
if !IsJsonAckError(acknowledgement) {
|
|
success = "true"
|
|
}
|
|
|
|
// Notify the sender that the ack has been received
|
|
ackAsJson, err := json.Marshal(acknowledgement)
|
|
if err != nil {
|
|
// If the ack is not a json object, error
|
|
return err
|
|
}
|
|
|
|
sudoMsg := []byte(fmt.Sprintf(
|
|
`{"ibc_lifecycle_complete": {"ibc_ack": {"channel": "%s", "sequence": %d, "ack": %s, "success": %s}}}`,
|
|
packet.SourceChannel, packet.Sequence, ackAsJson, success))
|
|
_, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg)
|
|
if err != nil {
|
|
// error processing the callback
|
|
// ToDo: Open Question: Should we also delete the callback here?
|
|
return sdkerrors.Wrap(err, "Ack callback error")
|
|
}
|
|
h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
|
|
return nil
|
|
}
|
|
|
|
func (h WasmHooks) OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error {
|
|
err := im.App.OnTimeoutPacket(ctx, packet, relayer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !h.ProperlyConfigured() {
|
|
// Not configured. Return from the underlying implementation
|
|
return nil
|
|
}
|
|
|
|
contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
|
|
if contract == "" {
|
|
// No callback configured
|
|
return nil
|
|
}
|
|
|
|
contractAddr, err := sdk.AccAddressFromBech32(contract)
|
|
if err != nil {
|
|
return sdkerrors.Wrap(err, "Timeout callback error") // The callback configured is not a bech32. Error out
|
|
}
|
|
|
|
sudoMsg := []byte(fmt.Sprintf(
|
|
`{"ibc_lifecycle_complete": {"ibc_timeout": {"channel": "%s", "sequence": %d}}}`,
|
|
packet.SourceChannel, packet.Sequence))
|
|
_, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg)
|
|
if err != nil {
|
|
// error processing the callback. This could be because the contract doesn't implement the message type to
|
|
// process the callback. Retrying this will not help, so we can delete the callback from storage.
|
|
// Since the packet has timed out, we don't expect any other responses that may trigger the callback.
|
|
ctx.EventManager().EmitEvents(sdk.Events{
|
|
sdk.NewEvent(
|
|
"ibc-timeout-callback-error",
|
|
sdk.NewAttribute("contract", contractAddr.String()),
|
|
sdk.NewAttribute("message", string(sudoMsg)),
|
|
sdk.NewAttribute("error", err.Error()),
|
|
),
|
|
})
|
|
}
|
|
h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence())
|
|
return nil
|
|
}
|
|
|
|
// NewEmitErrorAcknowledgement creates a new error acknowledgement after having emitted an event with the
|
|
// details of the error.
|
|
func NewEmitErrorAcknowledgement(ctx sdk.Context, err error, errorContexts ...string) channeltypes.Acknowledgement {
|
|
errorType := "ibc-acknowledgement-error"
|
|
logger := ctx.Logger().With("module", errorType)
|
|
|
|
attributes := make([]sdk.Attribute, len(errorContexts)+1)
|
|
attributes[0] = sdk.NewAttribute("error", err.Error())
|
|
for i, s := range errorContexts {
|
|
attributes[i+1] = sdk.NewAttribute("error-context", s)
|
|
logger.Error(fmt.Sprintf("error-context: %v", s))
|
|
}
|
|
|
|
ctx.EventManager().EmitEvents(sdk.Events{
|
|
sdk.NewEvent(
|
|
errorType,
|
|
attributes...,
|
|
),
|
|
})
|
|
|
|
return channeltypes.NewErrorAcknowledgement(err)
|
|
}
|
|
|
|
// IsJsonAckError checks an IBC acknowledgement to see if it's an error.
|
|
// This is a replacement for ack.Success() which is currently not working on some circumstances
|
|
func IsJsonAckError(acknowledgement []byte) bool {
|
|
var ackErr channeltypes.Acknowledgement_Error
|
|
if err := json.Unmarshal(acknowledgement, &ackErr); err == nil && len(ackErr.Error) > 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MustExtractDenomFromPacketOnRecv takes a packet with a valid ICS20 token data in the Data field and returns the
|
|
// denom as represented in the local chain.
|
|
// If the data cannot be unmarshalled this function will panic
|
|
func MustExtractDenomFromPacketOnRecv(packet ibcexported.PacketI) string {
|
|
var data transfertypes.FungibleTokenPacketData
|
|
if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
|
|
panic("unable to unmarshal ICS20 packet data")
|
|
}
|
|
|
|
var denom string
|
|
if transfertypes.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) {
|
|
// remove prefix added by sender chain
|
|
voucherPrefix := transfertypes.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
|
|
|
|
unprefixedDenom := data.Denom[len(voucherPrefix):]
|
|
|
|
// coin denomination used in sending from the escrow address
|
|
denom = unprefixedDenom
|
|
|
|
// The denomination used to send the coins is either the native denom or the hash of the path
|
|
// if the denomination is not native.
|
|
denomTrace := transfertypes.ParseDenomTrace(unprefixedDenom)
|
|
if denomTrace.Path != "" {
|
|
denom = denomTrace.IBCDenom()
|
|
}
|
|
} else {
|
|
prefixedDenom := transfertypes.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel()) + data.Denom
|
|
denom = transfertypes.ParseDenomTrace(prefixedDenom).IBCDenom()
|
|
}
|
|
return denom
|
|
}
|