diff --git a/tests/ibc-hooks/bytecode/counter.wasm b/tests/ibc-hooks/bytecode/counter.wasm index af14177e329..2e4d8b7ccf9 100644 Binary files a/tests/ibc-hooks/bytecode/counter.wasm and b/tests/ibc-hooks/bytecode/counter.wasm differ diff --git a/tests/ibc-hooks/ibc_middleware_test.go b/tests/ibc-hooks/ibc_middleware_test.go index 9f316700bb8..35bd95ffd30 100644 --- a/tests/ibc-hooks/ibc_middleware_test.go +++ b/tests/ibc-hooks/ibc_middleware_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "testing" + "time" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" @@ -460,6 +461,39 @@ func (suite *HooksTestSuite) TestAcks() { } +func (suite *HooksTestSuite) TestTimeouts() { + suite.chainA.StoreContractCode(&suite.Suite, "./bytecode/counter.wasm") + addr := suite.chainA.InstantiateContract(&suite.Suite, `{"count": 0}`, 1) + + // Generate swap instructions for the contract + callbackMemo := fmt.Sprintf(`{"ibc_callback":"%s"}`, addr) + // Send IBC transfer with the memo with crosschain-swap instructions + transferMsg := NewMsgTransfer(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000)), suite.chainA.SenderAccount.GetAddress().String(), addr.String(), callbackMemo) + transferMsg.TimeoutTimestamp = uint64(suite.coordinator.CurrentTime.Add(time.Minute).UnixNano()) + sendResult, err := suite.chainA.SendMsgsNoCheck(transferMsg) + suite.Require().NoError(err) + + packet, err := ibctesting.ParsePacketFromEvents(sendResult.GetEvents()) + suite.Require().NoError(err) + + // Move chainB forward one block + suite.chainB.NextBlock() + // One month later + suite.coordinator.IncrementTimeBy(time.Hour) + err = suite.path.EndpointA.UpdateClient() + suite.Require().NoError(err) + + err = suite.path.EndpointA.TimeoutPacket(packet) + suite.Require().NoError(err) + + // The test contract will increment the counter for itself by 10 when a packet times out + state := suite.chainA.QueryContract( + &suite.Suite, addr, + []byte(fmt.Sprintf(`{"get_count": {"addr": "%s"}}`, addr))) + suite.Require().Equal(`{"count":10}`, state) + +} + func (suite *HooksTestSuite) TestSendWithoutMemo() { // Sending a packet without memo to ensure that the ibc_callback middleware doesn't interfere with a regular send transferMsg := NewMsgTransfer(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000)), suite.chainA.SenderAccount.GetAddress().String(), suite.chainA.SenderAccount.GetAddress().String(), "") diff --git a/tests/ibc-hooks/testutils/contracts/counter/src/contract.rs b/tests/ibc-hooks/testutils/contracts/counter/src/contract.rs index db9d9e0e723..963973a92a8 100644 --- a/tests/ibc-hooks/testutils/contracts/counter/src/contract.rs +++ b/tests/ibc-hooks/testutils/contracts/counter/src/contract.rs @@ -117,6 +117,10 @@ pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result sudo::receive_ack(deps, env.contract.address, success), + SudoMsg::IBCTimeout { + channel: _, + sequence: _, + } => sudo::ibc_timeout(deps, env.contract.address), } } @@ -141,6 +145,19 @@ pub mod sudo { )?; Ok(Response::new().add_attribute("action", "ack")) } + + pub(crate) fn ibc_timeout(deps: DepsMut, contract: Addr) -> Result { + utils::update_counter( + deps, + contract, + &|counter| match counter { + None => 10, + Some(counter) => counter.count + 10, + }, + &|_counter| vec![], + )?; + Ok(Response::new().add_attribute("action", "timeout")) + } } pub fn naive_add_coins(lhs: &Vec, rhs: &Vec) -> Vec { diff --git a/tests/ibc-hooks/testutils/contracts/counter/src/msg.rs b/tests/ibc-hooks/testutils/contracts/counter/src/msg.rs index 334791b673b..db85e9ad4e5 100644 --- a/tests/ibc-hooks/testutils/contracts/counter/src/msg.rs +++ b/tests/ibc-hooks/testutils/contracts/counter/src/msg.rs @@ -41,4 +41,6 @@ pub enum SudoMsg { ack: String, success: bool, }, + #[serde(rename = "ibc_timeout")] + IBCTimeout { channel: String, sequence: u64 }, } diff --git a/x/ibc-hooks/wasm_hook.go b/x/ibc-hooks/wasm_hook.go index de9a147ec7b..571b3e05935 100644 --- a/x/ibc-hooks/wasm_hook.go +++ b/x/ibc-hooks/wasm_hook.go @@ -305,7 +305,7 @@ func (h WasmHooks) OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Con contractAddr, err := sdk.AccAddressFromBech32(contract) if err != nil { - return sdkerrors.Wrap(err, "Ack callback error") // The callback configured is not a beck32. Error out + return sdkerrors.Wrap(err, "Ack callback error") // The callback configured is not a bech32. Error out } success := "false" @@ -326,8 +326,47 @@ func (h WasmHooks) OnAcknowledgementPacketOverride(im IBCMiddleware, ctx sdk.Con _, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg) if err != nil { // error processing the callback + // ToDo: Open Question: Should we also delete the callback here? return sdkerrors.Wrap(err, "Ack callback error") } h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) return nil } + +func (h WasmHooks) OnTimeoutPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error { + err := im.App.OnTimeoutPacket(ctx, packet, relayer) + if err != nil { + return err + } + + if !h.ProperlyConfigured() { + // Not configured. Return from the underlying implementation + return nil + } + + contract := h.ibcHooksKeeper.GetPacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + if contract == "" { + // No callback configured + return nil + } + + contractAddr, err := sdk.AccAddressFromBech32(contract) + if err != nil { + return sdkerrors.Wrap(err, "Timeout callback error") // The callback configured is not a bech32. Error out + } + + sudoMsg := []byte(fmt.Sprintf( + `{"ibc_timeout": {"channel": "%s", "sequence": %d}}`, + packet.SourceChannel, packet.Sequence)) + _, err = h.ContractKeeper.Sudo(ctx, contractAddr, sudoMsg) + if err != nil { + // error processing the callback. This could be because the contract doesn't implement the message type to + // process the callback. Retrying this will not help, so we delete the callback from storage. + // Since the packet has timed out, we don't expect any other responses that may trigger the callback. + h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + return sdkerrors.Wrap(err, "Timeout callback error") + } + // + h.ibcHooksKeeper.DeletePacketCallback(ctx, packet.GetSourceChannel(), packet.GetSequence()) + return nil +}