mukan-ibc/modules/apps/transfer/keeper/relay.go
Mukan Erkin Törük 6852832fe8
Some checks failed
CodeQL / Analyze (push) Waiting to run
Docker Build & Push Simapp (main) / docker-build (push) Waiting to run
golangci-lint / lint (push) Waiting to run
Tests / Code Coverage / build (amd64) (push) Waiting to run
Tests / Code Coverage / build (arm64) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[additional-args:-tags="test_e2e" name:e2e path:./e2e]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:08-wasm path:./modules/light-clients/08-wasm]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:ibc-go path:.]) (push) Waiting to run
Deploy to GitHub Pages / Deploy to GitHub Pages (push) Has been cancelled
Buf-Push / push (push) Has been cancelled
initial: sovereign Mukan Network fork
2026-05-11 03:18:28 +03:00

397 lines
14 KiB
Go

package keeper
import (
"fmt"
"strings"
errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/ibc-go/v10/modules/apps/transfer/internal/events"
"github.com/cosmos/ibc-go/v10/modules/apps/transfer/types"
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors"
)
// 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 types.Token,
sender sdk.AccAddress,
) error {
if !k.GetParams(ctx).SendEnabled {
return types.ErrSendDisabled
}
if k.IsBlockedAddr(sender) {
return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to send funds", sender)
}
coin, err := token.ToCoin()
if err != nil {
return err
}
if err := k.BankKeeper.IsSendEnabledCoins(ctx, coin); err != nil {
return errorsmod.Wrap(types.ErrSendDisabled, err.Error())
}
// 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 the denom is prefixed by the port and channel on which we are sending
// the token, then we must be returning the token back to the chain they originated from
if token.Denom.HasPrefix(sourcePort, sourceChannel) {
// transfer the coins to the module account and burn them
if err := k.BankKeeper.SendCoinsFromAccountToModule(
ctx, sender, types.ModuleName, sdk.NewCoins(coin),
); err != nil {
return err
}
if err := k.BankKeeper.BurnCoins(
ctx, types.ModuleName, sdk.NewCoins(coin),
); err != nil {
// NOTE: should not happen as the module account was
// retrieved on the step above and it has enough balance
// to burn.
panic(fmt.Errorf("cannot burn coins after a successful send to a module account: %v", err))
}
} else {
// obtain the escrow address for the source channel end
escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel)
if err := k.EscrowCoin(ctx, sender, escrowAddress, coin); err != nil {
return err
}
}
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,
data types.InternalTransferRepresentation,
sourcePort string,
sourceChannel string,
destPort string,
destChannel string,
) error {
// validate packet data upon receiving
if err := data.ValidateBasic(); err != nil {
return errorsmod.Wrapf(err, "error validating ICS-20 transfer packet data")
}
if !k.GetParams(ctx).ReceiveEnabled {
return types.ErrReceiveDisabled
}
receiver, err := k.addressCodec.StringToBytes(data.Receiver)
if err != nil {
return errorsmod.Wrapf(ibcerrors.ErrInvalidAddress, "failed to decode receiver address: %s", data.Receiver)
}
if k.IsBlockedAddr(receiver) {
return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to receive funds", receiver)
}
token := data.Token
// parse the transfer amount
transferAmount, ok := sdkmath.NewIntFromString(token.Amount)
if !ok {
return errorsmod.Wrapf(types.ErrInvalidAmount, "unable to parse transfer amount: %s", token.Amount)
}
// 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 token.
if token.Denom.HasPrefix(sourcePort, sourceChannel) {
// sender chain is not the source, unescrow tokens
// remove prefix added by sender chain
token.Denom.Trace = token.Denom.Trace[1:]
coin := sdk.NewCoin(token.Denom.IBCDenom(), transferAmount)
escrowAddress := types.GetEscrowAddress(destPort, destChannel)
if err := k.UnescrowCoin(ctx, escrowAddress, receiver, coin); err != nil {
return err
}
} else {
// sender chain is the source, mint vouchers
// since SendPacket did not prefix the denomination, we must add the destination port and channel to the trace
trace := []types.Hop{types.NewHop(destPort, destChannel)}
token.Denom.Trace = append(trace, token.Denom.Trace...)
if !k.HasDenom(ctx, token.Denom.Hash()) {
k.SetDenom(ctx, token.Denom)
}
voucherDenom := token.Denom.IBCDenom()
if !k.BankKeeper.HasDenomMetaData(ctx, voucherDenom) {
k.SetDenomMetadata(ctx, token.Denom)
}
events.EmitDenomEvent(ctx, token)
voucher := sdk.NewCoin(voucherDenom, transferAmount)
// 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 errorsmod.Wrap(err, "failed to mint IBC tokens")
}
// send to receiver
moduleAddr := k.AuthKeeper.GetModuleAddress(types.ModuleName)
if err := k.BankKeeper.SendCoins(
ctx, moduleAddr, receiver, sdk.NewCoins(voucher),
); err != nil {
return errorsmod.Wrapf(err, "failed to send coins to receiver %s", data.Receiver)
}
}
// The ibc_module.go module will return the proper ack.
return nil
}
// OnAcknowledgementPacket responds to the success or failure of a packet acknowledgment
// written on the receiving chain.
//
// If the acknowledgement was a success then nothing occurs. Otherwise,
// if the acknowledgement failed, then the sender is refunded their tokens.
func (k Keeper) OnAcknowledgementPacket(
ctx sdk.Context,
sourcePort string,
sourceChannel string,
data types.InternalTransferRepresentation,
ack channeltypes.Acknowledgement,
) error {
switch ack.Response.(type) {
case *channeltypes.Acknowledgement_Result:
// the acknowledgement succeeded on the receiving chain so nothing
// needs to be executed and no error needs to be returned
return nil
case *channeltypes.Acknowledgement_Error:
if err := k.refundPacketTokens(ctx, sourcePort, sourceChannel, data); err != nil {
return err
}
return nil
default:
return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected one of [%T, %T], got %T", channeltypes.Acknowledgement_Result{}, channeltypes.Acknowledgement_Error{}, ack.Response)
}
}
// OnTimeoutPacket processes a transfer packet timeout by refunding the tokens to the sender
func (k Keeper) OnTimeoutPacket(
ctx sdk.Context,
sourcePort string,
sourceChannel string,
data types.InternalTransferRepresentation,
) error {
return k.refundPacketTokens(ctx, sourcePort, sourceChannel, data)
}
// refundPacketTokens will unescrow and send back the token back to sender
// if the sending chain was the source chain. Otherwise, the sent token
// were burnt in the original send so new tokens are minted and sent to
// the sending address.
func (k Keeper) refundPacketTokens(
ctx sdk.Context,
sourcePort string,
sourceChannel string,
data types.InternalTransferRepresentation,
) error {
// NOTE: packet data type already checked in handler.go
sender, err := k.addressCodec.StringToBytes(data.Sender)
if err != nil {
return err
}
if k.IsBlockedAddr(sender) {
return errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "%s is not allowed to receive funds", sender)
}
// escrow address for unescrowing tokens back to sender
escrowAddress := types.GetEscrowAddress(sourcePort, sourceChannel)
moduleAccountAddr := k.AuthKeeper.GetModuleAddress(types.ModuleName)
token := data.Token
coin, err := token.ToCoin()
if err != nil {
return err
}
// if the token we must refund is prefixed by the source port and channel
// then the tokens were burnt when the packet was sent and we must mint new tokens
if token.Denom.HasPrefix(sourcePort, sourceChannel) {
// mint vouchers back to sender
if err := k.BankKeeper.MintCoins(
ctx, types.ModuleName, sdk.NewCoins(coin),
); err != nil {
return err
}
if err := k.BankKeeper.SendCoins(ctx, moduleAccountAddr, sender, sdk.NewCoins(coin)); err != nil {
panic(fmt.Errorf("unable to send coins from module to account despite previously minting coins to module account: %v", err))
}
} else {
if err := k.UnescrowCoin(ctx, escrowAddress, sender, coin); err != nil {
return err
}
}
return nil
}
// EscrowCoin will send the given coin from the provided sender to the escrow address. It will also
// update the total escrowed amount by adding the escrowed coin's amount to the current total escrow.
func (k Keeper) EscrowCoin(ctx sdk.Context, sender, escrowAddress sdk.AccAddress, coin sdk.Coin) error {
if err := k.BankKeeper.SendCoins(ctx, sender, escrowAddress, sdk.NewCoins(coin)); err != nil {
// failure is expected for insufficient balances
return err
}
// track the total amount in escrow keyed by denomination to allow for efficient iteration
currentTotalEscrow := k.GetTotalEscrowForDenom(ctx, coin.GetDenom())
newTotalEscrow := currentTotalEscrow.Add(coin)
k.SetTotalEscrowForDenom(ctx, newTotalEscrow)
return nil
}
// UnescrowCoin will send the given coin from the escrow address to the provided receiver. It will also
// update the total escrow by deducting the unescrowed coin's amount from the current total escrow.
func (k Keeper) UnescrowCoin(ctx sdk.Context, escrowAddress, receiver sdk.AccAddress, coin sdk.Coin) error {
if err := k.BankKeeper.SendCoins(ctx, escrowAddress, receiver, sdk.NewCoins(coin)); 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 errorsmod.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")
}
// track the total amount in escrow keyed by denomination to allow for efficient iteration
currentTotalEscrow := k.GetTotalEscrowForDenom(ctx, coin.GetDenom())
newTotalEscrow := currentTotalEscrow.Sub(coin)
k.SetTotalEscrowForDenom(ctx, newTotalEscrow)
return nil
}
// tokenFromCoin constructs an IBC token given an SDK coin.
func (k Keeper) TokenFromCoin(ctx sdk.Context, coin sdk.Coin) (types.Token, error) {
// if the coin does not have an IBC denom, return as is
if !strings.HasPrefix(coin.Denom, "ibc/") {
return types.Token{
Denom: types.NewDenom(coin.Denom),
Amount: coin.Amount.String(),
}, nil
}
// NOTE: denomination and hex hash correctness checked during msg.ValidateBasic
denom, err := k.GetDenomFromIBCDenom(ctx, coin.Denom)
if err != nil {
return types.Token{}, err
}
return types.Token{
Denom: denom,
Amount: coin.Amount.String(),
}, nil
}
// GetDenomFromIBCDenom returns the `Denom` given the IBC Denom (ibc/{hex hash}) of the denomination.
// The ibcDenom is the hex hash of the denomination prefixed by "ibc/", often referred to as the IBC denom.
func (k Keeper) GetDenomFromIBCDenom(ctx sdk.Context, ibcDenom string) (types.Denom, error) {
hexHash := ibcDenom[len(types.DenomPrefix+"/"):]
hash, err := types.ParseHexHash(hexHash)
if err != nil {
return types.Denom{}, errorsmod.Wrap(types.ErrInvalidDenomForTransfer, err.Error())
}
denom, found := k.GetDenom(ctx, hash)
if !found {
return types.Denom{}, errorsmod.Wrap(types.ErrDenomNotFound, hexHash)
}
return denom, nil
}
// Deprecated: usage of this function should be replaced by `Keeper.GetDenomFromIBCDenom`
// DenomPathFromHash returns the full denomination path prefix from an ibc denom with a hash
// component.
func (k Keeper) DenomPathFromHash(ctx sdk.Context, ibcDenom string) (string, error) {
denom, err := k.GetDenomFromIBCDenom(ctx, ibcDenom)
if err != nil {
return "", err
}
return denom.Path(), nil
}
// createPacketDataBytesFromVersion creates the packet data bytes to be sent based on the application version.
func createPacketDataBytesFromVersion(appVersion, sender, receiver, memo string, token types.Token) ([]byte, error) {
switch appVersion {
case types.V1:
packetData := types.NewFungibleTokenPacketData(token.Denom.Path(), token.Amount, sender, receiver, memo)
if err := packetData.ValidateBasic(); err != nil {
return nil, errorsmod.Wrapf(err, "failed to validate %s packet data", types.V1)
}
return packetData.GetBytes(), nil
default:
return nil, errorsmod.Wrapf(types.ErrInvalidVersion, "app version must be one of %s", types.SupportedVersions)
}
}