-
Notifications
You must be signed in to change notification settings - Fork 648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: remove SendTransfer, require IBC transfers to be initiated with MsgTransfer #2446
Changes from 7 commits
4db8313
d2da1c9
e87087b
7549663
05098fb
c7bb641
1f03958
a11a447
a3d80c7
1b41b9b
9351467
dabfc5c
9c4d411
a878376
cca0b61
9e03e65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,7 @@ import ( | |
coretypes "github.com/cosmos/ibc-go/v6/modules/core/types" | ||
) | ||
|
||
// SendTransfer handles transfer sending logic. There are 2 possible cases: | ||
// 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 | ||
|
@@ -48,31 +48,6 @@ import ( | |
// 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' | ||
// | ||
// Note: An IBC Transfer must be initiated using a MsgTransfer via the Transfer rpc handler | ||
func (k Keeper) SendTransfer( | ||
ctx sdk.Context, | ||
sourcePort, | ||
sourceChannel string, | ||
token sdk.Coin, | ||
sender sdk.AccAddress, | ||
receiver string, | ||
timeoutHeight clienttypes.Height, | ||
timeoutTimestamp uint64, | ||
) error { | ||
return k.sendTransfer( | ||
ctx, | ||
sourcePort, | ||
sourceChannel, | ||
token, | ||
sender, | ||
receiver, | ||
timeoutHeight, | ||
timeoutTimestamp, | ||
) | ||
} | ||
|
||
// sendTransfer handles transfer sending logic. | ||
func (k Keeper) sendTransfer( | ||
ctx sdk.Context, | ||
sourcePort, | ||
|
@@ -83,14 +58,6 @@ func (k Keeper) sendTransfer( | |
timeoutHeight clienttypes.Height, | ||
timeoutTimestamp uint64, | ||
) error { | ||
if !k.GetSendEnabled(ctx) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason to move these checks to the message server? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the message server was added after send enabled, which is why it ended up within "SendTransfer" The reasoning for why I moved it is because I view it as auxiliary to the transfer logic. It isn't in the IBC specification for ICS20 I visualize the code like this:
I think this should be true for most of our handlers:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general I think we should avoid overloading functions. Basic enablement checks make sense to go directly in the entrypoint (msg_server.go), while functions nested deeper in the handler can focus on ICS logic entirely There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree++ Definitely a good way to reason about things! |
||
return types.ErrSendDisabled | ||
} | ||
|
||
if k.bankKeeper.BlockedAddr(sender) { | ||
return sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to send funds", sender) | ||
} | ||
|
||
sourceChannelEnd, found := k.channelKeeper.GetChannel(ctx, sourcePort, sourceChannel) | ||
if !found { | ||
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", sourcePort, sourceChannel) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,6 @@ import ( | |
"github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" | ||
clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" | ||
channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" | ||
host "github.com/cosmos/ibc-go/v6/modules/core/24-host" | ||
ibctesting "github.com/cosmos/ibc-go/v6/testing" | ||
"github.com/cosmos/ibc-go/v6/testing/simapp" | ||
) | ||
|
@@ -18,137 +17,132 @@ import ( | |
// chainA and coin that orignate on chainB | ||
func (suite *KeeperTestSuite) TestSendTransfer() { | ||
var ( | ||
amount sdk.Coin | ||
path *ibctesting.Path | ||
sender sdk.AccAddress | ||
err error | ||
coin sdk.Coin | ||
path *ibctesting.Path | ||
sender sdk.AccAddress | ||
timeoutHeight clienttypes.Height | ||
) | ||
|
||
testCases := []struct { | ||
msg string | ||
malleate func() | ||
sendFromSource bool | ||
expPass bool | ||
name string | ||
malleate func() | ||
expPass bool | ||
}{ | ||
{ | ||
"successful transfer from source chain", | ||
func() { | ||
suite.coordinator.CreateTransferChannels(path) | ||
amount = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) | ||
}, true, true, | ||
colin-axner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"successful transfer with native token", | ||
func() {}, true, | ||
}, | ||
{ | ||
"successful transfer with coin from counterparty chain", | ||
"successful transfer with IBC token", | ||
func() { | ||
// send coin from chainA back to chainB | ||
suite.coordinator.CreateTransferChannels(path) | ||
amount = types.GetTransferCoin(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, sdk.DefaultBondDenom, sdk.NewInt(100)) | ||
}, false, true, | ||
// send IBC token back to chainB | ||
coin = types.GetTransferCoin(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, coin.Denom, coin.Amount) | ||
}, true, | ||
}, | ||
{ | ||
"source channel not found", | ||
func() { | ||
// channel references wrong ID | ||
suite.coordinator.CreateTransferChannels(path) | ||
path.EndpointA.ChannelID = ibctesting.InvalidID | ||
amount = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) | ||
}, true, false, | ||
}, false, | ||
}, | ||
{ | ||
"next seq send not found", | ||
func() { | ||
path.EndpointA.ChannelID = "channel-0" | ||
path.EndpointB.ChannelID = "channel-0" | ||
path.EndpointA.ChannelID = "channel-100" | ||
path.EndpointB.ChannelID = "channel-100" | ||
// manually create channel so next seq send is never set | ||
suite.chainA.App.GetIBCKeeper().ChannelKeeper.SetChannel( | ||
suite.chainA.GetContext(), | ||
path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, | ||
channeltypes.NewChannel(channeltypes.OPEN, channeltypes.ORDERED, channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID), []string{path.EndpointA.ConnectionID}, ibctesting.DefaultChannelVersion), | ||
) | ||
suite.chainA.CreateChannelCapability(suite.chainA.GetSimApp().ScopedIBCMockKeeper, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) | ||
amount = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) | ||
}, true, false, | ||
}, false, | ||
}, | ||
{ | ||
"transfer failed - sender account is blocked", | ||
func() { | ||
suite.coordinator.CreateTransferChannels(path) | ||
amount = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) | ||
sender = suite.chainA.GetSimApp().AccountKeeper.GetModuleAddress(types.ModuleName) | ||
}, true, false, | ||
}, false, | ||
}, | ||
// createOutgoingPacket tests | ||
// - source chain | ||
{ | ||
"send coin failed", | ||
func() { | ||
suite.coordinator.CreateTransferChannels(path) | ||
amount = sdk.NewCoin("randomdenom", sdk.NewInt(100)) | ||
}, true, false, | ||
coin = sdk.NewCoin("randomdenom", sdk.NewInt(100)) | ||
}, false, | ||
}, | ||
// - receiving chain | ||
{ | ||
"send from module account failed", | ||
"failed to parse coin denom", | ||
func() { | ||
suite.coordinator.CreateTransferChannels(path) | ||
amount = types.GetTransferCoin(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, " randomdenom", sdk.NewInt(100)) | ||
}, false, false, | ||
coin = types.GetTransferCoin(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, "randomdenom", coin.Amount) | ||
}, false, | ||
}, | ||
{ | ||
"send from module account failed, insufficient balance", | ||
func() { | ||
coin = types.GetTransferCoin(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, coin.Denom, coin.Amount.Add(sdk.NewInt(1))) | ||
}, false, | ||
}, | ||
{ | ||
"channel capability not found", | ||
func() { | ||
suite.coordinator.CreateTransferChannels(path) | ||
cap := suite.chainA.GetChannelCapability(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) | ||
|
||
// Release channel capability | ||
suite.chainA.GetSimApp().ScopedTransferKeeper.ReleaseCapability(suite.chainA.GetContext(), cap) | ||
amount = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) | ||
}, true, false, | ||
}, false, | ||
}, | ||
{ | ||
"SendPacket fails, timeout height and timeout timestamp are zero", | ||
func() { | ||
timeoutHeight = clienttypes.ZeroHeight() | ||
}, false, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
tc := tc | ||
|
||
suite.Run(fmt.Sprintf("Case %s", tc.msg), func() { | ||
suite.Run(tc.name, func() { | ||
suite.SetupTest() // reset | ||
|
||
path = NewTransferPath(suite.chainA, suite.chainB) | ||
suite.coordinator.SetupConnections(path) | ||
sender = suite.chainA.SenderAccount.GetAddress() | ||
suite.coordinator.Setup(path) | ||
|
||
tc.malleate() | ||
coin = sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) | ||
sender = suite.chainA.SenderAccount.GetAddress() | ||
timeoutHeight = suite.chainB.GetTimeoutHeight() | ||
|
||
if !tc.sendFromSource { | ||
// send coin from chainB to chainA | ||
coinFromBToA := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100)) | ||
transferMsg := types.NewMsgTransfer(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, coinFromBToA, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), clienttypes.NewHeight(1, 110), 0) | ||
_, err = suite.chainB.SendMsgs(transferMsg) | ||
suite.Require().NoError(err) // message committed | ||
// create IBC token on chainA | ||
colin-axner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
transferMsg := types.NewMsgTransfer(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, coin, suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), suite.chainA.GetTimeoutHeight(), 0) | ||
result, err := suite.chainB.SendMsgs(transferMsg) | ||
suite.Require().NoError(err) // message committed | ||
|
||
// receive coin on chainA from chainB | ||
fungibleTokenPacket := types.NewFungibleTokenPacketData(coinFromBToA.Denom, coinFromBToA.Amount.String(), suite.chainB.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String()) | ||
packet := channeltypes.NewPacket(fungibleTokenPacket.GetBytes(), 1, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, clienttypes.NewHeight(1, 110), 0) | ||
packet, err := ibctesting.ParsePacketFromEvents(result.GetEvents()) | ||
suite.Require().NoError(err) | ||
|
||
// get proof of packet commitment from chainB | ||
err = path.EndpointA.UpdateClient() | ||
suite.Require().NoError(err) | ||
packetKey := host.PacketCommitmentKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) | ||
proof, proofHeight := path.EndpointB.QueryProof(packetKey) | ||
err = path.RelayPacket(packet) | ||
suite.Require().NoError(err) | ||
|
||
recvMsg := channeltypes.NewMsgRecvPacket(packet, proof, proofHeight, suite.chainA.SenderAccount.GetAddress().String()) | ||
_, err = suite.chainA.SendMsgs(recvMsg) | ||
suite.Require().NoError(err) // message committed | ||
} | ||
tc.malleate() | ||
|
||
err = suite.chainA.GetSimApp().TransferKeeper.SendTransfer( | ||
suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, amount, | ||
sender, suite.chainB.SenderAccount.GetAddress().String(), suite.chainB.GetTimeoutHeight(), 0, | ||
msg := types.NewMsgTransfer( | ||
path.EndpointA.ChannelConfig.PortID, | ||
path.EndpointA.ChannelID, | ||
coin, sender.String(), suite.chainB.SenderAccount.GetAddress().String(), | ||
timeoutHeight, 0, // only use timeout height | ||
) | ||
|
||
res, err := suite.chainA.GetSimApp().TransferKeeper.Transfer(sdk.WrapSDKContext(suite.chainA.GetContext()), msg) | ||
|
||
if tc.expPass { | ||
suite.Require().NoError(err) | ||
suite.Require().NotNil(res) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we should also add here checks for changes that we expect to have happened in case of success (e.g. the expected amount is escrowed in the escrow account)? We check for this already in the e2e tests but maybe it's good to add here for redundancy? Same comment maybe for the failure cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should. But maybe we should do in a followup issue so as not to increase the scope of this one too much? |
||
} else { | ||
suite.Require().Error(err) | ||
suite.Require().Nil(res) | ||
} | ||
}) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like using suite.Run because it adds the test case name to the output if it fails