cosmos-sdk/x/ibc/applications/transfer/keeper/relay.go

407 lines
14 KiB
Go

package keeper
import (
"fmt"
"strings"
"github.com/armon/go-metrics"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer/types"
clienttypes "github.com/cosmos/cosmos-sdk/x/ibc/core/02-client/types"
channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/core/04-channel/types"
host "github.com/cosmos/cosmos-sdk/x/ibc/core/24-host"
)
// SendTransfer handles transfer sending logic. There are 2 possible cases:
//
// 1. Sender chain is acting as the source zone. The coins are transferred
// to an escrow address (i.e locked) on the sender chain and then transferred
// to the receiving chain through IBC TAO logic. It is expected that the
// receiving chain will mint vouchers to the receiving address.
//
// 2. Sender chain is acting as the sink zone. The coins (vouchers) are burned
// on the sender chain and then transferred to the receiving chain though IBC
// TAO logic. It is expected that the receiving chain, which had previously
// sent the original denomination, will unescrow the fungible token and send
// it to the receiving address.
//
// Another way of thinking of source and sink zones is through the token's
// timeline. Each send to any chain other than the one it was previously
// received from is a movement forwards in the token's timeline. This causes
// trace to be added to the token's history and the destination port and
// destination channel to be prefixed to the denomination. In these instances
// the sender chain is acting as the source zone. When the token is sent back
// to the chain it previously received from, the prefix is removed. This is
// a backwards movement in the token's timeline and the sender chain
// is acting as the sink zone.
//
// Example:
// These steps of transfer occur: A -> B -> C -> A -> C -> B -> A
//
// 1. A -> B : sender chain is source zone. Denom upon receiving: 'B/denom'
// 2. B -> C : sender chain is source zone. Denom upon receiving: 'C/B/denom'
// 3. C -> A : sender chain is source zone. Denom upon receiving: 'A/C/B/denom'
// 4. A -> C : sender chain is sink zone. Denom upon receiving: 'C/B/denom'
// 5. C -> B : sender chain is sink zone. Denom upon receiving: 'B/denom'
// 6. B -> A : sender chain is sink zone. Denom upon receiving: 'denom'
func (k Keeper) SendTransfer(
ctx sdk.Context,
sourcePort,
sourceChannel string,
token sdk.Coin,
sender sdk.AccAddress,
receiver string,
timeoutHeight clienttypes.Height,
timeoutTimestamp uint64,
) error {
if !k.GetSendEnabled(ctx) {
return types.ErrSendDisabled
}
sourceChannelEnd, found := k.channelKeeper.GetChannel(ctx, sourcePort, sourceChannel)
if !found {
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", sourcePort, sourceChannel)
}
destinationPort := sourceChannelEnd.GetCounterparty().GetPortID()
destinationChannel := sourceChannelEnd.GetCounterparty().GetChannelID()
// get the next sequence
sequence, found := k.channelKeeper.GetNextSequenceSend(ctx, sourcePort, sourceChannel)
if !found {
return sdkerrors.Wrapf(
channeltypes.ErrSequenceSendNotFound,
"source port: %s, source channel: %s", sourcePort, sourceChannel,
)
}
// begin createOutgoingPacket logic
// See spec for this logic: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay
channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel))
if !ok {
return sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
}
// NOTE: denomination and hex hash correctness checked during msg.ValidateBasic
fullDenomPath := token.Denom
var err error
// deconstruct the token denomination into the denomination trace info
// to determine if the sender is the source chain
if strings.HasPrefix(token.Denom, "ibc/") {
fullDenomPath, err = k.DenomPathFromHash(ctx, token.Denom)
if err != nil {
return err
}
}
labels := []metrics.Label{
telemetry.NewLabel("destination-port", destinationPort),
telemetry.NewLabel("destination-channel", destinationChannel),
}
// NOTE: SendTransfer simply sends the denomination as it exists on its own
// chain inside the packet data. The receiving chain will perform denom
// prefixing as necessary.
if types.SenderChainIsSource(sourcePort, sourceChannel, fullDenomPath) {
labels = append(labels, telemetry.NewLabel("source", "true"))
// create the escrow address for the tokens
escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel)
// escrow source tokens. It fails if balance insufficient.
if err := k.bankKeeper.SendCoins(
ctx, sender, escrowAddress, sdk.NewCoins(token),
); err != nil {
return err
}
} else {
labels = append(labels, telemetry.NewLabel("source", "false"))
// transfer the coins to the module account and burn them
if err := k.bankKeeper.SendCoinsFromAccountToModule(
ctx, sender, types.ModuleName, sdk.NewCoins(token),
); err != nil {
return err
}
if err := k.bankKeeper.BurnCoins(
ctx, types.ModuleName, sdk.NewCoins(token),
); err != nil {
// NOTE: should not happen as the module account was
// retrieved on the step above and it has enough balace
// to burn.
panic(fmt.Sprintf("cannot burn coins after a successful send to a module account: %v", err))
}
}
packetData := types.NewFungibleTokenPacketData(
fullDenomPath, token.Amount.Uint64(), sender.String(), receiver,
)
packet := channeltypes.NewPacket(
packetData.GetBytes(),
sequence,
sourcePort,
sourceChannel,
destinationPort,
destinationChannel,
timeoutHeight,
timeoutTimestamp,
)
if err := k.channelKeeper.SendPacket(ctx, channelCap, packet); err != nil {
return err
}
defer func() {
telemetry.SetGaugeWithLabels(
[]string{"tx", "msg", "ibc", "transfer"},
float32(token.Amount.Int64()),
[]metrics.Label{telemetry.NewLabel("denom", fullDenomPath)},
)
telemetry.IncrCounterWithLabels(
[]string{"ibc", types.ModuleName, "send"},
1,
labels,
)
}()
return nil
}
// OnRecvPacket processes a cross chain fungible token transfer. If the
// sender chain is the source of minted tokens then vouchers will be minted
// and sent to the receiving address. Otherwise if the sender chain is sending
// back tokens this chain originally transferred to it, the tokens are
// unescrowed and sent to the receiving address.
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
// validate packet data upon receiving
if err := data.ValidateBasic(); err != nil {
return err
}
if !k.GetReceiveEnabled(ctx) {
return types.ErrReceiveDisabled
}
// decode the receiver address
receiver, err := sdk.AccAddressFromBech32(data.Receiver)
if err != nil {
return err
}
labels := []metrics.Label{
telemetry.NewLabel("source-port", packet.GetSourcePort()),
telemetry.NewLabel("source-channel", packet.GetSourceChannel()),
}
// This is the prefix that would have been prefixed to the denomination
// on sender chain IF and only if the token originally came from the
// receiving chain.
//
// NOTE: We use SourcePort and SourceChannel here, because the counterparty
// chain would have prefixed with DestPort and DestChannel when originally
// receiving this coin as seen in the "sender chain is the source" condition.
if types.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) {
// sender chain is not the source, unescrow tokens
// remove prefix added by sender chain
voucherPrefix := types.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 := types.ParseDenomTrace(unprefixedDenom)
if denomTrace.Path != "" {
denom = denomTrace.IBCDenom()
}
token := sdk.NewCoin(denom, sdk.NewIntFromUint64(data.Amount))
// unescrow tokens
escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
if err := k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(token)); err != nil {
// NOTE: this error is only expected to occur given an unexpected bug or a malicious
// counterparty module. The bug may occur in bank or any part of the code that allows
// the escrow address to be drained. A malicious counterparty module could drain the
// escrow address by allowing more tokens to be sent back then were escrowed.
return sdkerrors.Wrap(err, "unable to unescrow tokens, this may be caused by a malicious counterparty module or a bug: please open an issue on counterparty module")
}
defer func() {
telemetry.SetGaugeWithLabels(
[]string{"ibc", types.ModuleName, "packet", "receive"},
float32(data.Amount),
[]metrics.Label{telemetry.NewLabel("denom", unprefixedDenom)},
)
telemetry.IncrCounterWithLabels(
[]string{"ibc", types.ModuleName, "receive"},
1,
append(
labels, telemetry.NewLabel("source", "true"),
),
)
}()
return nil
}
// sender chain is the source, mint vouchers
// since SendPacket did not prefix the denomination, we must prefix denomination here
sourcePrefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
// NOTE: sourcePrefix contains the trailing "/"
prefixedDenom := sourcePrefix + data.Denom
// construct the denomination trace from the full raw denomination
denomTrace := types.ParseDenomTrace(prefixedDenom)
traceHash := denomTrace.Hash()
if !k.HasDenomTrace(ctx, traceHash) {
k.SetDenomTrace(ctx, denomTrace)
}
voucherDenom := denomTrace.IBCDenom()
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeDenomTrace,
sdk.NewAttribute(types.AttributeKeyTraceHash, traceHash.String()),
sdk.NewAttribute(types.AttributeKeyDenom, voucherDenom),
),
)
voucher := sdk.NewCoin(voucherDenom, sdk.NewIntFromUint64(data.Amount))
// mint new tokens if the source of the transfer is the same chain
if err := k.bankKeeper.MintCoins(
ctx, types.ModuleName, sdk.NewCoins(voucher),
); err != nil {
return err
}
// send to receiver
if err := k.bankKeeper.SendCoinsFromModuleToAccount(
ctx, types.ModuleName, receiver, sdk.NewCoins(voucher),
); err != nil {
panic(fmt.Sprintf("unable to send coins from module to account despite previously minting coins to module account: %v", err))
}
defer func() {
telemetry.SetGaugeWithLabels(
[]string{"ibc", types.ModuleName, "packet", "receive"},
float32(data.Amount),
[]metrics.Label{telemetry.NewLabel("denom", data.Denom)},
)
telemetry.IncrCounterWithLabels(
[]string{"ibc", types.ModuleName, "receive"},
1,
append(
labels, telemetry.NewLabel("source", "false"),
),
)
}()
return nil
}
// OnAcknowledgementPacket responds to the the success or failure of a packet
// acknowledgement written on the receiving chain. If the acknowledgement
// was a success then nothing occurs. If the acknowledgement failed, then
// the sender is refunded their tokens using the refundPacketToken function.
func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData, ack channeltypes.Acknowledgement) error {
switch ack.Response.(type) {
case *channeltypes.Acknowledgement_Error:
return k.refundPacketToken(ctx, packet, data)
default:
// the acknowledgement succeeded on the receiving chain so nothing
// needs to be executed and no error needs to be returned
return nil
}
}
// OnTimeoutPacket refunds the sender since the original packet sent was
// never received and has been timed out.
func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
return k.refundPacketToken(ctx, packet, data)
}
// refundPacketToken will unescrow and send back the tokens back to sender
// if the sending chain was the source chain. Otherwise, the sent tokens
// were burnt in the original send so new tokens are minted and sent to
// the sending address.
func (k Keeper) refundPacketToken(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
// NOTE: packet data type already checked in handler.go
// parse the denomination from the full denom path
trace := types.ParseDenomTrace(data.Denom)
token := sdk.NewCoin(trace.IBCDenom(), sdk.NewIntFromUint64(data.Amount))
// decode the sender address
sender, err := sdk.AccAddressFromBech32(data.Sender)
if err != nil {
return err
}
if types.SenderChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), data.Denom) {
// unescrow tokens back to sender
escrowAddress := types.GetEscrowAddress(packet.GetSourcePort(), packet.GetSourceChannel())
if err := k.bankKeeper.SendCoins(ctx, escrowAddress, sender, sdk.NewCoins(token)); err != nil {
// NOTE: this error is only expected to occur given an unexpected bug or a malicious
// counterparty module. The bug may occur in bank or any part of the code that allows
// the escrow address to be drained. A malicious counterparty module could drain the
// escrow address by allowing more tokens to be sent back then were escrowed.
return sdkerrors.Wrap(err, "unable to unescrow tokens, this may be caused by a malicious counterparty module or a bug: please open an issue on counterparty module")
}
return nil
}
// mint vouchers back to sender
if err := k.bankKeeper.MintCoins(
ctx, types.ModuleName, sdk.NewCoins(token),
); err != nil {
return err
}
if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(token)); err != nil {
panic(fmt.Sprintf("unable to send coins from module to account despite previously minting coins to module account: %v", err))
}
return nil
}
// DenomPathFromHash returns the full denomination path prefix from an ibc denom with a hash
// component.
func (k Keeper) DenomPathFromHash(ctx sdk.Context, denom string) (string, error) {
// trim the denomination prefix, by default "ibc/"
hexHash := denom[len(types.DenomPrefix+"/"):]
hash, err := types.ParseHexHash(hexHash)
if err != nil {
return "", sdkerrors.Wrap(types.ErrInvalidDenomForTransfer, err.Error())
}
denomTrace, found := k.GetDenomTrace(ctx, hash)
if !found {
return "", sdkerrors.Wrap(types.ErrTraceNotFound, hexHash)
}
fullDenomPath := denomTrace.GetFullDenomPath()
return fullDenomPath, nil
}