From 924b8e5356a805f92dda43dd6b2d6be411bb4e9c Mon Sep 17 00:00:00 2001 From: srdtrk <59252793+srdtrk@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:45:25 +0200 Subject: [PATCH] feat: implemented timeout (#7) * feat: timeout implemented * imp * initial test * works * imp: added ci --- .github/workflows/e2e.yml | 1 + Cargo.lock | 1 + Cargo.toml | 1 + contracts/ics20-transfer/src/ibc/relay.rs | 24 ++--- contracts/ics26-router/src/contract.rs | 100 +++++++++++++++++++-- contracts/ics26-router/src/types/events.rs | 30 +++++++ e2e/interchaintestv8/ibclite_test.go | 79 ++++++++++++++++ packages/shared/Cargo.toml | 1 + packages/shared/src/types/ibc.rs | 7 ++ 9 files changed, 223 insertions(+), 21 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 714c7b9..b6be3c5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -39,6 +39,7 @@ jobs: - TestWithICS07TendermintTestSuite/TestVerifyMembership - TestWithIBCLiteTestSuite/TestWasmProofs - TestWithIBCLiteTestSuite/TestCW20Transfer + - TestWithIBCLiteTestSuite/TestTimeout name: ${{ matrix.test }} runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 34cec2a..05e7535 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,7 @@ dependencies = [ "cw-ownable", "derive_more", "ibc-client-cw", + "ibc-core-client-types", "ibc-core-host", "ibc-proto", "schemars", diff --git a/Cargo.toml b/Cargo.toml index 31bd6e1..c655b39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ cw-ownable = { git = "https://github.com/CyberHoward/cw-plus-plus", branch = "bu ibc-core-host = { git = "https://github.com/srdtrk/ibc-rs", branch = "serdar/xxx-allow-ibc-lite-paths", default-features = false, features = ["schema"] } ibc-client-cw = { git = "https://github.com/srdtrk/ibc-rs", branch = "serdar/xxx-allow-ibc-lite-paths", default-features = false } ibc-client-tendermint = { git = "https://github.com/srdtrk/ibc-rs", branch = "serdar/xxx-allow-ibc-lite-paths", default-features = false, features = ["schema"] } +ibc-core-client-types = { git = "https://github.com/srdtrk/ibc-rs", branch = "serdar/xxx-allow-ibc-lite-paths", default-features = false } cw-ibc-lite-shared = { version = "0.1.0", path = "./packages/shared/" } cw-ibc-lite-derive = { version = "0.1.0", path = "./packages/derive/" } cw-ibc-lite-ics02-client = { version = "0.1.0", path = "./contracts/ics02-client/", default-features = false } diff --git a/contracts/ics20-transfer/src/ibc/relay.rs b/contracts/ics20-transfer/src/ibc/relay.rs index 2dea4af..0460c63 100644 --- a/contracts/ics20-transfer/src/ibc/relay.rs +++ b/contracts/ics20-transfer/src/ibc/relay.rs @@ -124,7 +124,6 @@ pub fn on_recv_packet( /// /// # Errors /// Will return an error if the acknowledgement cannot be processed. -#[allow(clippy::needless_pass_by_value)] pub fn on_acknowledgement_packet( deps: DepsMut, env: Env, @@ -147,15 +146,14 @@ pub fn on_acknowledgement_packet( /// /// # Errors /// Will return an error if the timeout cannot be processed and tokens refunded. -#[allow(clippy::needless_pass_by_value)] pub fn on_timeout_packet( - _deps: DepsMut, - _env: Env, - _info: MessageInfo, - _packet: ibc::Packet, - _relayer: String, + deps: DepsMut, + env: Env, + info: MessageInfo, + packet: ibc::Packet, + relayer: String, ) -> Result { - todo!() + on_acknowledgement_packet::error(deps, env, info, packet, "timeout".to_string(), relayer) } mod on_acknowledgement_packet { @@ -200,16 +198,10 @@ mod on_acknowledgement_packet { return Err(TransferError::unexpected_port_id(port_id, packet.source_port).into()); } - let base_denom = utils::transfer::parse_voucher_denom( - &ics20_packet.denom, - port_id.as_str(), - packet.source_channel.as_str(), - )?; - // Refund the escrowed balance. state::ESCROW.update( deps.storage, - (packet.source_channel.as_str(), base_denom), + (packet.source_channel.as_str(), &ics20_packet.denom), |escrowed_bal| -> Result<_, ContractError> { let mut escrowed_bal = escrowed_bal.unwrap_or_default(); escrowed_bal = escrowed_bal.checked_sub(ics20_packet.amount).map_err(|_| { @@ -220,7 +212,7 @@ mod on_acknowledgement_packet { )?; let cw20_msg: CosmosMsg = WasmMsg::Execute { - contract_addr: base_denom.to_string(), + contract_addr: ics20_packet.denom, msg: cosmwasm_std::to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient: ics20_packet.sender, amount: ics20_packet.amount, diff --git a/contracts/ics26-router/src/contract.rs b/contracts/ics26-router/src/contract.rs index 1ce2604..c796c8d 100644 --- a/contracts/ics26-router/src/contract.rs +++ b/contracts/ics26-router/src/contract.rs @@ -116,7 +116,9 @@ mod execute { utils, }; - use ibc_client_cw::types::VerifyMembershipMsgRaw; + use ibc_client_cw::types::{ + TimestampAtHeightMsg, VerifyMembershipMsgRaw, VerifyNonMembershipMsgRaw, + }; #[allow(clippy::too_many_arguments, clippy::needless_pass_by_value)] pub fn send_packet( @@ -343,12 +345,100 @@ mod execute { #[allow(clippy::needless_pass_by_value)] pub fn timeout( - _deps: DepsMut, + deps: DepsMut, _env: Env, - _info: MessageInfo, - _msg: TimeoutMsg, + info: MessageInfo, + msg: TimeoutMsg, ) -> Result { - todo!() + let packet = msg.packet; + + let ics02_address = state::ICS02_CLIENT_ADDRESS.load(deps.storage)?; + let ics02_contract = ics02_client::helpers::Ics02ClientContract::new(ics02_address); + + let ibc_app_address = state::IBC_APPS.load(deps.storage, packet.source_port.as_str())?; + let ibc_app_contract = apps::helpers::IbcApplicationContract::new(ibc_app_address); + + // Verify the counterparty. + let counterparty = ics02_contract + .query(&deps.querier) + .client_info(packet.source_channel.as_str())? + .counterparty_info + .ok_or(ContractError::CounterpartyNotFound)?; + if counterparty.client_id != packet.destination_channel.as_str() { + return Err(ContractError::invalid_counterparty( + counterparty.client_id, + packet.destination_channel.into(), + )); + } + + // NOTE: If commitment cannot be loaded, this indicates that this packet has already been + // acknowledged, timed out, or never sent. IBC Go treats this error as a no-op in order to + // prevent an entire relay transaction from failing and consuming unnecessary fees. We + // don't do this here. + let stored_packet_commitment = PureItem::from(ics24_host::PacketCommitmentPath { + port_id: packet.source_port.clone(), + channel_id: packet.source_channel.clone(), + sequence: packet.sequence, + }) + .load(deps.storage)?; + if stored_packet_commitment != packet.to_commitment_vec() { + return Err(ContractError::packet_commitment_mismatch( + stored_packet_commitment, + packet.to_commitment_vec(), + )); + } + + // Verify the timeout timestamp. + let timeout_timestamp = packet + .timeout + .timestamp() + .ok_or(ContractError::EmptyTimestamp)? + .nanos(); + let counterparty_timestamp = ics02_contract + .query(&deps.querier) + .client_querier(packet.source_channel.as_str())? + .timestamp_at_height(TimestampAtHeightMsg { + height: msg.proof_height.clone().into(), + })? + .timestamp; + if counterparty_timestamp < timeout_timestamp { + return Err(ContractError::invalid_timeout_timestamp( + counterparty_timestamp, + timeout_timestamp, + )); + } + + // Verify the packet non-membership. + let packet_ack_path: ics24_host::MerklePath = ics24_host::PacketReceiptPath { + port_id: packet.destination_port.clone(), + channel_id: packet.destination_channel.clone(), + sequence: packet.sequence, + } + .to_prefixed_merkle_path(counterparty.merkle_path_prefix)?; + let _ = ics02_contract + .query(&deps.querier) + .client_querier(packet.source_channel.as_str())? + .verify_non_membership(VerifyNonMembershipMsgRaw { + proof: msg.proof_unreceived.into(), + path: packet_ack_path, + height: msg.proof_height.into(), + delay_time_period: 0, + delay_block_period: 0, + })?; + + state::helpers::delete_packet_commitment(deps.storage, &packet)?; + + let event = events::timeout_packet::success(&packet); + let callback_msg = apps::callbacks::IbcAppCallbackMsg::OnTimeoutPacket { + packet, + relayer: info.sender.into(), + }; + + let timeout_callback = ibc_app_contract.call(callback_msg)?; + + Ok(Response::new() + .add_message(timeout_callback) + .add_event(event)) } #[allow(clippy::needless_pass_by_value)] diff --git a/contracts/ics26-router/src/types/events.rs b/contracts/ics26-router/src/types/events.rs index 1f1df88..bcd64b1 100644 --- a/contracts/ics26-router/src/types/events.rs +++ b/contracts/ics26-router/src/types/events.rs @@ -10,6 +10,8 @@ pub const EVENT_TYPE_RECV_PACKET: &str = "recv_packet"; pub const EVENT_TYPE_WRITE_ACKNOWLEDGEMENT: &str = "write_acknowledgement"; /// `EVENT_TYPE_ACKNOWLEDGE_PACKET` is the event type for an acknowledge packet event pub const EVENT_TYPE_ACKNOWLEDGE_PACKET: &str = "acknowledge_packet"; +/// `EVENT_TYPE_TIMEOUT_PACKET` is the event type for a timeout packet event +pub const EVENT_TYPE_TIMEOUT_PACKET: &str = "timeout_packet"; /// `ATTRIBUTE_KEY_CONTRACT_ADDRESS` is the attribute key for the contract address pub const ATTRIBUTE_KEY_CONTRACT_ADDRESS: &str = "contract_address"; @@ -179,3 +181,31 @@ pub mod acknowledge_packet { ]) } } + +/// Contains event messages emitted during the reply to +/// [`cw_ibc_lite_shared::types::apps::callbacks::IbcAppCallbackMsg::OnTimeoutPacket`] +pub mod timeout_packet { + use cosmwasm_std::{Attribute, Event}; + use cw_ibc_lite_shared::types::ibc; + + /// `timeout_packet` is the event message for a timeout packet event + #[must_use] + pub fn success(packet: &ibc::Packet) -> Event { + Event::new(super::EVENT_TYPE_TIMEOUT_PACKET).add_attributes(vec![ + Attribute::new(super::ATTRIBUTE_KEY_SRC_PORT, packet.source_port.as_str()), + Attribute::new( + super::ATTRIBUTE_KEY_SRC_CHANNEL, + packet.source_channel.as_str(), + ), + Attribute::new( + super::ATTRIBUTE_KEY_DST_PORT, + packet.destination_port.as_str(), + ), + Attribute::new( + super::ATTRIBUTE_KEY_DST_CHANNEL, + packet.destination_channel.as_str(), + ), + Attribute::new(super::ATTRIBUTE_KEY_SEQUENCE, packet.sequence.to_string()), + ]) + } +} diff --git a/e2e/interchaintestv8/ibclite_test.go b/e2e/interchaintestv8/ibclite_test.go index dab79b3..27eef79 100644 --- a/e2e/interchaintestv8/ibclite_test.go +++ b/e2e/interchaintestv8/ibclite_test.go @@ -553,6 +553,85 @@ func (s *IBCLiteTestSuite) TestCW20Transfer() { })) } +func (s *IBCLiteTestSuite) TestTimeout() { + ctx := context.Background() + s.SetupSuite(ctx) + + _, simd := s.ChainA, s.ChainB + + // Transfer some tokens from UserA to UserB + const sendAmount = 1_000_000 + var packet channeltypes.Packet + s.Require().True(s.Run("SendPacket", func() { + timeoutSeconds := uint64(10) + transferMsg := cw20base.MsgTransfer{ + SourceChannel: testvalues.FirstWasmClientID, + Receiver: s.UserB.FormattedAddress(), + Timeout: &timeoutSeconds, + } + cw20SendMsg := cw20base.ExecuteMsg{ + Send: &cw20base.ExecuteMsg_Send{ + Amount: cw20base.Uint128(strconv.FormatInt(sendAmount, 10)), + Contract: s.ics20Transfer.Address, + Msg: cw20base.ToJsonBinary(transferMsg), + }, + } + + res, err := s.cw20Base.Execute(ctx, s.UserA.KeyName(), cw20SendMsg, "--gas", "500000") + s.Require().NoError(err) + + packet, err = s.ExtractPacketFromEvents(res.Events) + s.Require().NoError(err) + + s.Require().True(s.Run("Check balances", func() { + // Check the balance of UserA + cw20Resp, err := s.cw20Base.QueryClient().Balance(ctx, &cw20base.QueryMsg_Balance{Address: s.UserA.FormattedAddress()}) + s.Require().NoError(err) + s.Require().Equal(strconv.FormatInt(testvalues.StartingTokenAmount-sendAmount, 10), string(cw20Resp.Balance)) + })) + })) + + // Wait for the timeout + time.Sleep(15 * time.Second) + s.UpdateClientContract(ctx, s.ics07Tendermint, simd) + + var ( + proofHeight int64 + proof []byte + value []byte + merklePath commitmenttypesv2.MerklePath + ) + s.Require().True(s.Run("Generate timeout proof", func() { + var err error + key := host.PacketReceiptKey(packet.DestinationPort, packet.DestinationChannel, packet.Sequence) + merklePath = commitmenttypes.NewMerklePath(key) + merklePath, err = commitmenttypes.ApplyPrefix(commitmenttypes.NewMerklePrefix([]byte(ibcexported.StoreKey)), merklePath) + s.Require().NoError(err) + + value, proof, proofHeight, err = s.QueryProofs(ctx, simd, ibcexported.StoreKey, key, int64(s.trustedHeight.RevisionHeight)) + s.Require().NoError(err) + s.Require().NotEmpty(proof) + s.Require().Empty(value) + s.Require().Equal(int64(s.trustedHeight.RevisionHeight), proofHeight) + })) + + s.Require().True(s.Run("TimeoutPacket", func() { + timeoutMsg := ics26router.ExecuteMsg{ + Timeout: &ics26router.ExecuteMsg_Timeout{ + Packet: ics26router.ToPacket(packet), + ProofUnreceived: ics26router.ToBinary(proof), + ProofHeight: ics26router.Height{ + RevisionHeight: int(s.trustedHeight.RevisionHeight), + RevisionNumber: int(s.trustedHeight.RevisionNumber), + }, + }, + } + + _, err := s.ics26Router.Execute(ctx, s.UserA.KeyName(), timeoutMsg, "--gas", "700000") + s.Require().NoError(err) + })) +} + // This is a test to verify that go clients can prove the state of cosmwasm contracts func (s *IBCLiteTestSuite) TestWasmProofs() { ctx := context.Background() diff --git a/packages/shared/Cargo.toml b/packages/shared/Cargo.toml index d1f9301..1bec748 100644 --- a/packages/shared/Cargo.toml +++ b/packages/shared/Cargo.toml @@ -15,6 +15,7 @@ serde = { workspace = true } thiserror = { workspace = true } ibc-core-host = { workspace = true } ibc-client-cw = { workspace = true } +ibc-core-client-types = { workspace = true } cw-ownable = { workspace = true } sha2 = { workspace = true } ibc-proto = { workspace = true } diff --git a/packages/shared/src/types/ibc.rs b/packages/shared/src/types/ibc.rs index fae164f..df11670 100644 --- a/packages/shared/src/types/ibc.rs +++ b/packages/shared/src/types/ibc.rs @@ -140,6 +140,13 @@ impl From for ibc_proto::ibc::core::client::v1::Height { } } +#[allow(clippy::fallible_impl_from)] +impl From for ibc_core_client_types::Height { + fn from(height: Height) -> Self { + Self::new(height.revision_number, height.revision_height).unwrap() + } +} + #[cfg(test)] mod tests { use crate::types::transfer::packet::Ics20Ack;