Skip to content

Commit

Permalink
Merge branch 'andrew/non-refundable-recovery' into dummy_middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Reecepbcups committed Nov 20, 2023
2 parents 1492f6e + db2db9b commit 966ddfe
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
Expand All @@ -15,6 +16,7 @@ import (
storetypes "github.com/cosmos/cosmos-sdk/store/types"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/bech32"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"

Expand Down Expand Up @@ -96,6 +98,80 @@ func (k *Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", "x/"+ibcexported.ModuleName+"-"+types.ModuleName)
}

// moveFundsToUserRecoverableAccount will move the funds from the escrow account to the user recoverable account
// this is only used when the maximum timeouts have been reached or there is an acknowledgement error and the packet is nonrefundable,
// i.e. an operation has occurred to make the original packet funds inaccessible to the user, e.g. a swap.
// We cannot refund the funds back to the original chain, so we move them to an account on this chain that the user can access.
func (k *Keeper) moveFundsToUserRecoverableAccount(
ctx sdk.Context,
packet channeltypes.Packet,
data transfertypes.FungibleTokenPacketData,
inFlightPacket *types.InFlightPacket,
) error {
fullDenomPath := data.Denom

amount, ok := sdk.NewIntFromString(data.Amount)
if !ok {
return fmt.Errorf("failed to parse amount from packet data for forward recovery: %s", data.Amount)
}
denomTrace := transfertypes.ParseDenomTrace(fullDenomPath)
token := sdk.NewCoin(denomTrace.IBCDenom(), amount)

userAccount, err := userRecoverableAccount(inFlightPacket)
if err != nil {
return fmt.Errorf("failed to get user recoverable account: %w", err)
}

if !transfertypes.SenderChainIsSource(packet.SourcePort, packet.SourceChannel, fullDenomPath) {
// mint vouchers back to sender
if err := k.bankKeeper.MintCoins(
ctx, transfertypes.ModuleName, sdk.NewCoins(token),
); err != nil {
return err
}

if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, transfertypes.ModuleName, userAccount, 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
}

escrowAddress := transfertypes.GetEscrowAddress(packet.SourcePort, packet.SourceChannel)

if err := k.bankKeeper.SendCoins(
ctx, escrowAddress, userAccount, sdk.NewCoins(token),
); err != nil {
return fmt.Errorf("failed to send coins from escrow account to user recoverable account: %w", err)
}

// update the total escrow amount for the denom.
k.unescrowToken(ctx, token)

return nil
}

// userRecoverableAccount finds an account on this chain that the original sender of the packet can recover funds from.
// If the destination receiver of the original packet is a valid bech32 address for this chain, we use that address.
// Otherwise, if the sender of the original packet is a valid bech32 address for another chain, we translate that address to this chain.
// Note that for the fallback, the coin type of the source chain sender account must be compatible with this chain.
func userRecoverableAccount(inFlightPacket *types.InFlightPacket) (sdk.AccAddress, error) {
var originalData transfertypes.FungibleTokenPacketData
err := transfertypes.ModuleCdc.UnmarshalJSON(inFlightPacket.PacketData, &originalData)
if err == nil {
sender, err := sdk.AccAddressFromBech32(originalData.Receiver)
if err == nil {
return sender, nil
}
}

_, sender, fallbackErr := bech32.DecodeAndConvert(inFlightPacket.OriginalSenderAddress)
if fallbackErr == nil {
return sender, nil
}

return nil, fmt.Errorf("failed to decode bech32 addresses: %w", errors.Join(err, fallbackErr))
}

func (k *Keeper) WriteAcknowledgementForForwardedPacket(
ctx sdk.Context,
packet channeltypes.Packet,
Expand All @@ -116,6 +192,12 @@ func (k *Keeper) WriteAcknowledgementForForwardedPacket(
// If this packet is non-refundable due to some action that took place between the initial ibc transfer and the forward
// we write a successful ack containing details on what happened regardless of ack error or timeout
if inFlightPacket.Nonrefundable {
// we are not allowed to refund back to the source chain.
// attempt to move funds to user recoverable account on this chain.
if err := k.moveFundsToUserRecoverableAccount(ctx, packet, data, inFlightPacket); err != nil {
return err
}

ackResult := fmt.Sprintf("packet forward failed after point of no return: %s", ack.GetError())
newAck := channeltypes.NewResultAcknowledgement([]byte(ackResult))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type DistributionKeeper interface {
// BankKeeper defines the expected bank keeper
type BankKeeper interface {
SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error
}
28 changes: 28 additions & 0 deletions middleware/packet-forward-middleware/test/mock/bank_keeper.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 966ddfe

Please sign in to comment.