Skip to content

Commit

Permalink
feat: implemented timeout (#7)
Browse files Browse the repository at this point in the history
* feat: timeout implemented

* imp

* initial test

* works

* imp: added ci
  • Loading branch information
srdtrk authored Jul 8, 2024
1 parent a4155e2 commit 924b8e5
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 21 deletions.
1 change: 1 addition & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
- TestWithICS07TendermintTestSuite/TestVerifyMembership
- TestWithIBCLiteTestSuite/TestWasmProofs
- TestWithIBCLiteTestSuite/TestCW20Transfer
- TestWithIBCLiteTestSuite/TestTimeout
name: ${{ matrix.test }}
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
24 changes: 8 additions & 16 deletions contracts/ics20-transfer/src/ibc/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Response, ContractError> {
todo!()
on_acknowledgement_packet::error(deps, env, info, packet, "timeout".to_string(), relayer)
}

mod on_acknowledgement_packet {
Expand Down Expand Up @@ -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(|_| {
Expand All @@ -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,
Expand Down
100 changes: 95 additions & 5 deletions contracts/ics26-router/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<Response, ContractError> {
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)]
Expand Down
30 changes: 30 additions & 0 deletions contracts/ics26-router/src/types/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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()),
])
}
}
79 changes: 79 additions & 0 deletions e2e/interchaintestv8/ibclite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions packages/shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/types/ibc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ impl From<Height> for ibc_proto::ibc::core::client::v1::Height {
}
}

#[allow(clippy::fallible_impl_from)]
impl From<Height> 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;
Expand Down

0 comments on commit 924b8e5

Please sign in to comment.