diff --git a/src/pages/ibc/existing-protocols.mdx b/src/pages/ibc/existing-protocols.mdx index e9369a0f..bcebaae9 100644 --- a/src/pages/ibc/existing-protocols.mdx +++ b/src/pages/ibc/existing-protocols.mdx @@ -2,7 +2,7 @@ tags: ["ibc", "ics20"] --- -import { Callout } from "nextra/components"; +import { Callout, Tabs } from "nextra/components"; # Using existing protocols @@ -96,15 +96,46 @@ the callbacks. to the `memo` field. -To make this as easy as possible, we provide a helper type `IbcCallbackRequest` -that you can use to generate the JSON: +{/* TODO: add link to `IbcCallbackRequest` docs once IBC Callbacks are released */} -{/* TODO: add `template="execute"` once IBC Callbacks are merged */} +To make this as easy as possible, we provide two ways to generate the correct +JSON. One is a builder type for the `TransferMsg` type which provides a +type-safe way to generate the complete `TransferMsg`, the other is a helper type +`IbcCallbackRequest` that just generates the JSON for the `memo` field: + +{/* TODO: add `template="execute"` once IBC Callbacks are released */} + + + ```rust -let _ = IbcMsg::Transfer { - to_address: "cosmos1exampleaddress".to_string(), +let msg = TransferMsgBuilder::new( + "channel-0".to_string(), + "cosmos1exampleaddress".to_string(), + Coin::new(10u32, "ucoin"), + Timestamp::from_seconds(12345), +) +.with_src_callback(IbcSrcCallback { + address: env.contract.address, + gas_limit: None, +}) +.with_dst_callback(IbcDstCallback { + address: to_address.clone(), + gas_limit: None, +}) +.build(); + +Ok(Response::new().add_message(msg)) +``` + + + + + +```rust +let msg = IbcMsg::Transfer { channel_id: "channel-0".to_string(), + to_address: "cosmos1exampleaddress".to_string(), amount: Coin::new(10u32, "ucoin"), timeout: Timestamp::from_seconds(12345).into(), memo: Some(to_json_string(&IbcCallbackRequest::both(IbcSrcCallback { @@ -115,8 +146,14 @@ let _ = IbcMsg::Transfer { gas_limit: None, })).unwrap()), }; + +Ok(Response::new().add_message(msg)) ``` + + + + As you can see, you can request callbacks for both the source and destination chain. However, you can also request callbacks for only one of them. For this, you need to provide the address that should receive the callback and you can @@ -129,16 +166,181 @@ optionally set a gas limit for the callback execution. Please take a look at the error and the contract will not be called. -### Entrypoints +### Receiving IBC Callbacks -TODO +To receive callbacks, you need to implement two new entrypoints in your +contract: -- two new entrypoints - - `ibc_source_chain_callback` - - `ibc_destination_chain_callback` +{/* TODO: add docs links once IBC Callbacks are released */} ---- +- `ibc_source_callback`, receiving an `IbcSourceCallbackMsg` which can be one of + two types: + - `IbcAckCallbackMsg` if the packet was acknowledged + - `IbcTimeoutCallbackMsg` if the packet timed out +- `ibc_destination_callback`, receiving an `IbcDestinationCallbackMsg` + +#### Source Callback + +The `ibc_source_callback` entrypoint is called when the packet was either +acknowledged or timed out. You can use this to update your contract state, +release locked funds or trigger other actions. + +As mentioned above, the receiver of this callback is always the contract that +sent the message. This means you don't need to assume that an attacker might be +sending you fake callbacks, reducing the need for validation. + +This is how you can implement the `ibc_source_callback` entrypoint: + +{/* TODO: Add template="core" when callbacks are released */} + +```rust +#[entry_point] +pub fn ibc_source_callback( + deps: DepsMut, + _env: Env, + msg: IbcSourceCallbackMsg, +) -> StdResult { + match msg { + IbcSourceCallbackMsg::Acknowledgement(ack) => { + // handle the acknowledgement + } + IbcSourceCallbackMsg::Timeout(timeout) => { + // handle the timeout + } + } + + Ok(IbcBasicResponse::new().add_attribute("action", "ibc_source_callback")) +} +``` + +##### Acknowledgement + +When the packet was acknowledged, you will receive the +`Acknowledgement(IbcAckCallbackMsg)` variant of `IbcSourceCallbackMsg`. This +means that the packet was successfully received and processed by the application +on the destination chain. The message contains the original packet data, the +acknowledgement and the address of the relayer. + +##### Timeout + +When the packet timed out, you will receive the `Timeout(IbcTimeoutCallbackMsg)` +variant of `IbcSourceCallbackMsg`. This means that the packet was not delivered +to the destination chain in time (e.g. because no relayer picked it up or the +chain is stopped). The message contains the original packet data and the address +of the relayer who told you about the timeout. + +#### Destination Callback + +The `ibc_destination_callback` entrypoint is called when a packet was +acknowledged on the destination chain. For the `IbcMsg::Transfer` message, this +means that the tokens were successfully transferred to the destination chain. It +allows you to use the received tokens immediately, update the contract state to +reflect the new tokens or trigger other actions. + + + It is important to validate that the packet and acknowledgement are what you + expect them to be. For example for a transfer message, the receiver of the + funds is not necessarily the contract that receives the callbacks. + + +This is how you can implement the `ibc_destination_callback` entrypoint: + + + This example uses the `ibc` crate with the `serde` feature, which provides a + data type for the transfer packet format to avoid defining that ourselves. You + can add it to your `Cargo.toml` by running `cargo add ibc --features serde`. + + +{/* TODO: Add template="core" when callbacks are released */} + +```rust +use ibc::apps::transfer::types::packet::PacketData as TransferPacketData; + +#[entry_point] +pub fn ibc_destination_callback( + deps: DepsMut, + _env: Env, + msg: IbcDestinationCallbackMsg, +) -> StdResult { + ensure_eq!( + msg.packet.dest.port_id, + "transfer", // transfer module uses this port by default + StdError::generic_err("only want to handle transfer packets") + ); + ensure_eq!( + msg.ack.data, + StdAck::success(b"\x01").to_binary(), // this is how a successful transfer ack looks + StdError::generic_err("only want to handle successful transfers") + ); + // At this point we know that this is a callback for a successful transfer, + // but not to whom it is going, how much and what denom. + + // Parse the packet data to get that information: + let packet_data: TransferPacketData = from_json(&msg.packet.data)?; + + // The receiver should be a valid address on this chain. + // Remember, we are on the destination chain. + let receiver = deps.api.addr_validate(packet_data.receiver.as_ref())?; + ensure_eq!( + receiver, + env.contract.address, + StdError::generic_err("only want to handle transfers to this contract") + ); + + // We only care about this chain's native token in this example. + // The `packet_data.token.denom` is formatted as `{port id}/{channel id}/{denom}`, + // where the port id and channel id are the source chain's identifiers. + // Assuming we are running this on Neutron and only want to handle NTRN tokens, + // the denom should look like this: + let ntrn_denom = format!( + "{}/{}/untrn", + msg.packet.src.port_id, msg.packet.src.channel_id + ); + ensure_eq!( + packet_data.token.denom.to_string(), + ntrn_denom, + StdError::generic_err("only want to handle NTRN tokens") + ); + + // Now, we can do something with our tokens. + // For example, we could send them to some address: + let msg = BankMsg::Send { + to_address: "neutron155exr8rqjrknusllpzxdfvezxr8ddpqehj9g9d".to_string(), + // this panics if the amount is too large + amount: coins(packet_data.token.amount.as_ref().as_u128(), "untrn"), + }; + + Ok(IbcBasicResponse::new() + .add_message(msg) + .add_attribute("action", "ibc_destination_callback")) +} +``` + + + Please note that this example assumes an ICS20 v1 channel. At the time of + writing, the specification and implementation have just been extended with a + v2 which changes the [packet format]. If you want to use this in production + code, you should make sure to support both formats, such that a channel + upgrade does not break your contract. + + +As mentioned above, anyone can send you a destination callback for a packet. +This means you need to make sure that the packet and acknowledgement are what +you expect them to be. For example, for a transfer message, you need to make +sure that the transfer was successful, that the receiver of the funds is your +contract and the denomination is what you want to receive. This requires some +knowledge about the [packet format]. + +[packet format]: + https://github.com/cosmos/ibc/blob/main/spec/app/ics-020-fungible-token-transfer/README.md#data-structures + +#### Error handling -Notes: +Returning an error or panicking from a callback will **not** influence the IBC +packet lifecycle. The packet will still be acknowledged or timed out. This means +that you can safely return errors from your callbacks if you want to ignore the +packet. -- add link to IbcCallbackRequest docs when merged and deployed +It will, however, undo any state changes that you made in the callback, just +like most other entrypoints. +{/* TODO: add link to corresponding core section when that is written */}