Skip to content

Commit 0461d17

Browse files
authored
Merge pull request #2267 from CosmWasm/co/ibc-callbacks-funds
Add `Transfer` special handling to IBC destination callback if it's a transfer message
2 parents 6ff32cc + 5467f1e commit 0461d17

File tree

5 files changed

+177
-36
lines changed

5 files changed

+177
-36
lines changed

app/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ func NewWasmApp(
622622
)
623623

624624
// Create fee enabled wasm ibc Stack
625-
wasmStackIBCHandler := wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, app.IBCKeeper.ChannelKeeper)
625+
wasmStackIBCHandler := wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, app.TransferKeeper, app.IBCKeeper.ChannelKeeper)
626626

627627
// Create Interchain Accounts Stack
628628
// SendPacket, since it is originating from the application to core IBC:

tests/e2e/ibc_callbacks_test.go

Lines changed: 123 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ import (
2222
"github.com/CosmWasm/wasmd/x/wasm/types"
2323
)
2424

25+
type transferExecMsg struct {
26+
ToAddress string `json:"to_address"`
27+
ChannelID string `json:"channel_id"`
28+
TimeoutSeconds uint32 `json:"timeout_seconds"`
29+
}
30+
31+
// executeMsg is the ibc-callbacks contract's execute msg
32+
type executeMsg struct {
33+
Transfer *transferExecMsg `json:"transfer"`
34+
}
35+
type queryMsg struct {
36+
CallbackStats struct{} `json:"callback_stats"`
37+
}
38+
type queryResp struct {
39+
IBCAckCallbacks []wasmvmtypes.IBCPacketAckMsg `json:"ibc_ack_callbacks"`
40+
IBCTimeoutCallbacks []wasmvmtypes.IBCPacketTimeoutMsg `json:"ibc_timeout_callbacks"`
41+
IBCDestinationCallbacks []wasmvmtypes.IBCDestinationCallbackMsg `json:"ibc_destination_callbacks"`
42+
}
43+
2544
func TestIBCCallbacks(t *testing.T) {
2645
// scenario:
2746
// given two chains
@@ -35,7 +54,6 @@ func TestIBCCallbacks(t *testing.T) {
3554
chainB := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(2)))
3655

3756
actorChainA := sdk.AccAddress(chainA.SenderPrivKey.PubKey().Address())
38-
oneToken := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(1)))
3957

4058
path := wasmibctesting.NewWasmPath(chainA, chainB)
4159
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
@@ -51,47 +69,36 @@ func TestIBCCallbacks(t *testing.T) {
5169
// with an ics-20 transfer channel setup between both chains
5270
coord.Setup(&path.Path)
5371

72+
oneToken := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(1)))
73+
ibcDenom := ibctransfertypes.NewDenom(
74+
sdk.DefaultBondDenom,
75+
ibctransfertypes.NewHop(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID),
76+
).IBCDenom()
77+
ibcCoin := sdk.NewCoin(ibcDenom, sdkmath.NewInt(1))
78+
5479
// with an ibc-callbacks contract deployed on chain A
5580
codeIDonA := chainA.StoreCodeFile("./testdata/ibc_callbacks.wasm").CodeID
5681

5782
// and on chain B
5883
codeIDonB := chainB.StoreCodeFile("./testdata/ibc_callbacks.wasm").CodeID
5984

60-
type TransferExecMsg struct {
61-
ToAddress string `json:"to_address"`
62-
ChannelID string `json:"channel_id"`
63-
TimeoutSeconds uint32 `json:"timeout_seconds"`
64-
}
65-
// ExecuteMsg is the ibc-callbacks contract's execute msg
66-
type ExecuteMsg struct {
67-
Transfer *TransferExecMsg `json:"transfer"`
68-
}
69-
type QueryMsg struct {
70-
CallbackStats struct{} `json:"callback_stats"`
71-
}
72-
type QueryResp struct {
73-
IBCAckCallbacks []wasmvmtypes.IBCPacketAckMsg `json:"ibc_ack_callbacks"`
74-
IBCTimeoutCallbacks []wasmvmtypes.IBCPacketTimeoutMsg `json:"ibc_timeout_callbacks"`
75-
IBCDestinationCallbacks []wasmvmtypes.IBCDestinationCallbackMsg `json:"ibc_destination_callbacks"`
76-
}
77-
7885
specs := map[string]struct {
79-
contractMsg ExecuteMsg
86+
contractMsg executeMsg
8087
// expAck is true if the packet is relayed, false if it times out
8188
expAck bool
8289
}{
8390
"success": {
84-
contractMsg: ExecuteMsg{
85-
Transfer: &TransferExecMsg{
91+
contractMsg: executeMsg{
92+
Transfer: &transferExecMsg{
8693
ChannelID: path.EndpointA.ChannelID,
8794
TimeoutSeconds: 100,
8895
},
8996
},
9097
expAck: true,
9198
},
9299
"timeout": {
93-
contractMsg: ExecuteMsg{
94-
Transfer: &TransferExecMsg{
100+
contractMsg: executeMsg{
101+
Transfer: &transferExecMsg{
95102
ChannelID: path.EndpointA.ChannelID,
96103
TimeoutSeconds: 1,
97104
},
@@ -128,17 +135,29 @@ func TestIBCCallbacks(t *testing.T) {
128135
wasmibctesting.RelayAndAckPendingPackets(path)
129136

130137
// then the contract on chain B should receive a receive callback
131-
var response QueryResp
132-
chainB.SmartQuery(contractAddrB.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
138+
var response queryResp
139+
chainB.SmartQuery(contractAddrB.String(), queryMsg{CallbackStats: struct{}{}}, &response)
133140
assert.Empty(t, response.IBCAckCallbacks)
134141
assert.Empty(t, response.IBCTimeoutCallbacks)
135142
assert.Len(t, response.IBCDestinationCallbacks, 1)
136143

137144
// and the receive callback should contain the ack
138145
assert.Equal(t, []byte("{\"result\":\"AQ==\"}"), response.IBCDestinationCallbacks[0].Ack.Data)
146+
assert.Equal(t,
147+
wasmvmtypes.Array[wasmvmtypes.Coin]{wasmvmtypes.NewCoin(1, ibcDenom)},
148+
response.IBCDestinationCallbacks[0].Transfer.Funds,
149+
)
150+
assert.Equal(t, contractAddrA.String(), response.IBCDestinationCallbacks[0].Transfer.Receiver)
151+
152+
balances := chainB.GetWasmApp().BankKeeper.GetAllBalances(chainB.GetContext(), contractAddrB)
153+
// sanity check that the balance of the contract is correct
154+
require.Equal(t, sdk.NewCoins(
155+
sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)),
156+
ibcCoin,
157+
), balances)
139158

140159
// and the contract on chain A should receive a callback with the ack
141-
chainA.SmartQuery(contractAddrA.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
160+
chainA.SmartQuery(contractAddrA.String(), queryMsg{CallbackStats: struct{}{}}, &response)
142161
assert.Len(t, response.IBCAckCallbacks, 1)
143162
assert.Empty(t, response.IBCTimeoutCallbacks)
144163
assert.Empty(t, response.IBCDestinationCallbacks)
@@ -150,14 +169,14 @@ func TestIBCCallbacks(t *testing.T) {
150169
require.NoError(t, wasmibctesting.TimeoutPendingPackets(coord, path))
151170

152171
// then the contract on chain B should not receive anything
153-
var response QueryResp
154-
chainB.SmartQuery(contractAddrB.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
172+
var response queryResp
173+
chainB.SmartQuery(contractAddrB.String(), queryMsg{CallbackStats: struct{}{}}, &response)
155174
assert.Empty(t, response.IBCAckCallbacks)
156175
assert.Empty(t, response.IBCTimeoutCallbacks)
157176
assert.Empty(t, response.IBCDestinationCallbacks)
158177

159178
// and the contract on chain A should receive a callback with the timeout result
160-
chainA.SmartQuery(contractAddrA.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
179+
chainA.SmartQuery(contractAddrA.String(), queryMsg{CallbackStats: struct{}{}}, &response)
161180
assert.Empty(t, response.IBCAckCallbacks)
162181
assert.Len(t, response.IBCTimeoutCallbacks, 1)
163182
assert.Empty(t, response.IBCDestinationCallbacks)
@@ -166,6 +185,80 @@ func TestIBCCallbacks(t *testing.T) {
166185
}
167186
}
168187

188+
func TestIBCDestinationCallbackTransfer(t *testing.T) {
189+
// scenario:
190+
// given two chains
191+
// with an ics-20 channel established
192+
// and an ibc-callbacks contract deployed on chain A
193+
// when someone sends an ibc transfer to chain B and back to the contract on A
194+
// then the contract on A should receive a destination chain callback with correct transfer info
195+
196+
coord := wasmibctesting.NewCoordinator(t, 2)
197+
chainA := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(1)))
198+
chainB := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(2)))
199+
200+
actorChainA := sdk.AccAddress(chainA.SenderPrivKey.PubKey().Address())
201+
actorChainB := sdk.AccAddress(chainB.SenderPrivKey.PubKey().Address())
202+
203+
path := wasmibctesting.NewWasmPath(chainA, chainB)
204+
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
205+
PortID: ibctransfertypes.PortID,
206+
Version: ibctransfertypes.V1,
207+
Order: channeltypes.UNORDERED,
208+
}
209+
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
210+
PortID: ibctransfertypes.PortID,
211+
Version: ibctransfertypes.V1,
212+
Order: channeltypes.UNORDERED,
213+
}
214+
// with an ics-20 transfer channel setup between both chains
215+
coord.Setup(&path.Path)
216+
217+
oneToken := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(1)))
218+
ibcDenom := ibctransfertypes.NewDenom(
219+
sdk.DefaultBondDenom,
220+
ibctransfertypes.NewHop(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID),
221+
).IBCDenom()
222+
ibcCoin := sdk.NewCoin(ibcDenom, sdkmath.NewInt(1))
223+
224+
// with an ibc-callbacks contract deployed on chain A
225+
codeID := chainA.StoreCodeFile("./testdata/ibc_callbacks.wasm").CodeID
226+
227+
contractAddr := chainA.InstantiateContract(codeID, []byte(`{}`))
228+
require.NotEmpty(t, contractAddr)
229+
230+
// when someone sends an ibc transfer to chain B
231+
chainA.SendMsgs(
232+
ibctransfertypes.NewMsgTransfer(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID,
233+
oneToken[0], actorChainA.String(), actorChainB.String(), chainA.GetTimeoutHeight(), 0, ""),
234+
)
235+
236+
wasmibctesting.RelayAndAckPendingPackets(path)
237+
238+
// and it is transferred back to the contract on A
239+
chainB.SendMsgs(
240+
ibctransfertypes.NewMsgTransfer(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID,
241+
ibcCoin, actorChainB.String(), contractAddr.String(), chainB.GetTimeoutHeight(), 0,
242+
fmt.Sprintf(`{"dest_callback":{"address":"%s"}}`, contractAddr.String())),
243+
)
244+
wasmibctesting.RelayAndAckPendingPackets(path)
245+
246+
// then the contract on A should receive a destination chain callback with correct funds
247+
var response queryResp
248+
chainA.SmartQuery(contractAddr.String(), queryMsg{CallbackStats: struct{}{}}, &response)
249+
assert.Empty(t, response.IBCAckCallbacks)
250+
assert.Empty(t, response.IBCTimeoutCallbacks)
251+
assert.Len(t, response.IBCDestinationCallbacks, 1)
252+
assert.Equal(t, []byte("{\"result\":\"AQ==\"}"), response.IBCDestinationCallbacks[0].Ack.Data)
253+
// the denom should be reversed back correctly to the original denom
254+
assert.Equal(t,
255+
wasmvmtypes.Array[wasmvmtypes.Coin]{wasmvmtypes.NewCoin(1, sdk.DefaultBondDenom)},
256+
response.IBCDestinationCallbacks[0].Transfer.Funds,
257+
)
258+
assert.Equal(t, contractAddr.String(), response.IBCDestinationCallbacks[0].Transfer.Receiver)
259+
assert.Equal(t, actorChainB.String(), response.IBCDestinationCallbacks[0].Transfer.Sender)
260+
}
261+
169262
func TestIBCCallbacksWithoutEntrypoints(t *testing.T) {
170263
// scenario:
171264
// given two chains

tests/e2e/testdata/ibc_callbacks.wasm

77.5 KB
Binary file not shown.

x/wasm/ibc.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"math"
55

66
wasmvmtypes "github.com/CosmWasm/wasmvm/v3/types"
7+
transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types"
78
clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types"
89
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
910
porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types"
@@ -34,11 +35,12 @@ type appVersionGetter interface {
3435
type IBCHandler struct {
3536
keeper types.IBCContractKeeper
3637
channelKeeper types.ChannelKeeper
38+
transferKeeper types.ICS20TransferPortSource
3739
appVersionGetter appVersionGetter
3840
}
3941

40-
func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, vg appVersionGetter) IBCHandler {
41-
return IBCHandler{keeper: k, channelKeeper: ck, appVersionGetter: vg}
42+
func NewIBCHandler(k types.IBCContractKeeper, ck types.ChannelKeeper, tk types.ICS20TransferPortSource, vg appVersionGetter) IBCHandler {
43+
return IBCHandler{keeper: k, channelKeeper: ck, transferKeeper: tk, appVersionGetter: vg}
4244
}
4345

4446
// OnChanOpenInit implements the IBCModule interface
@@ -412,9 +414,55 @@ func (i IBCHandler) IBCReceivePacketCallback(
412414
return err
413415
}
414416

417+
var transfer *wasmvmtypes.IBCTransferCallback
418+
419+
// detect successful IBC transfer, meaning:
420+
// 1. it was sent to the transfer module
421+
// 2. the acknowledgement was successful
422+
if packet.GetDestPort() == i.transferKeeper.GetPort(cachedCtx) && ack.Success() {
423+
424+
transferData, err := transfertypes.UnmarshalPacketData(packet.GetData(), version, "")
425+
if err != nil {
426+
return errorsmod.Wrap(err, "unmarshal transfer packet data")
427+
}
428+
429+
// just making sure we have a valid address
430+
receiverAddr, err := sdk.AccAddressFromBech32(transferData.Receiver)
431+
if err != nil {
432+
return err
433+
}
434+
435+
// For a more in-depth explanation of the logic here, see the transfer module implementation:
436+
// https://github.com/cosmos/ibc-go/blob/a6217ab02a4d57c52a938eeaff8aeb383e523d12/modules/apps/transfer/keeper/relay.go#L147-L175
437+
// and the sequence diagram in the ICS20 spec:
438+
// https://github.com/cosmos/ibc/blob/9be3630/spec/app/ics-020-fungible-token-transfer/README.md#data-structures
439+
if transferData.Token.Denom.HasPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) {
440+
// This is a denom coming from this chain, being sent back again, so we remove the prefix.
441+
// See for example the "A -> C" step in the sequence diagram.
442+
transferData.Token.Denom.Trace = transferData.Token.Denom.Trace[1:]
443+
} else {
444+
// prefixing happens on the receiving end, so we need to do that here
445+
// See for example the "C -> A" step in the sequence diagram.
446+
trace := []transfertypes.Hop{transfertypes.NewHop(packet.GetDestPort(), packet.GetDestChannel())}
447+
transferData.Token.Denom.Trace = append(trace, transferData.Token.Denom.Trace...)
448+
}
449+
450+
transfer = &wasmvmtypes.IBCTransferCallback{
451+
Receiver: receiverAddr.String(),
452+
Sender: transferData.Sender,
453+
Funds: wasmvmtypes.Array[wasmvmtypes.Coin]{
454+
{
455+
Denom: transferData.Token.GetDenom().IBCDenom(),
456+
Amount: transferData.Token.GetAmount(),
457+
},
458+
},
459+
}
460+
}
461+
415462
msg := wasmvmtypes.IBCDestinationCallbackMsg{
416-
Ack: wasmvmtypes.IBCAcknowledgement{Data: ack.Acknowledgement()},
417-
Packet: newIBCPacket(packet),
463+
Ack: wasmvmtypes.IBCAcknowledgement{Data: ack.Acknowledgement()},
464+
Packet: newIBCPacket(packet),
465+
Transfer: transfer,
418466
}
419467

420468
err = i.keeper.IBCDestinationCallback(cachedCtx, contractAddr, msg)

x/wasm/ibc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func TestOnRecvPacket(t *testing.T) {
110110
},
111111
}
112112
channelVersion := ""
113-
h := NewIBCHandler(&mock, nil, nil)
113+
h := NewIBCHandler(&mock, nil, nil, nil)
114114
em := &sdk.EventManager{}
115115
ctx := sdk.Context{}.WithEventManager(em)
116116
if spec.expPanic {

0 commit comments

Comments
 (0)