Skip to content
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

Merged
merged 16 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions modules/apps/transfer/keeper/mbt_relay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,15 +338,17 @@ func (suite *KeeperTestSuite) TestModelBasedRelay() {
if !ok {
panic("MBT failed to parse amount from string")
}
err = suite.chainB.GetSimApp().TransferKeeper.SendTransfer(
suite.chainB.GetContext(),
msg := types.NewMsgTransfer(
tc.packet.SourcePort,
tc.packet.SourceChannel,
sdk.NewCoin(denom, amount),
sender,
sender.String(),
tc.packet.Data.Receiver,
clienttypes.NewHeight(1, 110),
0)
suite.chainB.GetTimeoutHeight(), 0, // only use timeout height
)

_, err = suite.chainB.GetSimApp().TransferKeeper.Transfer(sdk.WrapSDKContext(suite.chainB.GetContext()), msg)

}
case "OnRecvPacket":
err = suite.chainB.GetSimApp().TransferKeeper.OnRecvPacket(suite.chainB.GetContext(), packet, tc.packet.Data)
Expand Down
9 changes: 9 additions & 0 deletions modules/apps/transfer/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/cosmos/ibc-go/v6/modules/apps/transfer/types"
)
Expand All @@ -14,11 +15,19 @@ var _ types.MsgServer = Keeper{}
func (k Keeper) Transfer(goCtx context.Context, msg *types.MsgTransfer) (*types.MsgTransferResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

if !k.GetSendEnabled(ctx) {
return nil, types.ErrSendDisabled
}

sender, err := sdk.AccAddressFromBech32(msg.Sender)
if err != nil {
return nil, err
}

if k.bankKeeper.BlockedAddr(sender) {
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to send funds", sender)
}

if err := k.sendTransfer(
ctx, msg.SourcePort, msg.SourceChannel, msg.Token, sender, msg.Receiver, msg.TimeoutHeight, msg.TimeoutTimestamp,
); err != nil {
Expand Down
47 changes: 28 additions & 19 deletions modules/apps/transfer/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ func (suite *KeeperTestSuite) TestMsgTransfer() {
func() {},
true,
},
{
"transfers disabled",
colin-axner marked this conversation as resolved.
Show resolved Hide resolved
func() {

},
true,
},
{
"invalid sender",
func() {
Expand All @@ -43,29 +50,31 @@ func (suite *KeeperTestSuite) TestMsgTransfer() {
}

for _, tc := range testCases {
suite.SetupTest()
suite.Run(tc.name, func() {
Copy link
Contributor Author

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

suite.SetupTest()

path := NewTransferPath(suite.chainA, suite.chainB)
suite.coordinator.Setup(path)
path := NewTransferPath(suite.chainA, suite.chainB)
suite.coordinator.Setup(path)

coin := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))
msg = types.NewMsgTransfer(
path.EndpointA.ChannelConfig.PortID,
path.EndpointA.ChannelID,
coin, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(),
suite.chainB.GetTimeoutHeight(), 0, // only use timeout height
)
coin := sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))
msg = types.NewMsgTransfer(
path.EndpointA.ChannelConfig.PortID,
path.EndpointA.ChannelID,
coin, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(),
suite.chainB.GetTimeoutHeight(), 0, // only use timeout height
)

tc.malleate()
tc.malleate()

res, err := suite.chainA.GetSimApp().TransferKeeper.Transfer(sdk.WrapSDKContext(suite.chainA.GetContext()), msg)
res, err := suite.chainA.GetSimApp().TransferKeeper.Transfer(sdk.WrapSDKContext(suite.chainA.GetContext()), msg)

if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
} else {
suite.Require().Error(err)
suite.Require().Nil(res)
}
if tc.expPass {
suite.Require().NoError(err)
suite.Require().NotNil(res)
} else {
suite.Require().Error(err)
suite.Require().Nil(res)
}
})
}
}
35 changes: 1 addition & 34 deletions modules/apps/transfer/keeper/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -83,14 +58,6 @@ func (k Keeper) sendTransfer(
timeoutHeight clienttypes.Height,
timeoutTimestamp uint64,
) error {
if !k.GetSendEnabled(ctx) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason to move these checks to the message server?

Copy link
Contributor Author

@colin-axner colin-axner Oct 3, 2022

Choose a reason for hiding this comment

The 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:

// entry point
MsgTransfer

// basic checks/auxiliary logic
IsSendEnabeld?
IsSenderBlocked?

// perform ics20 logic
sendTransfer

I think this should be true for most of our handlers:

// single entry point, Msg...

// basic checks in msg_server.go
isEnabled?
object.ValidateBasic()

// perform logic (typically relay.go)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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)
Expand Down
106 changes: 43 additions & 63 deletions modules/apps/transfer/keeper/relay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -18,137 +17,118 @@ import (
// chainA and coin that orignate on chainB
func (suite *KeeperTestSuite) TestSendTransfer() {
var (
amount sdk.Coin
coin sdk.Coin
path *ibctesting.Path
sender sdk.AccAddress
err error
)

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",
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,
},
{
"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,
},
}

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()

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(),
suite.chainB.GetTimeoutHeight(), 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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
}
})
}
Expand Down