Skip to content

Commit 6b3b5aa

Browse files
authored
Fix and simplify reverts of forwarding state (#6574)
* refactor: initial simplification attempt * refactor: further organize * fix: all tests fixed * docs: improved godocs * fix: logic and testing error * style: self suggestion * docs: suggestion * docs: spellcheck * style: suggestions * style: renamed to revertForwardedPacket * style: suggestion * docs: remove docs * docs: godoc suggestion * style: suggestion * docs: colin suggestions
1 parent ba7c4a8 commit 6b3b5aa

File tree

3 files changed

+148
-123
lines changed

3 files changed

+148
-123
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package keeper
2+
3+
import (
4+
"errors"
5+
6+
errorsmod "cosmossdk.io/errors"
7+
sdkmath "cosmossdk.io/math"
8+
9+
sdk "github.com/cosmos/cosmos-sdk/types"
10+
11+
"github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
12+
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
13+
host "github.com/cosmos/ibc-go/v8/modules/core/24-host"
14+
)
15+
16+
// ackForwardPacketError reverts the receive packet logic that occurs in the middle chain and writes the async ack for the prevPacket
17+
func (k Keeper) ackForwardPacketError(ctx sdk.Context, prevPacket channeltypes.Packet, failedPacketData types.FungibleTokenPacketDataV2) error {
18+
// the forwarded packet has failed, thus the funds have been refunded to the intermediate address.
19+
// we must revert the changes that came from successfully receiving the tokens on our chain
20+
// before propagating the error acknowledgement back to original sender chain
21+
if err := k.revertForwardedPacket(ctx, prevPacket, failedPacketData); err != nil {
22+
return err
23+
}
24+
25+
forwardAck := channeltypes.NewErrorAcknowledgement(errors.New("forwarded packet failed"))
26+
return k.acknowledgeForwardedPacket(ctx, prevPacket, forwardAck)
27+
}
28+
29+
// ackForwardPacketSuccess writes a successful async acknowledgement for the prevPacket
30+
func (k Keeper) ackForwardPacketSuccess(ctx sdk.Context, prevPacket channeltypes.Packet) error {
31+
forwardAck := channeltypes.NewResultAcknowledgement([]byte("forwarded packet succeeded"))
32+
return k.acknowledgeForwardedPacket(ctx, prevPacket, forwardAck)
33+
}
34+
35+
// ackForwardPacketTimeout reverts the receive packet logic that occurs in the middle chain and writes a failed async ack for the prevPacket
36+
func (k Keeper) ackForwardPacketTimeout(ctx sdk.Context, prevPacket channeltypes.Packet, timeoutPacketData types.FungibleTokenPacketDataV2) error {
37+
if err := k.revertForwardedPacket(ctx, prevPacket, timeoutPacketData); err != nil {
38+
return err
39+
}
40+
41+
// the timeout is converted into an error acknowledgement in order to propagate the failed packet forwarding
42+
// back to the original sender
43+
forwardAck := channeltypes.NewErrorAcknowledgement(errors.New("forwarded packet timed out"))
44+
return k.acknowledgeForwardedPacket(ctx, prevPacket, forwardAck)
45+
}
46+
47+
// acknowledgeForwardedPacket writes the async acknowledgement for packet
48+
func (k Keeper) acknowledgeForwardedPacket(ctx sdk.Context, packet channeltypes.Packet, ack channeltypes.Acknowledgement) error {
49+
capability, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(packet.DestinationPort, packet.DestinationChannel))
50+
if !ok {
51+
return errorsmod.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
52+
}
53+
54+
return k.ics4Wrapper.WriteAcknowledgement(ctx, capability, packet, ack)
55+
}
56+
57+
// revertForwardedPacket reverts the logic of receive packet that occurs in the middle chains during a packet forwarding.
58+
// If the packet fails to be forwarded all the way to the final destination, the state changes on this chain must be reverted
59+
// before sending back the error acknowledgement to ensure atomic packet forwarding.
60+
func (k Keeper) revertForwardedPacket(ctx sdk.Context, prevPacket channeltypes.Packet, failedPacketData types.FungibleTokenPacketDataV2) error {
61+
/*
62+
Recall that RecvPacket handles an incoming packet depending on the denom of the received funds:
63+
1. If the funds are native, then the amount is sent to the receiver from the escrow.
64+
2. If the funds are foreign, then a voucher token is minted.
65+
We revert it in this function by:
66+
1. Sending funds back to escrow if the funds are native.
67+
2. Burning voucher tokens if the funds are foreign
68+
*/
69+
70+
forwardingAddr := types.GetForwardAddress(prevPacket.DestinationPort, prevPacket.DestinationChannel)
71+
escrow := types.GetEscrowAddress(prevPacket.DestinationPort, prevPacket.DestinationChannel)
72+
73+
// we can iterate over the received tokens of prevPacket by iterating over the sent tokens of failedPacketData
74+
for _, token := range failedPacketData.Tokens {
75+
// parse the transfer amount
76+
transferAmount, ok := sdkmath.NewIntFromString(token.Amount)
77+
if !ok {
78+
return errorsmod.Wrapf(types.ErrInvalidAmount, "unable to parse transfer amount (%s) into math.Int", transferAmount)
79+
}
80+
coin := sdk.NewCoin(token.Denom.IBCDenom(), transferAmount)
81+
82+
// check if the token we received originated on the sender
83+
// given that the packet is being reversed, we check the DestinationChannel and DestinationPort
84+
// of the prevPacket to see if a hop was added to the trace during the receive step
85+
if token.Denom.SenderChainIsSource(prevPacket.DestinationPort, prevPacket.DestinationChannel) {
86+
// then send it back to the escrow address
87+
if err := k.escrowCoin(ctx, forwardingAddr, escrow, coin); err != nil {
88+
return err
89+
}
90+
91+
continue
92+
}
93+
94+
if err := k.burnCoin(ctx, forwardingAddr, coin); err != nil {
95+
return err
96+
}
97+
}
98+
return nil
99+
}

modules/apps/transfer/keeper/relay.go

Lines changed: 43 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package keeper
22

33
import (
4-
"errors"
54
"fmt"
65
"strings"
76

@@ -320,67 +319,46 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data t
320319
// acknowledgement was a success then nothing occurs. If the acknowledgement failed,
321320
// then the sender is refunded their tokens using the refundPacketToken function.
322321
func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketDataV2, ack channeltypes.Acknowledgement) error {
323-
prevPacket, found := k.GetForwardedPacket(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence)
324-
if found {
325-
channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(packet.SourcePort, packet.SourceChannel))
326-
if !ok {
327-
return errorsmod.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
328-
}
322+
prevPacket, isForwarded := k.GetForwardedPacket(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence)
329323

330-
switch ack.Response.(type) {
331-
case *channeltypes.Acknowledgement_Result:
332-
// the acknowledgement succeeded on the receiving chain so
333-
// we write the asynchronous acknowledgement for the sender
334-
// of the previous packet.
335-
fungibleTokenPacketAcknowledgement := channeltypes.NewResultAcknowledgement([]byte("forwarded packet succeeded"))
336-
return k.ics4Wrapper.WriteAcknowledgement(ctx, channelCap, prevPacket, fungibleTokenPacketAcknowledgement)
337-
case *channeltypes.Acknowledgement_Error:
338-
// the forwarded packet has failed, thus the funds have been refunded to the forwarding address.
339-
// we must revert the changes that came from successfully receiving the tokens on our chain
340-
// before propogating the error acknowledgement back to original sender chain
341-
if err := k.revertInFlightChanges(ctx, packet, prevPacket, data); err != nil {
342-
return err
343-
}
324+
switch ack.Response.(type) {
325+
case *channeltypes.Acknowledgement_Result:
326+
if isForwarded {
327+
return k.ackForwardPacketSuccess(ctx, prevPacket)
328+
}
344329

345-
fungibleTokenPacketAcknowledgement := channeltypes.NewErrorAcknowledgement(errors.New("forwarded packet failed"))
346-
return k.ics4Wrapper.WriteAcknowledgement(ctx, channelCap, prevPacket, fungibleTokenPacketAcknowledgement)
347-
default:
348-
return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected one of [%T, %T], got %T", channeltypes.Acknowledgement_Result{}, channeltypes.Acknowledgement_Error{}, ack.Response)
330+
// the acknowledgement succeeded on the receiving chain so nothing
331+
// needs to be executed and no error needs to be returned
332+
return nil
333+
case *channeltypes.Acknowledgement_Error:
334+
// We refund the tokens from the escrow address to the sender
335+
if err := k.refundPacketTokens(ctx, packet, data); err != nil {
336+
return err
349337
}
350-
} else {
351-
switch ack.Response.(type) {
352-
case *channeltypes.Acknowledgement_Result:
353-
// the acknowledgement succeeded on the receiving chain so nothing
354-
// needs to be executed and no error needs to be returned
355-
return nil
356-
case *channeltypes.Acknowledgement_Error:
357-
return k.refundPacketTokens(ctx, packet, data)
358-
default:
359-
return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected one of [%T, %T], got %T", channeltypes.Acknowledgement_Result{}, channeltypes.Acknowledgement_Error{}, ack.Response)
338+
if isForwarded {
339+
return k.ackForwardPacketError(ctx, prevPacket, data)
360340
}
341+
342+
return nil
343+
default:
344+
return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected one of [%T, %T], got %T", channeltypes.Acknowledgement_Result{}, channeltypes.Acknowledgement_Error{}, ack.Response)
361345
}
362346
}
363347

364348
// OnTimeoutPacket either reverts the state changes executed in receive and send
365349
// packet if the chain acted as a middle hop on a multihop transfer; or refunds
366350
// the sender if the original packet sent was never received and has been timed out.
367351
func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, data types.FungibleTokenPacketDataV2) error {
368-
prevPacket, found := k.GetForwardedPacket(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence)
369-
if found {
370-
channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(packet.SourcePort, packet.SourceChannel))
371-
if !ok {
372-
return errorsmod.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
373-
}
374-
375-
if err := k.revertInFlightChanges(ctx, packet, prevPacket, data); err != nil {
376-
return err
377-
}
352+
if err := k.refundPacketTokens(ctx, packet, data); err != nil {
353+
return err
354+
}
378355

379-
fungibleTokenPacketAcknowledgement := channeltypes.NewErrorAcknowledgement(fmt.Errorf("forwarded packet timed out"))
380-
return k.ics4Wrapper.WriteAcknowledgement(ctx, channelCap, prevPacket, fungibleTokenPacketAcknowledgement)
356+
prevPacket, isForwarded := k.GetForwardedPacket(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence)
357+
if isForwarded {
358+
return k.ackForwardPacketTimeout(ctx, prevPacket, data)
381359
}
382360

383-
return k.refundPacketTokens(ctx, packet, data)
361+
return nil
384362
}
385363

386364
// refundPacketTokens will unescrow and send back the tokens back to sender
@@ -429,63 +407,6 @@ func (k Keeper) refundPacketTokens(ctx sdk.Context, packet channeltypes.Packet,
429407
return nil
430408
}
431409

432-
// revertInFlightChanges reverts the logic of receive packet and send packet
433-
// that occurs in the middle chains during a packet forwarding. If an error
434-
// occurs further down the line, the state changes on this chain must be
435-
// reverted before sending back the error acknowledgement to ensure atomic packet forwarding.
436-
func (k Keeper) revertInFlightChanges(ctx sdk.Context, sentPacket channeltypes.Packet, receivedPacket channeltypes.Packet, sentPacketData types.FungibleTokenPacketDataV2) error {
437-
forwardEscrow := types.GetEscrowAddress(sentPacket.SourcePort, sentPacket.SourceChannel)
438-
reverseEscrow := types.GetEscrowAddress(receivedPacket.DestinationPort, receivedPacket.DestinationChannel)
439-
440-
// the token on our chain is the token in the sentPacket
441-
for _, token := range sentPacketData.Tokens {
442-
// parse the transfer amount
443-
transferAmount, ok := sdkmath.NewIntFromString(token.Amount)
444-
if !ok {
445-
return errorsmod.Wrapf(types.ErrInvalidAmount, "unable to parse transfer amount (%s) into math.Int", transferAmount)
446-
}
447-
coin := sdk.NewCoin(token.Denom.IBCDenom(), transferAmount)
448-
449-
// check if the packet we sent out was sending as source or not
450-
// if it is source, then we escrowed the outgoing tokens
451-
if token.Denom.SenderChainIsSource(sentPacket.SourcePort, sentPacket.SourceChannel) {
452-
// check if the packet we received was a source token for our chain
453-
// check if here should be ReceiverChainIsSource
454-
if token.Denom.SenderChainIsSource(receivedPacket.DestinationPort, receivedPacket.DestinationChannel) {
455-
// receive sent tokens from the received escrow to the forward escrow account
456-
// so we must send the tokens back from the forward escrow to the original received escrow account
457-
return k.unescrowCoin(ctx, forwardEscrow, reverseEscrow, coin)
458-
}
459-
460-
// receive minted vouchers and sent to the forward escrow account
461-
// so we must remove the vouchers from the forward escrow account and burn them
462-
if err := k.bankKeeper.BurnCoins(
463-
ctx, types.ModuleName, sdk.NewCoins(coin),
464-
); err != nil {
465-
return err
466-
}
467-
} else { //nolint:gocritic
468-
// in this case we burned the vouchers of the outgoing packets
469-
// check if the packet we received was a source token for our chain
470-
// in this case, the tokens were unescrowed from the reverse escrow account
471-
if token.Denom.SenderChainIsSource(receivedPacket.DestinationPort, receivedPacket.DestinationChannel) {
472-
// in this case we must mint the burned vouchers and send them back to the escrow account
473-
if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil {
474-
return err
475-
}
476-
477-
if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, reverseEscrow, sdk.NewCoins(coin)); err != nil {
478-
panic(fmt.Errorf("unable to send coins from module to account despite previously minting coins to module account: %v", err))
479-
}
480-
}
481-
482-
// if it wasn't a source token on receive, then we simply had minted vouchers and burned them in the receive.
483-
// So no state changes were made, and thus no reversion is necessary
484-
}
485-
}
486-
return nil
487-
}
488-
489410
// escrowCoin will send the given coin from the provided sender to the escrow address. It will also
490411
// update the total escrowed amount by adding the escrowed coin's amount to the current total escrow.
491412
func (k Keeper) escrowCoin(ctx sdk.Context, sender, escrowAddress sdk.AccAddress, coin sdk.Coin) error {
@@ -572,3 +493,20 @@ func createPacketDataBytesFromVersion(appVersion, sender, receiver, memo string,
572493

573494
return packetDataBytes
574495
}
496+
497+
// burnCoin sends coins from the account to the transfer module account and then burn them.
498+
// We do this because bankKeeper.BurnCoins only works with a module account in SDK v0.50,
499+
// the next version of the SDK will allow burning coins from any account.
500+
// TODO: remove this function once we switch forwarding address to a module account (#6561)
501+
func (k Keeper) burnCoin(ctx sdk.Context, account sdk.AccAddress, coin sdk.Coin) error {
502+
coins := sdk.NewCoins(coin)
503+
if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, account, types.ModuleName, coins); err != nil {
504+
return err
505+
}
506+
507+
if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, coins); err != nil {
508+
return err
509+
}
510+
511+
return nil
512+
}

modules/apps/transfer/keeper/relay_forwarding_test.go

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
sdk "github.com/cosmos/cosmos-sdk/types"
99

10+
"github.com/cosmos/ibc-go/v8/modules/apps/transfer/internal"
1011
"github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
1112
clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
1213
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
@@ -349,9 +350,6 @@ func (suite *KeeperTestSuite) TestSimplifiedHappyPathForwarding() {
349350
}
350351

351352
// This test replicates the Acknowledgement Failure Scenario 5
352-
// Currently seems like the middle hop is not reverting state changes when an error occurs.
353-
// In turn the final hop properly reverts changes. There may be an error in the way async ack are managed
354-
// or in the way i'm trying to activate the OnAck function.
355353
func (suite *KeeperTestSuite) TestAcknowledgementFailureScenario5Forwarding() {
356354
amount := sdkmath.NewInt(100)
357355
/*
@@ -472,7 +470,7 @@ func (suite *KeeperTestSuite) TestAcknowledgementFailureScenario5Forwarding() {
472470

473471
// Now we want to trigger C -> B -> A
474472
// The coin we want to send out is exactly the one we received on C
475-
// coin = sdk.NewCoin(denomTraceBC.IBCDenom(), amount)
473+
coin = sdk.NewCoin(denomABC.IBCDenom(), amount)
476474

477475
sender = suite.chainC.SenderAccounts[0].SenderAccount
478476
receiver = suite.chainA.SenderAccounts[0].SenderAccount // Receiver is the A chain account
@@ -554,33 +552,23 @@ func (suite *KeeperTestSuite) TestAcknowledgementFailureScenario5Forwarding() {
554552
// NOW WE HAVE TO SEND ACK TO B, PROPAGTE ACK TO C, CHECK FINAL RESULTS
555553

556554
// Reconstruct packet data
557-
denom := types.ExtractDenomFromPath(denomAB.Path())
558-
data := types.NewFungibleTokenPacketDataV2(
559-
[]types.Token{
560-
{
561-
Denom: denom,
562-
Amount: amount.String(),
563-
},
564-
}, types.GetForwardAddress(path1.EndpointB.ChannelConfig.PortID, path1.EndpointB.ChannelID).String(), suite.chainA.SenderAccounts[0].SenderAccount.GetAddress().String(), "", nil)
565-
packetRecv := channeltypes.NewPacket(data.GetBytes(), 3, path1.EndpointB.ChannelConfig.PortID, path1.EndpointB.ChannelID, path1.EndpointA.ChannelConfig.PortID, path1.EndpointA.ChannelID, clienttypes.NewHeight(1, 100), 0)
555+
data, err := internal.UnmarshalPacketData(packet.Data, types.V2)
556+
suite.Require().NoError(err)
566557

567558
err = path1.EndpointB.UpdateClient()
568559
suite.Require().NoError(err)
569560
ack := channeltypes.NewErrorAcknowledgement(fmt.Errorf("failed packet transfer"))
570561

571562
// err = path1.EndpointA.AcknowledgePacket(packetRecv, ack.Acknowledgement())
572-
err = suite.chainB.GetSimApp().TransferKeeper.OnAcknowledgementPacket(suite.chainB.GetContext(), packetRecv, data, ack)
563+
err = suite.chainB.GetSimApp().TransferKeeper.OnAcknowledgementPacket(suite.chainB.GetContext(), packet, data, ack)
573564
suite.Require().NoError(err)
574565

575566
// Check that Escrow B has been refunded amount
576-
// NOTE This is failing. The revertInFlightsChanges sohuld mint back voucher to chainBescrow
577-
// but this is not happening. It may be a problem related with how we're writing async acks.
578-
//
579567
coin = sdk.NewCoin(denomAB.IBCDenom(), amount)
580568
totalEscrowChainB = suite.chainB.GetSimApp().TransferKeeper.GetTotalEscrowForDenom(suite.chainB.GetContext(), coin.GetDenom())
581569
suite.Require().Equal(sdkmath.NewInt(100), totalEscrowChainB.Amount)
582570

583-
denom = types.ExtractDenomFromPath(denomABC.Path())
571+
denom := types.ExtractDenomFromPath(denomABC.Path())
584572
data = types.NewFungibleTokenPacketDataV2(
585573
[]types.Token{
586574
{

0 commit comments

Comments
 (0)