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

252 lines
8.0 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 the source chain of the coins (i.e where they were minted): the coins
// are transferred to an escrow address (i.e locked) on the sender chain and then
// transferred to the destination chain (i.e not the source chain) via a packet
// with the corresponding fungible token data.
//
// 2. Coins are not native from the sender chain (i.e tokens sent where transferred over
// through IBC already): the coins are burned and then a packet is sent to the
// source chain of the tokens.
func (k Keeper) SendTransfer(
ctx sdk.Context,
sourcePort,
sourceChannel string,
amount sdk.Coins,
sender sdk.AccAddress,
receiver string,
timeoutHeight,
timeoutTimestamp uint64,
) error {
sourceChannelEnd, found := k.channelKeeper.GetChannel(ctx, sourcePort, sourceChannel)
if !found {
return sdkerrors.Wrap(channeltypes.ErrChannelNotFound, 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,
)
}
return k.createOutgoingPacket(
ctx, sequence, sourcePort, sourceChannel, destinationPort, destinationChannel,
amount, sender, receiver, timeoutHeight, timeoutTimestamp,
)
}
// See spec for this function: https://github.com/cosmos/ics/tree/master/spec/ics-020-fungible-token-transfer#packet-relay
func (k Keeper) createOutgoingPacket(
ctx sdk.Context,
seq uint64,
sourcePort, sourceChannel,
destinationPort, destinationChannel string,
amount sdk.Coins,
sender sdk.AccAddress,
receiver string,
timeoutHeight, timeoutTimestamp uint64,
) error {
channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel))
if !ok {
return sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
}
// NOTE:
// - Coins transferred from the destination chain should have their denomination
// prefixed with source port and channel IDs.
// - Coins transferred from the source chain can have their denomination
// clear from prefixes when transferred to the escrow account (i.e when they are
// locked) BUT MUST have the destination port and channel ID when constructing
// the packet data.
if len(amount) != 1 {
return sdkerrors.Wrapf(types.ErrOnlyOneDenomAllowed, "%d denoms included", len(amount))
}
prefix := types.GetDenomPrefix(destinationPort, destinationChannel)
source := strings.HasPrefix(amount[0].Denom, prefix)
if source {
// clear the denomination from the prefix to send the coins to the escrow account
coins := make(sdk.Coins, len(amount))
for i, coin := range amount {
if strings.HasPrefix(coin.Denom, prefix) {
coins[i] = sdk.NewCoin(coin.Denom[len(prefix):], coin.Amount)
} else {
coins[i] = coin
}
}
// escrow tokens if the destination chain is the same as the sender's
escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel)
// escrow source tokens. It fails if balance insufficient.
if err := k.bankKeeper.SendCoins(
ctx, sender, escrowAddress, coins,
); err != nil {
return err
}
} else {
// build the receiving denomination prefix if it's not present
prefix = types.GetDenomPrefix(sourcePort, sourceChannel)
for _, coin := range amount {
if !strings.HasPrefix(coin.Denom, prefix) {
return sdkerrors.Wrapf(types.ErrInvalidDenomForTransfer, "denom was: %s", coin.Denom)
}
}
// transfer the coins to the module account and burn them
if err := k.bankKeeper.SendCoinsFromAccountToModule(
ctx, sender, types.ModuleName, amount,
); err != nil {
return err
}
// burn vouchers from the sender's balance if the source is from another chain
if err := k.bankKeeper.BurnCoins(
ctx, types.ModuleName, amount,
); 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(
amount, sender.String(), receiver,
)
packet := channeltypes.NewPacket(
packetData.GetBytes(),
seq,
sourcePort,
sourceChannel,
destinationPort,
destinationChannel,
timeoutHeight,
timeoutTimestamp,
)
return k.channelKeeper.SendPacket(ctx, channelCap, packet)
}
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
// NOTE: packet data type already checked in handler.go
if len(data.Amount) != 1 {
return sdkerrors.Wrapf(types.ErrOnlyOneDenomAllowed, "%d denoms included", len(data.Amount))
}
prefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
source := strings.HasPrefix(data.Amount[0].Denom, prefix)
// decode the receiver address
receiver, err := sdk.AccAddressFromBech32(data.Receiver)
if err != nil {
return err
}
if source {
// mint new tokens if the source of the transfer is the same chain
if err := k.bankKeeper.MintCoins(
ctx, types.ModuleName, data.Amount,
); err != nil {
return err
}
// send to receiver
return k.bankKeeper.SendCoinsFromModuleToAccount(
ctx, types.ModuleName, receiver, data.Amount,
)
}
// check the denom prefix
prefix = types.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
coins := make(sdk.Coins, len(data.Amount))
for i, coin := range data.Amount {
if !strings.HasPrefix(coin.Denom, prefix) {
return sdkerrors.Wrapf(
sdkerrors.ErrInvalidCoins,
"%s doesn't contain the prefix '%s'", coin.Denom, prefix,
)
}
coins[i] = sdk.NewCoin(coin.Denom[len(prefix):], coin.Amount)
}
// unescrow tokens
escrowAddress := types.GetEscrowAddress(packet.GetDestPort(), packet.GetDestChannel())
return k.bankKeeper.SendCoins(ctx, escrowAddress, receiver, coins)
}
func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData, ack types.FungibleTokenPacketAcknowledgement) error {
if !ack.Success {
return k.refundPacketAmount(ctx, packet, data)
}
return nil
}
func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
return k.refundPacketAmount(ctx, packet, data)
}
func (k Keeper) refundPacketAmount(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketData) error {
// NOTE: packet data type already checked in handler.go
if len(data.Amount) != 1 {
return sdkerrors.Wrapf(types.ErrOnlyOneDenomAllowed, "%d denoms included", len(data.Amount))
}
// check the denom prefix
prefix := types.GetDenomPrefix(packet.GetDestPort(), packet.GetDestChannel())
source := strings.HasPrefix(data.Amount[0].Denom, prefix)
// decode the sender address
sender, err := sdk.AccAddressFromBech32(data.Sender)
if err != nil {
return err
}
if source {
coins := make(sdk.Coins, len(data.Amount))
for i, coin := range data.Amount {
coin := coin
if !strings.HasPrefix(coin.Denom, prefix) {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "%s doesn't contain the prefix '%s'", coin.Denom, prefix)
}
coins[i] = sdk.NewCoin(coin.Denom[len(prefix):], coin.Amount)
}
// unescrow tokens back to sender
escrowAddress := types.GetEscrowAddress(packet.GetSourcePort(), packet.GetSourceChannel())
return k.bankKeeper.SendCoins(ctx, escrowAddress, sender, coins)
}
// mint vouchers back to sender
if err := k.bankKeeper.MintCoins(
ctx, types.ModuleName, data.Amount,
); err != nil {
return err
}
return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, sender, data.Amount)
}