Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Receiving IBC Callbacks #40

Merged
merged 4 commits into from
Jun 18, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 216 additions & 14 deletions src/pages/ibc/existing-protocols.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
tags: ["ibc", "ics20"]
---

import { Callout } from "nextra/components";
import { Callout, Tabs } from "nextra/components";

# Using existing protocols

Expand Down Expand Up @@ -96,15 +96,46 @@ the callbacks.
to the `memo` field.
</Callout>

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 */}

<Tabs items={['TransferMsgBuilder (recommended)', 'IbcCallbackRequest']}>
<Tabs.Tab>

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

</Tabs.Tab>

<Tabs.Tab>

```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 {
Expand All @@ -115,8 +146,14 @@ let _ = IbcMsg::Transfer {
gas_limit: None,
})).unwrap()),
};

Ok(Response::new().add_message(msg))
```

</Tabs.Tab>

</Tabs>

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
Expand All @@ -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.
</Callout>

### 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<IbcBasicResponse> {
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.

<Callout type="warning">
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.
</Callout>

This is how you can implement the `ibc_destination_callback` entrypoint:

<Callout>
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`.
</Callout>

{/* 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<IbcBasicResponse> {
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"))
}
```

<Callout type="warning">
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.
</Callout>

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 */}