295 lines
11 KiB
Go
295 lines
11 KiB
Go
package keeper
|
|
|
|
import (
|
|
"strings"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
"github.com/cosmos/cosmos-sdk/x/ibc-transfer/types"
|
|
channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/types"
|
|
host "github.com/cosmos/cosmos-sdk/x/ibc/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,
|
|
timeoutTimestamp uint64,
|
|
) error {
|
|
|
|
if !k.GetTransfersEnabled(ctx) {
|
|
return types.ErrTransfersDisabled
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
// 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 {
|
|
// 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.
|
|
return err
|
|
}
|
|
}
|
|
|
|
packetData := types.NewFungibleTokenPacketData(
|
|
fullDenomPath, token.Amount.Uint64(), sender.String(), receiver,
|
|
)
|
|
|
|
packet := channeltypes.NewPacket(
|
|
packetData.GetBytes(),
|
|
sequence,
|
|
sourcePort,
|
|
sourceChannel,
|
|
destinationPort,
|
|
destinationChannel,
|
|
timeoutHeight,
|
|
timeoutTimestamp,
|
|
)
|
|
|
|
return k.channelKeeper.SendPacket(ctx, channelCap, packet)
|
|
}
|
|
|
|
// 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.GetTransfersEnabled(ctx) {
|
|
return types.ErrTransfersDisabled
|
|
}
|
|
|
|
// decode the receiver address
|
|
receiver, err := sdk.AccAddressFromBech32(data.Receiver)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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):]
|
|
token := sdk.NewCoin(unprefixedDenom, sdk.NewIntFromUint64(data.Amount))
|
|
|
|
// unescrow tokens
|
|
escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
|
|
return k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(token))
|
|
}
|
|
|
|
// 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
|
|
return k.bankKeeper.SendCoinsFromModuleToAccount(
|
|
ctx, types.ModuleName, receiver, sdk.NewCoins(voucher),
|
|
)
|
|
}
|
|
|
|
// 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 types.FungibleTokenPacketAcknowledgement) error {
|
|
if !ack.Success {
|
|
return k.refundPacketToken(ctx, packet, data)
|
|
}
|
|
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
|
|
|
|
token := sdk.NewCoin(data.Denom, 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(), token.Denom) {
|
|
// unescrow tokens back to sender
|
|
escrowAddress := types.GetEscrowAddress(packet.GetSourcePort(), packet.GetSourceChannel())
|
|
return k.bankKeeper.SendCoins(ctx, escrowAddress, sender, sdk.NewCoins(token))
|
|
}
|
|
|
|
// mint vouchers back to sender
|
|
if err := k.bankKeeper.MintCoins(
|
|
ctx, types.ModuleName, sdk.NewCoins(token),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, sdk.NewCoins(token))
|
|
}
|
|
|
|
// 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) {
|
|
hexHash := denom[4:]
|
|
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
|
|
}
|