Skip to content

Commit 5b55926

Browse files
committed
Implement ibc callback funds field
1 parent 6ff32cc commit 5b55926

File tree

5 files changed

+166
-34
lines changed

5 files changed

+166
-34
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: 120 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,28 @@ 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].Funds,
149+
)
150+
151+
balances := chainB.GetWasmApp().BankKeeper.GetAllBalances(chainB.GetContext(), contractAddrB)
152+
// sanity check that the balance of the contract is correct
153+
require.Equal(t, sdk.NewCoins(
154+
sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)),
155+
ibcCoin,
156+
), balances)
139157

140158
// and the contract on chain A should receive a callback with the ack
141-
chainA.SmartQuery(contractAddrA.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
159+
chainA.SmartQuery(contractAddrA.String(), queryMsg{CallbackStats: struct{}{}}, &response)
142160
assert.Len(t, response.IBCAckCallbacks, 1)
143161
assert.Empty(t, response.IBCTimeoutCallbacks)
144162
assert.Empty(t, response.IBCDestinationCallbacks)
@@ -150,14 +168,14 @@ func TestIBCCallbacks(t *testing.T) {
150168
require.NoError(t, wasmibctesting.TimeoutPendingPackets(coord, path))
151169

152170
// then the contract on chain B should not receive anything
153-
var response QueryResp
154-
chainB.SmartQuery(contractAddrB.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
171+
var response queryResp
172+
chainB.SmartQuery(contractAddrB.String(), queryMsg{CallbackStats: struct{}{}}, &response)
155173
assert.Empty(t, response.IBCAckCallbacks)
156174
assert.Empty(t, response.IBCTimeoutCallbacks)
157175
assert.Empty(t, response.IBCDestinationCallbacks)
158176

159177
// and the contract on chain A should receive a callback with the timeout result
160-
chainA.SmartQuery(contractAddrA.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
178+
chainA.SmartQuery(contractAddrA.String(), queryMsg{CallbackStats: struct{}{}}, &response)
161179
assert.Empty(t, response.IBCAckCallbacks)
162180
assert.Len(t, response.IBCTimeoutCallbacks, 1)
163181
assert.Empty(t, response.IBCDestinationCallbacks)
@@ -166,6 +184,78 @@ func TestIBCCallbacks(t *testing.T) {
166184
}
167185
}
168186

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

tests/e2e/testdata/ibc_callbacks.wasm

74.4 KB
Binary file not shown.

x/wasm/ibc.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package wasm
22

33
import (
44
"math"
5+
"slices"
56

67
wasmvmtypes "github.com/CosmWasm/wasmvm/v3/types"
8+
transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types"
79
clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types"
810
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
911
porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types"
@@ -34,11 +36,12 @@ type appVersionGetter interface {
3436
type IBCHandler struct {
3537
keeper types.IBCContractKeeper
3638
channelKeeper types.ChannelKeeper
39+
transferKeeper types.ICS20TransferPortSource
3740
appVersionGetter appVersionGetter
3841
}
3942

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

4447
// OnChanOpenInit implements the IBCModule interface
@@ -412,9 +415,48 @@ func (i IBCHandler) IBCReceivePacketCallback(
412415
return err
413416
}
414417

418+
var funds wasmvmtypes.Array[wasmvmtypes.Coin]
419+
// detect successful transfer
420+
successAck := []byte(`{"result":"AQ=="}`) // TODO: hardcoded check is not nice
421+
if packet.GetDestPort() == i.transferKeeper.GetPort(cachedCtx) &&
422+
ack.Success() && slices.Equal(ack.Acknowledgement(), successAck) {
423+
// decode packet
424+
transferData, err := transfertypes.UnmarshalPacketData(packet.GetData(), version, "")
425+
if err != nil {
426+
return errorsmod.Wrap(err, "unmarshal transfer packet data")
427+
}
428+
429+
// validate receiver address
430+
receiverAddr, err := sdk.AccAddressFromBech32(transferData.Receiver)
431+
if err != nil {
432+
return err
433+
}
434+
if receiverAddr.Equals(contractAddr) {
435+
// fill funds with the transfer amount
436+
437+
// For a more in-depth explanation of the logic here, see the transfer module implementation:
438+
// https://github.com/cosmos/ibc-go/blob/a6217ab02a4d57c52a938eeaff8aeb383e523d12/modules/apps/transfer/keeper/relay.go#L147-L175
439+
if transferData.Token.Denom.HasPrefix(packet.GetSourcePort(), packet.GetSourceChannel()) {
440+
// this is a denom coming from this chain, being sent back again
441+
// remove prefix
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+
trace := []transfertypes.Hop{transfertypes.NewHop(packet.GetDestPort(), packet.GetDestChannel())}
446+
transferData.Token.Denom.Trace = append(trace, transferData.Token.Denom.Trace...)
447+
}
448+
449+
funds = append(funds, wasmvmtypes.Coin{
450+
Denom: transferData.Token.GetDenom().IBCDenom(),
451+
Amount: transferData.Token.GetAmount(),
452+
})
453+
}
454+
}
455+
415456
msg := wasmvmtypes.IBCDestinationCallbackMsg{
416457
Ack: wasmvmtypes.IBCAcknowledgement{Data: ack.Acknowledgement()},
417458
Packet: newIBCPacket(packet),
459+
Funds: funds,
418460
}
419461

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