Skip to content

Instantly share code, notes, and snippets.

@larry0x
Last active November 15, 2022 02:02
Show Gist options
  • Save larry0x/a12c566e5a58556bba7e6fd5754dc3c9 to your computer and use it in GitHub Desktop.
Save larry0x/a12c566e5a58556bba7e6fd5754dc3c9 to your computer and use it in GitHub Desktop.

ICS-20 Extended

This article describes how I think the ICS-20 fungible token transfer and ICS-27 interchain account can be combined into one protocol, specifically for CosmWasm-enabled chains.

For simplicity, let's call this new standard "ICS-20e" (e = extended). ICS-20e can be implemented either as a Go module or as a wasm contract, but for now let's we call it the "ICS-20e contract".

Packet data

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Binary, Coin, Uint128};

#[cw_serde]
struct PacketData {
    sender: String,
    coins: Vec<Coin>,
    msg: PacketMsg,
}

#[cw_serde]
enum PacketMsg {
    NoAction {
        recipient: String,
    },
    Instantiate {
        code_id: u64,
        msg: Binary,
        label: String,
        admin: Option<String>,
    },
    Execute {
        contract_addr: String,
        msg: Binary,
    },
    Migrate {
        contract_addr: String,
        code_id: u64,
        msg: Binary,
    },
}

Firstly, while the current ICS-20 standard only allows one coin per packet, with ICS-20e it is possible to batch many tokens into a single packet.

Secondly, the packet contains a msg parameter. While similar to the current ICS-20's memo, it specifies that the msg must be one of the variants: no action, instantiate, execute, or migrate.

Interchain account

An ICA is a wasm contract that implements the following execute method:

use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Respoonse, StdError, WasmMsg};
use cw_storage_plus::Item;

const ICS20E_CONTRACT: Item<Addr> = Item::new("ics20e_contract");

#[entry_point]
fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: WasmMsg,
) -> Result<Response> {
    if info.sender != ICS20E_CONTRACT.load(deps.storage)? {
        return Err(StdError::Unauthorized {});
    }

    Ok(Response::new().add_message(WasmMsg))
}

See we see, the ICA contract accepts an arbitrary WasmMsg, verifies that it is from the ICS-20e contract, then simply dispatches it.

Packet handling

When a packet is received, the action to take depends on the msg parameter. If it is Ics20Msg::NoAction, then the transfer is handled similarly to the current ICS-20: IOU tokens with the appropriate ibc/* denoms are minted to the recipient, with no further action.

If the msg is Instantiate, Execute, or Migrate, the module first checks whether the sender has an ICA contract associated with it. If no, one will be instantiated and the address recorded in the following map:

use cw_storage_plus::Map;

/// (connection id, sender address) => interchain account address
const INTERCHAIN_ACCOUNTS: Map<(&str, &str), Addr> = Map::new("icas");

Once the ICA is created, the ICS-20e contract will translate the ICS20Msg to WasmMsg, and send it to the ICA contract:

use cosmwasm_std::BankMsg;

fn handle_packet(packet: PacketData, ica_addr: &Addr) -> Result<Response> {
    // convert denoms on the source chain to the denom traces on the recipient
    // chain
    let coins: Vec<Coins> = to_denom_traces(&packet.coins)?;

    let wasm_msg = match packet.msg {
        // if the packet msg is "no action", simply mint the coins to the
        // recipient with no further action
        Ics20Msg::NoAction {
            recipient,
        } => {
            return Ok(Response::new().add_message(BankMsg::Send {
                to_address: recipient,
                amount: coins,
            }));
        },

        // if the packet msg is "instantiate", translate it to a `WasmMsg::Instantiate`
        IcsMsg::Instantiate {
            code_id,
            msg,
            label,
            admin,
        } => WasmMsg::Instantiate {
            admin,
            code_id,
            msg,
            funds: coins,
        },

        // same with execute and migrate
        IcsMsg::Execute {
            contract_addr,
            msg,
        } => WasmMsg::Execute {
            contract_addr,
            msg,
            funds: coins,
        },

        IcsMsg::Migrate {
            contract_addr,
            new_code_id,
            msg,
        } => {
            if !coin.is_empty() {
                return Err(StdError::generic_err(
                    "contract migration does not support funds",
                ));
            }
            WasmMsg::Migrate {
                contract_addr,
                new_code_id,
                msg,
            }
        },
    }

    // send the translated WasmMsg to the ICA
    Ok(Response::new().add_message(WasmMsg::Execute {
        contract_addr: ica_addr.to_string(),
        msg: to_binary(&wasm_msg)?,
        funds: coins,
    }))
}

Summary

As we see, this standard combines both ICS-20 and ICS-27, allowing transferring multiple tokens to an account on another chain, then (optionally) instantiate/execute/migrate a wasm contract with those tokens.

Compared to the Osmosis module:

  • individual ICA address for each sender
  • supports instantiate and migrate in addition to execute
  • supports multiple coins in one packet
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment