ICS20

Cross-chain token transfers via IBC (Inter-Blockchain Communication) protocol

Overview

The ICS20 precompile provides an interface to the Inter-Blockchain Communication (IBC) protocol, allowing smart contracts to perform cross-chain token transfers. It enables sending tokens to other IBC-enabled chains and querying information about IBC denominations.

Precompile Address: 0x0000000000000000000000000000000000000802

Gas Costs

Gas costs are approximated and may vary based on the transfer complexity and chain settings.

Method
Gas Cost

Transfer

50,000 + (100 × memo length)

Queries

1000 + (3 × bytes of input)

Channel Validation

Before initiating a transfer, ensure that:

  • The source channel exists and is in an OPEN state

  • The channel is connected to the intended destination chain

  • The port ID matches the expected value (typically "transfer")

You can verify channel status using the IBC module queries or chain explorers.

Timeout Mechanism

IBC transfers include two timeout options to prevent tokens from being locked indefinitely:

  1. Height Timeout: Specified as {revisionNumber, revisionHeight}. Set both to 0 to disable.

  2. Timestamp Timeout: Unix timestamp in nanoseconds. Set to 0 to disable.

At least one timeout mechanism must be set. Recommended practice is to use timestamp timeout set to 1 hour from the current time.

Address Format Requirements

**Current Limitation**: Receiver addresses must be in bech32 format (e.g., `Ontomir1...`). Hex addresses (e.g., `0x...`) are not currently supported for the receiver parameter, though this limitation will be removed in a future release.

The sender parameter is automatically converted from hex to bech32 format internally.

EVM Callbacks Support

The ICS20 precompile supports EVM callbacks through the memo field, enabling smart contracts to:

  • Execute automatically when receiving cross-chain transfers

  • Handle acknowledgments and timeouts for sent transfers

  • Implement complex cross-chain contract interactions

For detailed callback implementation, see [IBC Module](/docs/evm/next/documentation/Ontomir-sdk/modules/ibc) and [Callbacks Interface](/docs/evm/next/documentation/smart-contracts/precompiles/callbacks).

Transaction Methods

transfer

Initiates a cross-chain token transfer using the IBC protocol.

**Receiver Address Format**: Currently only accepts bech32 addresses (e.g., `Ontomir1...`) for the receiver parameter. Hex address support (e.g., `0x...`) will be added in a future release. ```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ICS20Example { address constant ICS20_PRECOMPILE = 0x0000000000000000000000000000000000000802; struct Height { uint64 revisionNumber; uint64 revisionHeight; } event IBCTransferInitiated( address indexed sender, string indexed receiver, string sourceChannel, string denom, uint256 amount, uint64 sequence ); function transfer( string calldata sourceChannel, string calldata denom, uint256 amount, string calldata receiver, uint64 timeoutTimestamp, string calldata memo ) external payable returns (uint64 sequence) { require(bytes(sourceChannel).length > 0, "Source channel required"); require(bytes(denom).length > 0, "Denom required"); require(amount > 0, "Amount must be greater than 0"); require(bytes(receiver).length > 0, "Receiver address required"); require(timeoutTimestamp > 0, "Timeout timestamp required"); // Default port for ICS20 transfers string memory sourcePort = "transfer"; // Disable height-based timeout (using timestamp instead) Height memory timeoutHeight = Height({ revisionNumber: 0, revisionHeight: 0 }); (bool success, bytes memory result) = ICS20_PRECOMPILE.call{value: msg.value}( abi.encodeWithSignature( "transfer(string,string,string,uint256,address,string,tuple(uint64,uint64),uint64,string)", sourcePort, sourceChannel, denom, amount, msg.sender, receiver, timeoutHeight, timeoutTimestamp, memo ) ); require(success, "IBC transfer failed"); sequence = abi.decode(result, (uint64)); emit IBCTransferInitiated(msg.sender, receiver, sourceChannel, denom, amount, sequence); return sequence; } // Helper function to calculate timeout (1 hour from now) function calculateTimeout(uint256 durationSeconds) external view returns (uint64) { return uint64((block.timestamp + durationSeconds) * 1e9); // Convert to nanoseconds } // Quick transfer with 1-hour timeout function quickTransfer( string calldata sourceChannel, string calldata denom, uint256 amount, string calldata receiver ) external payable returns (uint64) { uint64 timeoutTimestamp = this.calculateTimeout(3600); // 1 hour return this.transfer{value: msg.value}( sourceChannel, denom, amount, receiver, timeoutTimestamp, "" ); }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

// ABI definition for the transfer function
const precompileAbi = [
  "function transfer(string memory sourcePort, string memory sourceChannel, string memory denom, uint256 amount, address sender, string memory receiver, tuple(uint64 revisionNumber, uint64 revisionHeight) timeoutHeight, uint64 timeoutTimestamp, string memory memo) returns (uint64)"
];

// Provider and contract setup
const provider = new ethers.JsonRpcProvider("<RPC_URL>");
const precompileAddress = "0x0000000000000000000000000000000000000802";
const signer = new ethers.Wallet("<PRIVATE_KEY>", provider);
const contract = new ethers.Contract(precompileAddress, precompileAbi, signer);

// Transfer parameters
const sourcePort = "transfer";
const sourceChannel = "channel-0";
const denom = "test"; // Token denomination
const amount = ethers.parseEther("1.0"); // Amount to transfer
const sender = await signer.getAddress();
const receiver = "Ontomir1..."; // Destination address on target chain
const timeoutHeight = { revisionNumber: 0, revisionHeight: 0 }; // Height timeout disabled
const timeoutTimestamp = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
const memo = "";

async function transferTokens() {
  try {
    const tx = await contract.transfer(
      sourcePort,
      sourceChannel, 
      denom,
      amount,
      sender,
      receiver,
      timeoutHeight,
      timeoutTimestamp,
      memo
    );

    console.log("Transfer initiated:", tx.hash);
    await tx.wait();
    console.log("Transfer confirmed");
  } catch (error) {
    console.error("Error initiating transfer:", error);
  }
}

// transferTokens();
# Note: cURL cannot be used for transaction methods as they require signatures
# Use the ethers.js example above for IBC transfers
echo "IBC transfer requires a signed transaction - use ethers.js or other Web3 library"

Query Methods

denom

Queries denomination information for an IBC token by its hash.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ICS20DenomQuery { address constant ICS20_PRECOMPILE = 0x0000000000000000000000000000000000000802; struct Hop { string portId; string channelId; } struct Denom { string base; Hop[] trace; } function getDenom(string memory hash) external view returns (Denom memory denom) { require(bytes(hash).length > 0, "Hash cannot be empty"); (bool success, bytes memory result) = ICS20_PRECOMPILE.staticcall( abi.encodeWithSignature("denom(string)", hash) ); require(success, "Failed to get denom"); denom = abi.decode(result, (Denom)); return denom; } // Helper function to check if a denom is native (no trace) function isNativeDenom(string memory hash) external view returns (bool) { Denom memory denom = this.getDenom(hash); return denom.trace.length == 0; } // Helper function to get the base denomination function getBaseDenom(string memory hash) external view returns (string memory) { Denom memory denom = this.getDenom(hash); return denom.base; }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

// ABI definition for the function
const precompileAbi = [
  "function denom(string memory hash) view returns (tuple(string base, tuple(string portId, string channelId)[] trace) denom)"
];

// Provider and contract setup
const provider = new ethers.JsonRpcProvider("<RPC_URL>");
const precompileAddress = "0x0000000000000000000000000000000000000802";
const contract = new ethers.Contract(precompileAddress, precompileAbi, provider);

// Input: The hash of the denomination to query
const denomHash = "ibc/..."; // Placeholder for actual denomination hash

async function getDenom() {
  try {
    const denom = await contract.denom(denomHash);
    console.log("Denomination Info:", JSON.stringify(denom, null, 2));
  } catch (error) {
    console.error("Error fetching denomination:", error);
  }
}

getDenom();
# Note: Replace <RPC_URL> and the placeholder hash with your actual data.
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "0x0000000000000000000000000000000000000802",
            "data": "0x7780092400000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

denoms

Retrieves a paginated list of all denomination traces registered on the chain.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ICS20DenomsList { address constant ICS20_PRECOMPILE = 0x0000000000000000000000000000000000000802; struct PageRequest { bytes key; uint64 offset; uint64 limit; bool countTotal; bool reverse; } struct PageResponse { bytes nextKey; uint64 total; } struct Hop { string portId; string channelId; } struct Denom { string base; Hop[] trace; } function getDenoms(PageRequest memory pageRequest) external view returns (Denom[] memory denoms, PageResponse memory pageResponse) { (bool success, bytes memory result) = ICS20_PRECOMPILE.staticcall( abi.encodeWithSignature( "denoms((bytes,uint64,uint64,bool,bool))", pageRequest ) ); require(success, "Failed to get denoms"); (denoms, pageResponse) = abi.decode(result, (Denom[], PageResponse)); return (denoms, pageResponse); } // Helper function to get all IBC denoms (with trace) function getIBCDenoms(uint64 limit) external view returns (Denom[] memory) { PageRequest memory pageRequest = PageRequest({ key: "", offset: 0, limit: limit, countTotal: false, reverse: false }); (Denom[] memory allDenoms,) = this.getDenoms(pageRequest); // Count IBC denoms (those with trace) uint256 ibcCount = 0; for (uint i = 0; i < allDenoms.length; i++) { if (allDenoms[i].trace.length > 0) { ibcCount++; } } // Filter IBC denoms Denom[] memory ibcDenoms = new Denom; uint256 index = 0; for (uint i = 0; i < allDenoms.length; i++) { if (allDenoms[i].trace.length > 0) { ibcDenoms[index++] = allDenoms[i]; } } return ibcDenoms; }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

// ABI definition for the function
const precompileAbi = [
  "function denoms(tuple(bytes key, uint64 offset, uint64 limit, bool countTotal, bool reverse) pageRequest) view returns (tuple(string base, tuple(string portId, string channelId)[] trace)[] denoms, tuple(bytes nextKey, uint64 total) pageResponse)"
];

// Provider and contract setup
const provider = new ethers.JsonRpcProvider("<RPC_URL>");
const precompileAddress = "0x0000000000000000000000000000000000000802";
const contract = new ethers.Contract(precompileAddress, precompileAbi, provider);

// Input for pagination
const pagination = {
  key: "0x",
  offset: 0,
  limit: 10,
  countTotal: true,
  reverse: false,
};

async function getDenoms() {
  try {
    const result = await contract.denoms(pagination);
    console.log("Denominations:", JSON.stringify(result.denoms, null, 2));
    console.log("Pagination Response:", result.pageResponse);
  } catch (error) {
    console.error("Error fetching denominations:", error);
  }
}

getDenoms();
# Note: Replace <RPC_URL> with your actual RPC endpoint.
# This example queries for the first 10 denominations.
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "0x0000000000000000000000000000000000000802",
            "data": "0x5310b39500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

denomHash

Computes the hash of a denomination trace path.

```solidity Solidity expandable lines // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;

contract ICS20DenomHash { address constant ICS20_PRECOMPILE = 0x0000000000000000000000000000000000000802; function getDenomHash(string memory trace) external view returns (string memory hash) { require(bytes(trace).length > 0, "Trace cannot be empty"); (bool success, bytes memory result) = ICS20_PRECOMPILE.staticcall( abi.encodeWithSignature("denomHash(string)", trace) ); require(success, "Failed to compute denom hash"); hash = abi.decode(result, (string)); return hash; } // Helper function to build and hash a trace path function buildAndHashTrace( string memory portId, string memory channelId, string memory baseDenom ) external view returns (string memory) { // Build trace in format: "port/channel/denom" string memory trace = string(abi.encodePacked( portId, "/", channelId, "/", baseDenom )); return this.getDenomHash(trace); } // Helper function to verify if a hash matches a trace function verifyDenomHash( string memory trace, string memory expectedHash ) external view returns (bool) { string memory actualHash = this.getDenomHash(trace); return keccak256(bytes(actualHash)) == keccak256(bytes(expectedHash)); }

}


```javascript Ethers.js expandable lines
import { ethers } from "ethers";

// ABI definition for the function
const precompileAbi = [
  "function denomHash(string memory trace) view returns (string memory hash)"
];

// Provider and contract setup
const provider = new ethers.JsonRpcProvider("<RPC_URL>");
const precompileAddress = "0x0000000000000000000000000000000000000802";
const contract = new ethers.Contract(precompileAddress, precompileAbi, provider);

// Input: The trace path to hash
const tracePath = "transfer/channel-0/test"; // Placeholder for actual trace path

async function getDenomHash() {
  try {
    const hash = await contract.denomHash(tracePath);
    console.log("Denomination Hash:", hash);
  } catch (error) {
    console.error("Error computing denomination hash:", error);
  }
}

getDenomHash();
# Note: Replace <RPC_URL> and the placeholder trace with your actual data.
curl -X POST --data '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        {
            "to": "0x0000000000000000000000000000000000000802",
            "data": "0x82ff49f000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"
        },
        "latest"
    ],
    "id": 1
}' -H "Content-Type: application/json" <RPC_URL>

Full Solidity Interface & ABI

// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.18;

import "../common/Types.sol";

/// @dev The ICS20I contract's address.
address constant ICS20_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000802;

/// @dev The ICS20 contract's instance.
ICS20I constant ICS20_CONTRACT = ICS20I(ICS20_PRECOMPILE_ADDRESS);

/// @dev Denom contains the base denomination for ICS20 fungible tokens and the
/// source tracing information path.
struct Denom {
    /// base denomination of the relayed fungible token.
    string base;
    /// trace contains a list of hops for multi-hop transfers.
    Hop[] trace;
}

/// @dev Hop defines a port ID, channel ID pair specifying where
/// tokens must be forwarded next in a multi-hop transfer.
struct Hop {
    string portId;
    string channelId;
}

/// @author Evmos Team
/// @title ICS20 Transfer Precompiled Contract
/// @dev The interface through which solidity contracts will interact with IBC Transfer (ICS20)
/// @custom:address 0x0000000000000000000000000000000000000802
interface ICS20I {
    /// @dev Emitted when an ICS-20 transfer is executed.
    /// @param sender The address of the sender.
    /// @param receiver The address of the receiver.
    /// @param sourcePort The source port of the IBC transaction, For v2 packets, leave it empty.
    /// @param sourceChannel The source channel of the IBC transaction, For v2 packets, set the client ID.
    /// @param denom The denomination of the tokens transferred.
    /// @param amount The amount of tokens transferred.
    /// @param memo The IBC transaction memo.
    event IBCTransfer(
        address indexed sender,
        string indexed receiver,
        string sourcePort,
        string sourceChannel,
        string denom,
        uint256 amount,
        string memo
    );

    /// @dev Transfer defines a method for performing an IBC transfer.
    /// @param sourcePort the port on which the packet will be sent
    /// @param sourceChannel the channel by which the packet will be sent
    /// @param denom the denomination of the Coin to be transferred to the receiver
    /// @param amount the amount of the Coin to be transferred to the receiver
    /// @param sender the hex address of the sender
    /// @param receiver the bech32 address of the receiver (hex addresses not yet supported)
    /// @param timeoutHeight the timeout height relative to the current block height.
    /// The timeout is disabled when set to 0
    /// @param timeoutTimestamp the timeout timestamp in absolute nanoseconds since unix epoch.
    /// The timeout is disabled when set to 0
    /// @param memo optional memo
    /// @return nextSequence sequence number of the transfer packet sent
    function transfer(
        string memory sourcePort,
        string memory sourceChannel,
        string memory denom,
        uint256 amount,
        address sender,
        string memory receiver,
        Height memory timeoutHeight,
        uint64 timeoutTimestamp,
        string memory memo
    ) external returns (uint64 nextSequence);

    /// @dev denoms Defines a method for returning all denoms.
    /// @param pageRequest Defines the pagination parameters to for the request.
    function denoms(
        PageRequest memory pageRequest
    )
        external
        view
        returns (
            Denom[] memory denoms,
            PageResponse memory pageResponse
        );

    /// @dev Denom defines a method for returning a denom.
    function denom(
        string memory hash
    ) external view returns (Denom memory denom);

    /// @dev DenomHash defines a method for returning a hash of the denomination info.
    function denomHash(
        string memory trace
    ) external view returns (string memory hash);
}
{
  "_format": "hh-sol-artifact-1",
  "contractName": "ICS20I",
  "sourceName": "solidity/precompiles/ics20/ICS20I.sol",
  "abi": [
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "sender",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "string",
          "name": "receiver",
          "type": "string"
        },
        {
          "indexed": false,
          "internalType": "string",
          "name": "sourcePort",
          "type": "string"
        },
        {
          "indexed": false,
          "internalType": "string",
          "name": "sourceChannel",
          "type": "string"
        },
        {
          "indexed": false,
          "internalType": "string",
          "name": "denom",
          "type": "string"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "string",
          "name": "memo",
          "type": "string"
        }
      ],
      "name": "IBCTransfer",
      "type": "event"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "hash",
          "type": "string"
        }
      ],
      "name": "denom",
      "outputs": [
        {
          "components": [
            {
              "internalType": "string",
              "name": "base",
              "type": "string"
            },
            {
              "components": [
                {
                  "internalType": "string",
                  "name": "portId",
                  "type": "string"
                },
                {
                  "internalType": "string",
                  "name": "channelId",
                  "type": "string"
                }
              ],
              "internalType": "struct Hop[]",
              "name": "trace",
              "type": "tuple[]"
            }
          ],
          "internalType": "struct Denom",
          "name": "denom",
          "type": "tuple"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "trace",
          "type": "string"
        }
      ],
      "name": "denomHash",
      "outputs": [
        {
          "internalType": "string",
          "name": "hash",
          "type": "string"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "components": [
            {
              "internalType": "bytes",
              "name": "key",
              "type": "bytes"
            },
            {
              "internalType": "uint64",
              "name": "offset",
              "type": "uint64"
            },
            {
              "internalType": "uint64",
              "name": "limit",
              "type": "uint64"
            },
            {
              "internalType": "bool",
              "name": "countTotal",
              "type": "bool"
            },
            {
              "internalType": "bool",
              "name": "reverse",
              "type": "bool"
            }
          ],
          "internalType": "struct PageRequest",
          "name": "pageRequest",
          "type": "tuple"
        }
      ],
      "name": "denoms",
      "outputs": [
        {
          "components": [
            {
              "internalType": "string",
              "name": "base",
              "type": "string"
            },
            {
              "components": [
                {
                  "internalType": "string",
                  "name": "portId",
                  "type": "string"
                },
                {
                  "internalType": "string",
                  "name": "channelId",
                  "type": "string"
                }
              ],
              "internalType": "struct Hop[]",
              "name": "trace",
              "type": "tuple[]"
            }
          ],
          "internalType": "struct Denom[]",
          "name": "denoms",
          "type": "tuple[]"
        },
        {
          "components": [
            {
              "internalType": "bytes",
              "name": "nextKey",
              "type": "bytes"
            },
            {
              "internalType": "uint64",
              "name": "total",
              "type": "uint64"
            }
          ],
          "internalType": "struct PageResponse",
          "name": "pageResponse",
          "type": "tuple"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "sourcePort",
          "type": "string"
        },
        {
          "internalType": "string",
          "name": "sourceChannel",
          "type": "string"
        },
        {
          "internalType": "string",
          "name": "denom",
          "type": "string"
        },
        {
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        },
        {
          "internalType": "address",
          "name": "sender",
          "type": "address"
        },
        {
          "internalType": "string",
          "name": "receiver",
          "type": "string"
        },
        {
          "components": [
            {
              "internalType": "uint64",
              "name": "revisionNumber",
              "type": "uint64"
            },
            {
              "internalType": "uint64",
              "name": "revisionHeight",
              "type": "uint64"
            }
          ],
          "internalType": "struct Height",
          "name": "timeoutHeight",
          "type": "tuple"
        },
        {
          "internalType": "uint64",
          "name": "timeoutTimestamp",
          "type": "uint64"
        },
        {
          "internalType": "string",
          "name": "memo",
          "type": "string"
        }
      ],
      "name": "transfer",
      "outputs": [
        {
          "internalType": "uint64",
          "name": "nextSequence",
          "type": "uint64"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ]
}