Skip to content

Module 9 - Building an OmniChain Token on Router Chain

Cloning the Repository

For Understanding how to Write a Smart Contract for an OmniChain Token on Router Chain, Clone the Below Repository in your Local Machine -

Terminal window
git clone https://github.com/ShivankK26/OmniChain-Crypto-Token .

This will Clone the OmniChain Token Contract in the Current Directory.

File Structure

The cosmwasm Directory consists of Smart Contracts written on Router Chain and evm Directory consists of the Smart Contracts written on Polygon and Arbitrum.

  • Directorycosmwasm/
    • Directory.cargo/
      • config
    • Directorycontracts/
      • Directoryxerc20/
        • Directoryexamples/
          • schema.rs
        • Directorysrc/
          • contract.rs
          • execution.rs
          • handle_sudo_execution.rs
          • lib.rs
          • modifiers.rs
          • query.rs
          • state.rs
          • tests.rs
        • Cargo.toml
    • Directorypackages/
      • Directorynew-crosstalk-sample/
        • Directorysrc/
          • lib.rs
          • xerc20.rs
        • Cargo.toml
    • Directoryscripts/
      • build.sh
    • .editorconfig
    • .gitignore
    • Cargo.lock
    • Cargo.toml
    • rustfmt.toml
  • Directoryevm/
    • Directorycontracts/
      • XERC20.sol
    • Directorydeployment/
      • Directoryconfig/
        • xerc20.json
      • deployments.json
    • Directorytasks/
      • Directorydeploy/
        • DeployOnEachChain.ts
        • XERC20.ts
      • enroll_added_chain.ts
      • enroll_on_chain.ts
      • index.ts
      • storeDeployments.ts
    • Directorytest/
      • PingPong.ts
    • Directoryutils/
      • chain.ts
      • onEachChain.ts
      • types.ts
      • utils.ts
    • .gitignore
    • hardhat.config.ts
    • package-lock.json
    • package.json
    • README.md
    • tsconfig.json
    • yarn.lock

CosmWasm-side

Contract.rs

Imports and Constants

use crate::execution::handle_execute;
use crate::handle_sudo_execution::{handle_sudo_ack, handle_sudo_request};
use crate::query::handle_query;
use crate::state::{CREATE_I_SEND_REQUEST, CROSS_CHAIN_TOKEN, INSTANTIATE_REPLY_ID, OWNER};
// ... (other imports)
const CONTRACT_NAME: &str = "XERC20";
const CONTRACT_VERSION: &str = "0.1.1";

This Section Imports necessary Modules and Defines Constants for the Contract Name and Version. It uses various CosmWasm Libraries and Router Protocol Bindings.

Instantiate Function

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut<RouterQuery>,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
OWNER.save(deps.storage, &info.sender.to_string())?;
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::new().add_submessage(SubMsg {
msg: WasmMsg::Instantiate { // This creates a new contract instance
// The deployer of the parent contract becomes the admin of the token contract
admin: Some(info.sender.to_string()),
// The code_id references the pre-deployed CW20 contract code on chain
code_id: msg.cw20_code_id,
// Token configuration parameters
msg: to_binary(&TokenInstantiateMsg {
name: msg.token_name, // Token name from instantiate message
symbol: msg.token_symbol, // Token symbol from instantiate message
decimals: 18, // Standard 18 decimals
initial_balances: vec![], // No initial token distribution
mint: Some(MinterResponse { // Configure minting privileges
// This contract gets minting rights
minter: env.contract.address.to_string(),
cap: None, // No maximum supply cap
}),
marketing: None, // No marketing info
})?,
funds: vec![], // No funds sent with instantiation
label: "XERC20 TOKEN".to_string(), // Label for the new contract
}
.into(),
gas_limit: None, // No specific gas limit
id: INSTANTIATE_REPLY_ID, // ID to identify this message in the reply
reply_on: ReplyOn::Success, // Only trigger reply on successful execution
}))
}

The instantiate function is called when the Contract is first Deployed. It -

  1. Saves the Contract Owner.
  2. Sets the Contract Version.
  3. Creates a new CW20 Token as the underlying Token for Omnichain functionality.
  4. Response::new() - Creates a new empty Response.
  5. add_submessage() - Adds a Submessage to be executed after the Current Transaction.
  6. SubMsg - A Submessage that can Trigger a reply.
  7. WasmMsg::Instantiate - Message Type for Creating a new Contract Instance.

The CW20 Token is Created via a Submessage, allowing for Asynchronous handling of the Token Creation. The Contract will receive a Reply when this Succeeds (via INSTANTIATE_REPLY_ID).

Reply Function

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(
deps: DepsMut<RouterQuery>, // Provides mutable access to storage, API, and Router-specific queries
_env: Env, // Environment info (unused, hence underscore)
msg: Reply // Contains the reply data from submessage execution
) -> StdResult<Response<RouterMsg>> { // Returns a Response that can contain Router-specific messages
// Handle different reply scenarios based on their ID
match msg.id {
// Handle reply from CW20 token instantiation
INSTANTIATE_REPLY_ID => {
// Parse the instantiation response to get new contract's information
let response = parse_reply_instantiate_data(msg).unwrap();
// Store the new token's contract address in permanent storage
CROSS_CHAIN_TOKEN.save(deps.storage, &response.contract_address)?;
// Return success response with the token address as an attribute for event logging
return Ok(
Response::new().add_attribute("cw20token", response.contract_address.clone())
);
}
// Handle reply from cross-chain send request
CREATE_I_SEND_REQUEST => {
// Log the reply ID for debugging
deps.api.debug(&msg.id.to_string());
// Create empty response object for Router messages
let response: Response<RouterMsg> = Response::new();
// Handle the result of the submessage
match msg.result {
// If submessage was successful
SubMsgResult::Ok(msg_result) => match msg_result.data {
// If the successful result contains data
Some(binary_data) => {
deps.api.debug("Binary Data Found");
// Parse the binary data into a CrosschainRequestResponse
let cross_chain_req_res: CrosschainRequestResponse =
from_binary(&binary_data).unwrap();
// Format and log debug information about the response
let info_str: String = format!(
"Binary data {:?}, response {:?}",
&binary_data.to_string(),
cross_chain_req_res
);
deps.api.debug(&info_str);
// Return empty success response
return Ok(response);
}
// If no data was included in the successful result
None => deps.api.debug("No Binary Data Found"),
},
// If submessage execution failed
SubMsgResult::Err(err) => deps.api.debug(&err.to_string()),
}
}
// Handle any unknown reply IDs
id => return Err(StdError::generic_err(format!("Unknown reply id: {}", id))),
}
// Return empty success response if no other return occurred
Ok(Response::new())
}
  • The Reply function handles Responses from Submessages. It has Two Main Cases -

    1. INSTANTIATE_REPLY_ID - Handles the Reply from CW20 Token Creation, Saving the new Token’s Address.
    2. CREATE_I_SEND_REQUEST - Handles Replies from cross-chain Send Requests, including Logging Debug Information.
  • Key features -

    1. It’s an entry point for handling submessage replies.
    2. Includes Extensive Debug Logging for Development Purposes.
    3. Uses the Router’s Specific Query and Message Types (RouterQuery, RouterMsg).
    4. Has a TODO Note about nonce handling which suggests Incomplete cross-chain functionality.
    5. Returns error for Unknown Reply IDs to prevent Unhandled Cases.

Execute Function

// Entry point attribute - marks this as an entry point when not compiled as a library
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
// Dependencies including storage access and Router-specific queries
deps: DepsMut<RouterQuery>,
// Blockchain environment information (block height, time, contract address etc)
env: Env,
// Information about the message sender and funds sent with the transaction
info: MessageInfo,
// The actual execute message containing the action to perform
msg: ExecuteMsg,
// Returns either a success Response with Router messages or an error
) -> StdResult<Response<RouterMsg>> {
// Delegates all execution handling to a separate function
// This pattern is common for better code organization and testing
handle_execute(deps, env, info, msg)
}
  • This is a Standard CosmWasm execute entry Point. It acts as a Thin Wrapper around handle_execute. It uses Router-specific Types (RouterQuery, RouterMsg) Suggesting cross-chain functionality.
  • This function is the entry point for all Execute Messages sent to the Contract. It Delegates the actual handling to a Separate handle_execute function.

Sudo Function

// Entry point attribute - marks this as a privileged sudo entry point
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn sudo(
// Dependencies for storage access and Router-specific queries
deps: DepsMut<RouterQuery>,
// Blockchain environment information
env: Env,
// Special privileged message type that can only be called by the chain's sudo authority
msg: SudoMsg,
) -> StdResult<Response<RouterMsg>> {
// Match on the type of sudo message received
match msg {
// Handle incoming cross-chain requests
SudoMsg::HandleIReceive {
// Address that sent the cross-chain request
request_sender,
// ID of the chain where the request originated
src_chain_id,
// Unique identifier for this cross-chain request
request_identifier,
// Actual data/payload sent in the cross-chain request
payload,
} => handle_sudo_request(
deps,
env,
request_sender,
src_chain_id,
request_identifier,
payload,
),
// Handle acknowledgments of cross-chain requests
SudoMsg::HandleIAck {
// Identifier of the original request being acknowledged
request_identifier,
// Flag indicating if the cross-chain execution was successful
exec_flag,
// Data resulting from the cross-chain execution
exec_data,
// Amount to be refunded if the operation failed or wasn't fully used
refund_amount,
} => handle_sudo_ack(
deps,
env,
request_identifier,
exec_flag,
exec_data,
refund_amount,
),
}
}
  • The Sudo function handles Privileged Operations, typically Called by the Router Protocol Infrastructure. It has Two Main Operations -

    1. HandleIReceive - Processes Incoming cross-chain Requests. It processes - who sent the Request, Which Chain it came from, A Unique Identifier, and the actual Payload Data.
    2. HandleIAck - Handles Acknowledgments for Outgoing cross-chain Requests. It processes - The Request being Acknowledged, whether it succeeded, any returned Data, and any refund Amount.
  • It Delegates actual handling to Two Separate functions -

    1. handle_sudo_request for incoming requests.
    2. handle_sudo_ack for acknowledgments.

Migrate Function

// Entry point attribute - marks this as a migration entry point
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(
// Dependencies for storage access and Router-specific queries
deps: DepsMut<RouterQuery>,
// Blockchain environment information
env: Env,
// Migration message (unused, hence underscore)
_msg: MigrateMsg,
) -> StdResult<Response> {
// Get the current version information of the contract from storage
let ver = cw2::get_contract_version(deps.storage)?;
// Verify that we're migrating the correct contract type
// This prevents accidentally migrating from a different contract
if ver.contract != CONTRACT_NAME.to_string() {
return Err(StdError::generic_err("Can only upgrade from same type").into());
}
// NOTE: Version check is commented out but would prevent upgrading from newer versions
// Proper semver comparison would be better than string comparison
// if ver.version >= CONTRACT_VERSION.to_string() {
// return Err(StdError::generic_err("Cannot upgrade from a newer version").into());
// }
// Log migration information for debugging
let info_str: String = format!(
"migrating contract: {}, new_contract_version: {}, contract_name: {}",
env.contract.address, // Current contract's address
CONTRACT_VERSION.to_string(), // New version being upgraded to
CONTRACT_NAME.to_string() // Name of the contract
);
deps.api.debug(&info_str);
// Update the contract version in storage to the new version
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// Return empty successful response
Ok(Response::default())
}

This function allows for Upgrading the Contract. It Checks that the Migration is from the Correct Contract Type and Logs Information about the Migration.

Query Function

// Entry point attribute - marks this as a query entry point when not compiled as a library
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(
// Read-only dependencies for storage access and Router-specific queries
// Note: Uses Deps instead of DepsMut since queries are read-only
deps: Deps<RouterQuery>,
// Blockchain environment information (block height, time, contract address etc)
env: Env,
// The query message containing what information is being requested
msg: QueryMsg,
// Returns a binary-encoded response that can be decoded by the caller
) -> StdResult<Binary> {
// Delegates actual query handling to a separate function
// This pattern is common for better code organization and testing
handle_query(deps, env, msg)
}

This function handles all Query Requests to the Contract, Delegating to a Separate handle_query function.

This Contract serves as the Main Entry Point for the Omnichain Token, Coordinating between Local Token Operations (via the CW20 token) and cross-chain Operations (via Router Protocol).

execution.rs

This is an Implementation of a cross-chain (omnichain) Token System that allows Tokens to move Between different Blockchains. Think of it as a Bridge that Connects Multiple Blockchain Networks, allowing Users to send Tokens from one Chain to another.

Imports and Constants

use cosmwasm_std::{
to_binary, Binary, CosmosMsg, DepsMut, Env, Event, MessageInfo, ReplyOn, Response, StdResult,
SubMsg, Uint128, WasmMsg,
};
use new_crosstalk_sample::xerc20::{ChainTypeInfo, ContractInfo, ExecuteMsg};
use router_wasm_bindings::{
ethabi::{encode, ethereum_types::U256, Token},
types::{AckType, RequestMetaData},
Bytes, RouterMsg, RouterQuery,
};
use crate::{
modifiers::is_owner_modifier,
query::{fetch_oracle_gas_price, fetch_white_listed_contract},
state::{
CHAIN_ID, CHAIN_TYPE_MAPPING, CREATE_I_SEND_REQUEST, CROSS_CHAIN_TOKEN, OWNER,
WHITELISTED_CONTRACT_MAPPING,
},
};
  • These are the Core Types and functions from cosmwasm_std, which is the main Library for Building CosmWasm Smart Contracts.
  • These Imports come from the Router Protocol Bindings and the new_crosstalk_sample Contract.

Execute Handler (Main Entry Point)

pub fn handle_execute(
deps: DepsMut<RouterQuery>,
env: Env,
info: MessageInfo,
msg: ExecuteMsg
) -> StdResult<Response<RouterMsg>> {
match msg {
ExecuteMsg::SetChainTypes { chain_type_info } => {
set_chain_types_info(deps, env, info, chain_type_info)
}
ExecuteMsg::UpdateOwner { new_owner } => update_owner(deps, &env, &info, new_owner),
ExecuteMsg::SetChainId { id } => set_chain_id(deps, env, info, id),
ExecuteMsg::SetXerc20Addr { addr } => set_xerc20_addr(deps, env, info, addr),
ExecuteMsg::Mint { recipient, amount } => mint(deps, &env, &info, recipient, amount),
ExecuteMsg::SetWhiteListedContracts { contracts } => {
set_white_listed_contracts(deps, &env, &info, contracts)
}
ExecuteMsg::TrasferCrossChain {
amount, recipient, dest_chain_id
} => transfer_cross_chain(deps, env, info, amount, recipient, dest_chain_id),
}
}

This is like a Traffic Controller that directs different Operations to their Specific Handlers. It Supports -

  • Administrative Operations (setting configurations)
  • Token Operations (minting, cross-chain transfers)
  • System Configuration (chain IDs, contract addresses)
  • This function Matches the Message (msg) with the different Operations, like setting chain types, updating ownership, minting, etc.
  • Each ExecuteMsg variant Calls a Different function to handle the Logic for that particular Operation.

Administrative Functions

// Owner Management
pub fn update_owner(
deps: DepsMut<RouterQuery>,
_env: &Env,
info: &MessageInfo,
new_owner: String,
) -> StdResult<Response<RouterMsg>> {
is_owner_modifier(deps.as_ref(), &info)?;
OWNER.save(deps.storage, &new_owner)?;
let res = Response::new();
Ok(res)
}

The Contract allows Changing the Owner, but Only the Current Owner can perform this Operation -

  • Controls who has Administrative Rights
  • Only Current Owner can Transfer Ownership
  • Critical for Security Governance
  • Ownership check - The Modifier is_owner_modifier ensures only the Current Owner can change Ownership.
  • Update state - The new Owner’s Address is saved in the Contract’s State.
// Whitelist Management
pub fn set_white_listed_contracts(
deps: DepsMut<RouterQuery>,
_env: &Env,
info: &MessageInfo,
contracts: Vec<ContractInfo>,
) -> StdResult<Response<RouterMsg>> {
is_owner_modifier(deps.as_ref(), &info)?;
for i in 0..contracts.len() { // iterates through `contracts` vector
WHITELISTED_CONTRACT_MAPPING.save( // `WHITELISTED_CONTRACT_MAPPING` is updated using the save function for each contract in the vector.
deps.storage, // save stores the contract_addr under the chain_id key in the contract’s storage.
&contracts[i].chain_id, // This ensures that each contract's address is mapped to its respective chain ID in the whitelist.
&contracts[i].contract_addr,
)?;
}
let res = Response::new().add_attribute("action", "SetCustodyContracts");
Ok(res)
}

The function set_white_listed_contracts is responsible for Setting a list of Whitelisted Contracts on different Chains in the Contract’s State. Only the Contract Owner is allowed to execute this Operation, which is enforced by the is_owner_modifier function.

  • This Line Calls the is_owner_modifier function to ensure that only the Contract Owner can execute this function.
  • add_attribute(“action”, “SetCustodyContracts”) - Adds a Custom Attribute to the Response, indicating that the action performed was the Setting of Custody Contracts (whitelisting contracts). This is Useful for Debugging, Logging, and Transaction Tracking.
// Chain Configuration
pub fn set_chain_types_info(
deps: DepsMut<RouterQuery>,
_env: Env,
info: MessageInfo,
chain_type_info: Vec<ChainTypeInfo>,
) -> StdResult<Response<RouterMsg>> {
is_owner_modifier(deps.as_ref(), &info)?;
for i in 0..chain_type_info.len() {
CHAIN_TYPE_MAPPING.save(
deps.storage,
&chain_type_info[i].chain_id,
&chain_type_info[i].chain_type,
)?;
}
let event_name = String::from("SetChainTypeInfo");
let set_chain_bytes_info_event = Event::new(event_name);
let res = Response::new()
.add_attribute("action", "SetChainTypeInfo")
.add_event(set_chain_bytes_info_event);
Ok(res)
}

When the SetChainTypes Message is received, the Contract Sets the Mapping for different Chain Types -

  • Purpose - Stores Information about Different Chain Types in the CHAIN_TYPE_MAPPING.
  • Ownership check - The function begins by Verifying that the Caller is the Contract Owner with is_owner_modifier.
  • Chain Type Mapping - It Loops over chain_type_info to Store each Chain ID and Type Pair.
  • Event - Emits an event SetChainTypeInfo for Tracking.
pub fn set_xerc20_addr(
deps: DepsMut<RouterQuery>,
_env: Env,
info: MessageInfo,
addr: String,
) -> StdResult<Response<RouterMsg>> {
is_owner_modifier(deps.as_ref(), &info)?;
CROSS_CHAIN_TOKEN.save(deps.storage, &addr)?;
let event_name: String = String::from("SetXERC20Addr");
let event: Event = Event::new(event_name);
let res = Response::new()
.add_attribute("action", "SetXERC20Addr")
.add_event(event);
Ok(res)
}

The function set_xerc20_addr is responsible for Setting the Address of a cross-chain Token (referred to as XERC20 in this case) in the Contract’s Storage. This function ensures that only the Contract Owner can Execute this Operation, which is enforced by the is_owner_modifier. Let’s go through each part of the function -

  • This Line Calls the is_owner_modifier function to ensure that the Sender (identified in info.sender) is the Owner of the Contract.
  • CROSS_CHAIN_TOKEN - This is a Constant or State Variable that refers to the Storage Location where the address of the cross-chain Token is saved. The save function Stores the provided value (the new XERC20 token address, addr) into the Contract’s Storage.
  • Event - CosmWasm Contracts can emit events that are useful for External Systems (like blockchain explorers) to Track specific actions Within the Contract. This Line Creates an event called SetXERC20Addr.
pub fn set_chain_id(
deps: DepsMut<RouterQuery>,
_env: Env,
info: MessageInfo,
id: String,
) -> StdResult<Response<RouterMsg>> {
is_owner_modifier(deps.as_ref(), &info)?; // This line checks whether the sender of the message (info.sender) is the contract owner using the `is_owner_modifier` function.
CHAIN_ID.save(deps.storage, &id)?;
let event_name: String = String::from("SetChainId"); // This part of the function creates a new event that will be logged in the transaction output.
let event: Event = Event::new(event_name); // `String::from("SetChainId")`: Creates a string with the event name `SetChainId`, indicating that the chain ID has been set.
let res = Response::new() // Response::new(): Creates a new response object that will be returned once the function execution is complete.
.add_attribute("action", "SetChainId") // `add_attribute("action", "SetChainId")`: Adds an attribute to the response to provide additional information about the action performed.
.add_event(event);
Ok(res)
}
  • CHAIN_ID is a variable in the Contract’s State that represents the ID of the Blockchain Network the Contract is working with.
  • save(deps.storage, &id) - This stores the new CHAIN_ID in the Contract’s Persistent Storage. deps.storage is used to access the Contract’s Storage, and id is the new Chain ID value Passed to the function.

Token Operations

// Minting Function
pub fn mint(
deps: DepsMut<RouterQuery>,
_env: &Env,
info: &MessageInfo,
recipient: String,
amount: Uint128,
) -> StdResult<Response<RouterMsg>> {
is_owner_modifier(deps.as_ref(), &info)?;
deps.api.addr_validate(&recipient)?;
let mint_msg = cw20_base::msg::ExecuteMsg::Mint { recipient, amount };
let xerc20_token: String = CROSS_CHAIN_TOKEN.load(deps.storage)?;
let exec_mint_msg: CosmosMsg<RouterMsg> = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: xerc20_token,
funds: vec![],
msg: to_binary(&mint_msg)?,
});
let res = Response::new().add_message(exec_mint_msg);
Ok(res)
}

This function Mints new Tokens to a specific Recipient Address -

  • OwnershipCheck - Only the Contract Owner can Mint new Tokens.
  • Recipient Validation - The Recipient’s address is Validated to ensure it’s a Valid address on-chain.
  • Mint Message - The Contract creates a Mint Message using the cw20_base standard for Minting Tokens.
  • Execute the Mint - It sends a Message to the Contract responsible for handling cross-chain Tokens (xerc20_token) to Mint Tokens for the Recipient.

Cross-Chain Transfer (Most Complex Operation)

pub fn transfer_cross_chain(
deps: DepsMut<RouterQuery>,
_env: Env,
info: MessageInfo,
amount: Uint128,
recipient: Binary,
dest_chain_id: String,
) -> StdResult<Response<RouterMsg>> {
let u256 = U256::from(amount.u128());
let payload: Vec<u8> = encode(&[Token::Bytes(recipient.0), Token::Uint(u256)]);
let burn_msg = cw20_base::msg::ExecuteMsg::BurnFrom {
owner: info.sender.to_string(),
amount,
};
let xerc20_token: String = CROSS_CHAIN_TOKEN.load(deps.storage)?;
let exec_burn_msg: CosmosMsg<RouterMsg> = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: xerc20_token,
funds: vec![],
msg: to_binary(&burn_msg)?,
});
// Cross-chain metadata and relay information
let chain_id = CHAIN_ID.load(deps.storage)?;
let ack_gas_price = fetch_oracle_gas_price(deps.as_ref(), chain_id)?;
let dest_gas_price = fetch_oracle_gas_price(deps.as_ref(), dest_chain_id.clone())?;
let dest_contract_address = fetch_white_listed_contract(deps.as_ref(), &dest_chain_id)?;
let request_metadata = RequestMetaData {
dest_gas_limit: 200_000,
dest_gas_price,
ack_gas_limit: 200_000,
ack_gas_price,
relayer_fee: Uint128::zero(),
ack_type: AckType::AckOnBoth,
is_read_call: false,
asm_address: String::default(),
};
let request_packet: Bytes = encode(&[
Token::String(dest_contract_address.clone()),
Token::Bytes(payload),
]);
let i_send_request: RouterMsg = RouterMsg::CrosschainCall {
version: 1,
route_amount: Uint128::zero(),
route_recipient: String::default(),
dest_chain_id,
request_metadata: request_metadata.get_abi_encoded_bytes(),
request_packet,
};
let cross_chain_sub_msg = SubMsg {
id: CREATE_I_SEND_REQUEST,
msg: i_send_request.into(),
gas_limit: None,
reply_on: ReplyOn::Success,
};
let res = Response::new()
.add_message(exec_burn_msg)
.add_submessage(cross_chain_sub_msg.into())
.add_attribute("dest_contract_address", dest_contract_address);
Ok(res)
}

This is the Core of the Omnichain functionality, enabling Token Transfers across Chains. Here’s what happens -

  • Burn Token Locally - Before Transferring Tokens cross-chain, the Amount is Burned from the Sender’s Balance using the BurnFrom message.
  • Prepare cross-chain Message - The Payload contains the Recipient Address and amount to Transfer, which is encoded.
  • Oracle Gas Prices - Fetches Gas Prices from an Oracle for the Source and Destination Chains.
  • Whitelisted Contract Check - Ensures that the Destination Chain’s Contract is Whitelisted before Proceeding with the Transfer.
  • RouterMsg - The RouterMsg::CrosschainCall sends the Transfer Request through Router Protocol to the Destination Chain, with Metadata like Gas Limits and acknowledgment Types.

handle_sudo_execution.rs

The handle_sudo_execution.rs file handles two Important Parts of the Omnichain Token Project -

  1. handle_sudo_request
  2. handle_sudo_ack
pub fn handle_sudo_request(
deps: DepsMut<RouterQuery>,
_env: Env,
request_sender: String,
src_chain_id: String,
request_identifier: u64,
payload: Binary,
) -> StdResult<Response<RouterMsg>> {
deps.api.debug("XERC20 INFO: Handle Sudo Request");
let src_chain_type: u64 = CHAIN_TYPE_MAPPING.load(deps.storage, &src_chain_id)?; // Retrieves the type of the source chain from the contract's storage (e.g., EVM, Cosmos, etc.).
let sender: String = match src_chain_type { // If the chain is of type 1 (likely an EVM chain), it converts the sender's address to lowercase.
1 => request_sender.to_lowercase(),
_ => request_sender.clone(),
};
is_white_listed_modifier(deps.as_ref(), &src_chain_id, &sender)?; // Verifies if the sender's contract on the source chain is whitelisted.
deps.api.debug("Request Coming from whitelisted Contract");
// bytes memory packet = abi.encode(recipient, amount);
let token_vec = match decode(&[ParamType::Bytes, ParamType::Uint(128)], &payload.0) { // Decodes the payload, which contains: recepient address, amount to mint.
Ok(data) => data,
Err(_) => {
return Err(StdError::GenericErr {
msg: String::from("err.into()"),
});
}
};
let u128_val: u128 = token_vec[1].clone().into_uint().unwrap().as_u128(); // Extracts the recipient address and the amount to mint.
let amount = Uint128::new(u128_val);
let addr: Bytes = token_vec[0].clone().into_bytes().unwrap();
let recipient = // Converts the recipient's address from bytes to a string using the Cosmos chain type.
convert_address_from_bytes_to_string(&addr, ChainType::ChainTypeCosmos.get_chain_code())?;
let info_str: String = format!("recipient {:?}, amount {:?}", recipient, amount);
deps.api.debug(&info_str);
deps.api.addr_validate(&recipient)?;
let mint_msg = cw20_base::msg::ExecuteMsg::Mint { recipient, amount }; // Creates a Mint message for the CW20 token contract to mint the specified amount of tokens to the recipient.
let xerc20_token: String = CROSS_CHAIN_TOKEN.load(deps.storage)?;
let exec_mint_msg: CosmosMsg<RouterMsg> = CosmosMsg::Wasm(WasmMsg::Execute { // Executes the mint operation by sending the mint_msg to the XERC20 contract. The xerc20_token address is retrieved from storage using CROSS_CHAIN_TOKEN.
contract_addr: xerc20_token,
funds: vec![],
msg: to_binary(&mint_msg)?,
});
let info_str: String = format!("exec_mint_token {:?}", exec_mint_msg);
deps.api.debug(&info_str);
let res: Response<RouterMsg> = Response::new() // The function constructs a response that includes the mint message and adds relevant attributes like the request sender, request identifier, and source chain ID.
.add_message(exec_mint_msg)
.add_attribute("sender", request_sender)
.add_attribute("request_identifier", request_identifier.to_string())
.add_attribute("src_chain_id", src_chain_id);
Ok(res)
}

This function is responsible for handling Incoming Sudo Requests, Verifying them, and Minting Tokens on the Target Chain for the Recipient.

pub fn handle_sudo_ack(
deps: DepsMut<RouterQuery>,
_env: Env,
request_identifier: u64,
exec_flag: bool,
exec_data: Binary,
_refund_amount: Coin,
) -> StdResult<Response<RouterMsg>> {
let info_str: String = format!( // Logs the execution status, including the request identifier and any execution data.
"handle_sudo_ack, request_identifier {:?}, exec_data {:?}",
request_identifier, exec_data
);
deps.api.debug(&info_str);
let event = Event::new("ExecutionStatus") // Creates an event named "ExecutionStatus" with attributes for the request identifier and execution flag.
.add_attribute("requestIdentifier", request_identifier.to_string())
.add_attribute("execFlag", exec_flag.to_string());
Ok(Response::new().add_event(event)) // Returns a response with the execution status event.
}

This function handles the Acknowledgment of a Sudo Request Execution, Logging whether the Execution was Successful.

lib.rs

Imports and Constants

pub mod contract;
pub mod execution;
pub mod handle_sudo_execution;
pub mod modifiers;
pub mod query;
pub mod state;
pub use serde::{Deserialize, Serialize};
#[cfg(test)]
mod tests;

The above Code Implementation is a Standard lib.rs file of a CosmWasm Project.

modifiers.rs

The file modifiers.rs Defines Two functions (is_owner_modifier and is_white_listed_modifier) that act as “modifiers” or Checks Before allowing Certain Operations to proceed. These functions help ensure that Only Authorized Users (the owner or whitelisted contracts) can Perform Specific Actions on the Contract.

Imports and Constants

use cosmwasm_std::{Deps, MessageInfo, StdError, StdResult};
use router_wasm_bindings::RouterQuery;
use crate::{query::is_white_listed_contract, state::OWNER};

In the above Code, We’re Implementing router_wasm_bindings package along with cosmwasm_std package.

pub fn is_owner_modifier(deps: Deps<RouterQuery>, info: &MessageInfo) -> StdResult<()> {
let owner: String = match OWNER.load(deps.storage) { // The function tries to load the owner's address from the contract's storage using OWNER.load(deps.storage).
Ok(owner) => owner,
Err(err) => return StdResult::Err(err),
};
if owner != info.sender { // info.sender holds the address of the person attempting to execute the function.
return StdResult::Err(StdError::GenericErr {
msg: String::from("Auth: Invalid Owner"),
});
}
Ok(())
}
  • The is_owner_modifier function Checks if the Person trying to execute a Specific function is the Contract Owner. If they are not, it returns an error, preventing further Execution of the function.
  • The Contract State has a Key called OWNER, which Stores the address of the Contract’s Owner.
pub fn is_white_listed_modifier(
deps: Deps,
chain_id: &str,
contract: &str,
) -> StdResult<()> {
// Check if the given contract on a specific chain is whitelisted
let is_white_listed_contract = is_white_listed_contract(deps, chain_id, contract);
// Log the chain_id and contract for debugging purposes
let info_str: String = format!("--chain_id: {:?}, contract: {:?}", chain_id, contract);
deps.api.debug(&info_str);
// If the contract is not whitelisted, return an error with details
if !is_white_listed_contract {
let info_str: String = format!(
"Auth: The Sender/Receiver contract is not whitelisted, chain_id: {:?}, contract: {:?}",
chain_id, contract
);
deps.api.debug(&info_str);
return StdResult::Err(StdError::GenericErr { msg: info_str });
}
// If the contract is whitelisted, return Ok (no error)
Ok(())
}
  • This function Checks if a Contract (from a specific chain) is Whitelisted, meaning it’s allowed to interact with this Contract.
  • is_owner_modifier - Ensures Only the Owner can Execute Certain functions. This is essential for Administrative Actions.
  • is_white_listed_modifier - Ensures only approved (whitelisted) Contracts on other Chains can Interact with this Contract. This is Crucial for cross-chain Interactions to Maintain Security and prevent Unauthorized access.

query.rs

The query.rs file in the Omnichain Token Project Defines various Query handlers and helper functions to fetch Specific information from the Contract’s State and External Sources. These functions respond to Queries sent to the Contract and provide Information related to the Contract’s Configuration, such as Whitelisted Contracts, Chain Types, Owner Details, and Prices for Gas and Tokens from an External Oracle (via Router Protocol).

Imports and Constants

use cosmwasm_std::{to_binary, Binary, Deps, Env, Order, StdResult, Uint128};
use cw2::get_contract_version;
use new_crosstalk_sample::xerc20::QueryMsg;
use router_wasm_bindings::{
types::{GasPriceResponse, TokenPriceResponse},
RouterQuerier, RouterQuery,
};
use crate::state::{
CHAIN_ID, CHAIN_TYPE_MAPPING, CROSS_CHAIN_TOKEN, OWNER, WHITELISTED_CONTRACT_MAPPING,
};

The above Code Comprises of Importing cosmwasm_std, cw2, router_wasm_bindings, etc. packages.

Handling Query Function

pub fn handle_query(deps: Deps<RouterQuery>, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetContractVersion {} => to_binary(&get_contract_version(deps.storage)?), // Fetches the contract version.
QueryMsg::FetchWhiteListedContract { chain_id } => { // Fetches the whitelisted contract on a given chain using its chain_id.
to_binary(&fetch_white_listed_contract(deps, &chain_id)?)
}
QueryMsg::FetchOwner {} => to_binary(&fetch_owner(deps)?), // Returns the contract owner.
QueryMsg::FetchXerc20 {} => to_binary(&fetch_xerc20_addr(deps)?), // Fetches the address of the cross-chain token (XERC20).
QueryMsg::FetchChainId {} => to_binary(&fetch_chain_id(deps)?), // Returns the chain_id of the current contract.
QueryMsg::AllWhiteListedContract {} => to_binary(&fetch_all_white_listed_contract(deps)?), // Fetches all whitelisted contracts.
QueryMsg::FetchChainType { chain_id } => to_binary(&fetch_chain_type(deps, &chain_id)?), // Fetches the type of the chain corresponding to the given chain_id.
}
}

Helper Functions

pub fn is_white_listed_contract(
deps: Deps<RouterQuery>,
chain_id: &str,
contract_addr: &str,
) -> bool {
match WHITELISTED_CONTRACT_MAPPING.load(deps.storage, chain_id) {
Ok(contract) => return contract == contract_addr,
_ => false,
}
}
  • It Checks if the given Contract on a Specified Chain is Whitelisted.
  • Loads the Whitelisted Contract for a given chain_id from Storage.
  • Compares the Loaded Contract Address with the provided contract_addr.
pub fn fetch_white_listed_contract(deps: Deps<RouterQuery>, chain_id: &str) -> StdResult<String> {
WHITELISTED_CONTRACT_MAPPING.load(deps.storage, chain_id)
}
  • It fetches the Whitelisted Contract Address for a given Chain.
  • Loads the Whitelisted Contract for the provided chain_id from Storage.
pub fn fetch_owner(deps: Deps<RouterQuery>) -> StdResult<String> {
OWNER.load(deps.storage)
}
  • It fetches the Owner of the Contract.
  • Loads the Owner from Storage and returns the Owner’s Address as a String.
pub fn fetch_all_white_listed_contract(
deps: Deps<RouterQuery>,
) -> StdResult<Vec<(String, String)>> {
match WHITELISTED_CONTRACT_MAPPING
.range(deps.storage, None, None, Order::Ascending)
.collect()
{
Ok(data) => return Ok(data),
Err(err) => return Err(err),
};
}
  • It fetches all Whitelisted Contracts Stored in the Contract.
  • Retrieves all entries in WHITELISTED_CONTRACT_MAPPING, which Stores Chain IDs and Contract Addresses.
  • The range() method is used to fetch all Records, which are then returned as a Cector of Tuples (chain_id, contract_address).
pub fn fetch_chain_type(deps: Deps<RouterQuery>, chain_id: &str) -> StdResult<u64> {
CHAIN_TYPE_MAPPING.load(deps.storage, chain_id)
}
  • It fetches the type of the chain corresponding to a given chain_id.
  • Loads the Chain Type (as a u64) for the provided chain_id from Storage and returns it.
pub fn fetch_oracle_gas_price(deps: Deps<RouterQuery>, chain_id: String) -> StdResult<u64> {
let router_querier: RouterQuerier = RouterQuerier::new(&deps.querier);
let gas_price_response: GasPriceResponse = router_querier.gas_price(chain_id)?;
Ok((gas_price_response.gas_price * 120) / 100)
}
  • It fetches the Gas Price for a given Chain by querying the Router Protocol Oracle.
  • Uses RouterQuerier to Query the Router Protocol Oracle for the Gas Price of a specified Chain.
  • The returned Gas Price is multiplied by 120% to adjust it Before returning the Result.
pub fn fetch_oracle_token_price(deps: Deps<RouterQuery>, symbol: String) -> StdResult<Uint128> {
let router_querier: RouterQuerier = RouterQuerier::new(&deps.querier);
let token_price_response: TokenPriceResponse = router_querier.token_price(symbol)?;
Ok(token_price_response.token_price)
}
  • It fetches the Price of a given Token Symbol by Querying the Router Protocol Oracle.
  • Queries the Router Protocol Oracle for the Price of a Specific Token Symbol using RouterQuerier.
pub fn fetch_xerc20_addr(deps: Deps<RouterQuery>) -> StdResult<String> {
CROSS_CHAIN_TOKEN.load(deps.storage)
}
  • It fetches the Address of the cross-chain XERC20 Token.
  • Loads the XERC20 Token Address from Storage and returns it as a String.
pub fn fetch_chain_id(deps: Deps<RouterQuery>) -> StdResult<String> {
CHAIN_ID.load(deps.storage)
}
  • It fetches the Chain ID of the Current Contract.
  • Loads the Chain ID from storage and returns it as a String.

state.rs

The state.rs file Defines the Contract’s State Variables and how they are stored in the Blockchain using CosmWasm’s Storage Utilities like Item and Map. These Variables are used throughout the Contract to Maintain important pieces of Data, such as the Contract Owner, Whitelisted Contracts, Chain Types, Chain IDs, and the cross-chain Token Information.

use cw_storage_plus::{Item, Map};
pub const INSTANTIATE_REPLY_ID: u64 = 1; // to track the reply ID when the contract is instantiated
pub const CREATE_I_SEND_REQUEST: u64 = 2; // used when creating a send request for cross-chain operations.
pub const OWNER: Item<String> = Item::new("owner"); // keeps the contract owner's address, which is used to control administrative functions within the contract.
pub const WHITELISTED_CONTRACT_MAPPING: Map<&str, String> = Map::new("forwarder_contract_mapping"); // used to store the contracts on different chains that are allowed to interact with this contract.
pub const CHAIN_TYPE_MAPPING: Map<&str, u64> = Map::new("chain_type_mapping"); // stores the type of the chain corresponding to the chain_id. This could be used to distinguish between different types of chains
pub const CHAIN_ID: Item<String> = Item::new("chain_id"); // identifies the blockchain network on which the contract is deployed. It’s stored under the key "chain_id"
pub const CROSS_CHAIN_TOKEN: Item<String> = Item::new("cross_chain_token"); // holds the address of the token that is being used for cross-chain transfers or interactions.

tests.rs

It Contains the Test Scripts for the Smart Contract which we Wite.

EVM-side

XERC20.sol

Imports and Inheritance

import "@routerprotocol/evm-gateway-contracts/contracts/IDapp.sol";
import "@routerprotocol/evm-gateway-contracts/contracts/IGateway.sol";
import "@routerprotocol/evm-gateway-contracts/contracts/Utils.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

The Contract Imports necessary Libraries and Interfaces for cross-chain functionality and Basic ERC-20 Token Implementation.

  • IDapp and IGateway are Router Protocol Interfaces enabling cross-chain Communication.
  • ERC20 is OpenZeppelin’s Implementation of the ERC-20 Standard for Tokens.

State Variables

address public owner;
string public ChainName;
IGateway public gatewayContract;
mapping(string => string) public name; // Maps chain names to their chain IDs
mapping(string => string) public ourContractOnChains; // Stores contract addresses on other chains
mapping(string => address) public gateway; // Stores gateway addresses for different chains

Define contract-level Variables to store Owner, Gateway Details, Chain Mappings, etc.

  • owner Stores the Address of the Contract Owner.
  • name Maps Chain names (like “mumbai”) to Chain IDs for cross-chain Reference.
  • ourContractOnChains Maps the Chain ID to the Contract Address on the Destination Chain.

Constructor

constructor(
string memory chainName,
uint256 amount
) ERC20("My Token", "MTK") {
name["mumbai"] = "80001";
name["fuji"] = "43113";
gateway["mumbai"] = 0x94caA85bC578C05B22BDb00E6Ae1A34878f047F7;
gateway["fuji"] = 0x94caA85bC578C05B22BDb00E6Ae1A34878f047F7;
ChainName = chainName;
address gatewayAddress = gateway[chainName];
gatewayContract = IGateway(gatewayAddress);
owner = msg.sender;
_mint(msg.sender, amount);
// setting metadata for dapp
gatewayContract.setDappMetadata("0xdeF7d841DEFC2B6d201958d6Ba98bF6eAd884f6d");
}

The Constructor Initializes the Contract on Deployment. It sSets the Chain and Gateway Addresses based on the given Chain Name and Mints an Initial Token Supply.

Set Cross-Chain Contract Addresses

// @notice function to set the fee payer address on Router Chain.
// @param feePayerAddress address of the fee payer on Router Chain.
function setDappMetadata(string memory feePayerAddress) external {
require(msg.sender == owner, "only owner");
gatewayContract.setDappMetadata(feePayerAddress);
}
// @notice function to set the Router Gateway Contract.
// @param gateway address of the gateway contract.
function setGateway(address gateway) external {
require(msg.sender == owner, "only owner");
gatewayContract = IGateway(gateway);
}
function mint(address account, uint256 amount) external {
require(msg.sender == owner, "only owner");
_mint(account, amount);
}
function setContractOnChain(
string calldata chainName,
string calldata contractAddress
) external {
require(msg.sender == owner, "only owner");
ourContractOnChains[name[chainName]] = contractAddress;
}

Define functions to Set Metadata, Gateways, and Contract Addresses on Destination Chains, controlled by the Owner.

  • setDappMetadata sets the Fee Payer Address.
  • setGateway updates the Gateway Contract Address.
  • setContractOnChain stores the Destination Chain’s Contract Address.
  • mint is ued for Minting of the Token.

Cross-Chain Transfer Function

function transferCrossChain(
uint256 amount,
string memory destinationChain
) public payable {
require(
keccak256(bytes(ourContractOnChains[name[destinationChain]])) != keccak256(bytes("")),
"contract on dest not set"
);
require(
balanceOf(msg.sender) >= amount,
"ERC20: Amount cannot be greater than the balance"
);
_burn(msg.sender, amount); // Burn tokens from the sender’s account
// encoding the data that we need to use on destination chain to mint the tokens there.
bytes memory packet = abi.encode(msg.sender, amount);
bytes memory requestPacket = abi.encode(
ourContractOnChains[name[destinationChain]],
packet
);
gatewayContract.iSend{ value: msg.value }(
1,
0,
string(""),
name[destinationChain],
hex"000000000007a12000000006fc23ac0000000000000000000000000000000000000000000000000000000000000000000000",
requestPacket
);
}

This function enables the actual cross-chain Token Transfer by Burning Tokens on the Source Chain and initiating a cross-chain Message.

  • _burn removes Tokens from the Sender’s Balance on the Source Chain.
  • packet encodes the Sender’s Address and Amount, while requestPacket includes the Destination Contract Address.
  • iSend initiates the cross-chain Message.

Receiving Cross-Chain Tokens on the Destination Chain

function iReceive(
string memory requestSender,
bytes memory packet,
string memory srcChainId
) external override returns (bytes memory) {
require(msg.sender == address(gatewayContract), "only gateway");
require(
keccak256(bytes(ourContractOnChains[srcChainId])) == keccak256(bytes(requestSender))
);
(address recipient, uint256 amount) = abi.decode(packet, (address, uint256));
_mint(recipient, amount); // Mint tokens to the recipient
return "";
}

The iReceive function mints tokens on the destination chain for the recipient specified in the cross-chain message.

  • iReceive Checks that the Call comes from the Gateway and that the Source Contract matches.
  • It Decodes the Packet to retrieve recipient and amount, then Mints Tokens to the Recipient’s Address.

Acknowledgement Handling

// @notice function to handle the acknowledgement received from the destination chain
// back on the source chain.
// @param requestIdentifier event nonce which is received when we create a cross-chain request
// We can use it to keep a mapping of which nonces have been executed and which did not.
// @param execFlag a boolean value suggesting whether the call was successfully
// executed on the destination chain.
// @param execData returning the data returned from the handleRequestFromSource
// function of the destination chain.
function iAck(
uint256 requestIdentifier,
bool execFlag,
bytes memory execData
) external override {}

The iAck function can Optionally Track whether a cross-chain Transaction succeeded or failed on the Destination Chain.

  • requestIdentifier is an Event nonce to Track the request.
  • execFlag Indicates success or failure.
  • execData Contains any Data returned from the Destination Chain’s handling function.

Utility Functions

// @notice function to get the request metadata to be used while initiating cross-chain request
// @return requestMetadata abi-encoded metadata according to source and destination chains
function getRequestMetadata(
uint64 destGasLimit,
uint64 destGasPrice,
uint64 ackGasLimit,
uint64 ackGasPrice,
uint128 relayerFees,
uint8 ackType,
bool isReadCall,
string calldata asmAddress
) public pure returns (bytes memory) {
bytes memory requestMetadata = abi.encodePacked(
destGasLimit,
destGasPrice,
ackGasLimit,
ackGasPrice,
relayerFees,
ackType,
isReadCall,
asmAddress
);
return requestMetadata;
}
// @notice function to convert type address into type bytes.
// @param a address to be converted
// @return b bytes pertaining to the address
// @notice Function to convert bytes to address
// @param _bytes bytes to be converted
// @return addr address pertaining to the bytes
function toAddress(bytes memory _bytes) internal pure returns (address addr) {
bytes20 srcTokenAddress;
assembly {
srcTokenAddress := mload(add(_bytes, 0x20))
}
addr = address(srcTokenAddress);
}

Helper functions for Address Conversion and Metadata packing.

  • getRequestMetadata prepares Metadata required by Router’s cross-chain functions.
  • toAddress Converts a Byte array to an Address, often used for Unpacking cross-chain Data.