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 -
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 -
- Saves the Contract Owner.
- Sets the Contract Version.
- Creates a new CW20 Token as the underlying Token for Omnichain functionality.
Response::new()
- Creates a new empty Response.add_submessage()
- Adds a Submessage to be executed after the Current Transaction.SubMsg
- A Submessage that can Trigger a reply.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 -
INSTANTIATE_REPLY_ID
- Handles the Reply from CW20 Token Creation, Saving the new Token’s Address.CREATE_I_SEND_REQUEST
- Handles Replies from cross-chain Send Requests, including Logging Debug Information.
-
Key features -
- It’s an entry point for handling submessage replies.
- Includes Extensive Debug Logging for Development Purposes.
- Uses the Router’s Specific Query and Message Types (RouterQuery, RouterMsg).
- Has a TODO Note about nonce handling which suggests Incomplete cross-chain functionality.
- 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 -
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.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 -
handle_sudo_request
for incoming requests.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 Managementpub 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 Managementpub 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 Configurationpub 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 newCHAIN_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 Functionpub 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 -
handle_sudo_request
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 instantiatedpub 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
andIGateway
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 IDsmapping(string => string) public ourContractOnChains; // Stores contract addresses on other chainsmapping(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, whilerequestPacket
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
andamount
, 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 chainsfunction 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 bytesfunction 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.