Bridge ERC-20 Tokens
SKALE’s Interchain Messaging Agent includes a native bridging layer for ERC-20 tokens, the most popular fungible token standard that exists in blockchain today. The following introduces key information on setting up the bridgeable token as well as how to actually programatically bridge the token.
Important Information
Section titled “Important Information”- When a SKALE Chain is created, there are NO ERC-20 tokens mapped by default
- In order to bridge an ERC-20 token to a SKALE Chain from Ethereum, it must be added by the SKALE Chain owner or operator via the standard mapping process
- All ERC-20 tokens bridged into SKALE have a set of basic requirements to make them compatible with SKALE’s bridging layer
Bridge Setup
Section titled “Bridge Setup”1. Prepare the ERC-20 on SKALE
Section titled “1. Prepare the ERC-20 on SKALE”ERC-20 tokens that are being bridged to SKALE should follow two basic requirements in order to be compatible with the SKALE IMA bridging layer:
- Have a mint function that is locked down to the IMA Bridge
- Have a burn function that is locked down to the IMA Bridge
The following is the base interchain token code that meets the above bridging requirements for IMA. It is not recommended to use this directly, but to use a wrapper. See InterchainSKL for an 18 decimal example and InterchainUSDT for a 6 decimal example.
// SPDX-License-Identifier: MITpragma solidity 0.8.19;
// Importing the ERC20 standard contract and AccessControl for role-based access management.import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
/** * @title InterchainERC20 * @dev This contract is an ERC20 token implementation with role-based access control for minting and burning. * It utilizes OpenZeppelin's ERC20 and AccessControl for functionality. */contract InterchainERC20 is ERC20, AccessControl { // Define roles using hashed constants for efficient comparison. bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor(string memory name, string memory symbol) ERC20(name, symbol) { // Assign the minter role to a predefined address. _grantRole(MINTER_ROLE, 0xD2aAA00500000000000000000000000000000000);
// Assign the burner role to a predefined address. _grantRole(BURNER_ROLE, 0xD2aAA00500000000000000000000000000000000); }
function mint(address to, uint256 amount) public virtual { // Ensure that the caller has the MINTER_ROLE. require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
// Mint the specified amount of tokens to the target address. _mint(to, amount); }
function burn(uint256 amount) public virtual { // Ensure that the caller has the BURNER_ROLE. require(hasRole(BURNER_ROLE, msg.sender), "Caller is not a burner");
// Burn the specified amount of tokens from the caller's account. _burn(msg.sender, amount); }}
2. Deploy the ERC-20 on SKALE
Section titled “2. Deploy the ERC-20 on SKALE”Utilize your preferred tooling i.e Foundry, Hardhat, Remix, etc. to deploy your IMA compatible ERC-20 token to the SKALE Chain you want to be able to bridge assets too.
This is an example of an 18 decimal ERC-20 token that would be deployed on SKALE
// SPDX-License-Identifier: MITpragma solidity 0.8.19;
import { InterchainERC20 } from "./InterchainERC20.sol";
contract InterchainSKL is InterchainERC20("Skale Token", "SKL") {}
This is an example of an 6 decimal ERC-20 token that would be deployed on SKALE
// SPDX-License-Identifier: MITpragma solidity 0.8.19;
import { InterchainERC20 } from "./InterchainERC20.sol";
contract InterchainUSDT is InterchainERC20("Tether USD", "USDT") { function decimals() public view override returns (uint8) { return 6; }}
InterchainERC20.sol is inherited from the code above
3. Map the SKALE and Ethereum tokens together
Section titled “3. Map the SKALE and Ethereum tokens together”Add the token on Ethereum via SAFE
Section titled “Add the token on Ethereum via SAFE”Start by visiting your SAFE via the official SAFE website or your preferred tool. If you are unsure, you can follow the steps here.
Once on your SAFE, start by preparing a transaction via the Transaction Builder and follow the screenshots below, which will use Europa Testnet and the SKL token as en example for the mapping.
Add the token on SKALE Chain TokenManagerERC20
Section titled “Add the token on SKALE Chain TokenManagerERC20”npx msig encodeData [SKALE_CHAIN_NAME] TokenManagerERC20 addERC20TokenByOwner Mainnet 0x[TOKEN_ON_ETHEREUM] 0x[TOKEN_ON_SKALE_CHAIN]
After this, execute by following the steps on Using SAFE
Verify the Mapping
Section titled “Verify the Mapping”To verify the mapping, you should have an event emitted from the following:
-
DepositBoxERC20 on Ethereum -
event ERC20TokenAdded(string schainName, address indexed contractOnMainnet);
-
TokenManagerERC20 on SKALE Chain -
event ERC20TokenAdded(SchainHash indexed chainHash, address indexed erc20OnMainChain, address indexed erc20OnSchain);
Bridging ERC-20
Section titled “Bridging ERC-20”The following does not require you to setup your own token. This works with ANY ERC-20 token that is mapped from Ethereum to any SKALE Chain as long as the actual ERC-20 token on each side does not have additional restrictions around who can transfer.
The flow for bridging an ERC-20 from Ethereum to SKALE follows a very similar flow to a standard ERC-20 transfer:
- Approve the bridge contract on the ERC-20 token to allow it to control some amount of funds
- Call the bridge directly to transfer the asset from Ethereum -> SKALE Chain, if the mapping exists
- Wait for the message to be posted by the validator set on the SKALE Chain, which is the net-new minted tokens corresponding to the value locked on Ethereum during the bridge
Bridge to SKALE (from Ethereum)
Section titled “Bridge to SKALE (from Ethereum)”import { Contract, JsonRpcProvider, Wallet, parseEther } from "ethers"; // npm add ethers
const PRIVATE_KEY = "[YOUR_PRIVATE_KEY]";const ETHEREUM_RPC_URL = "[YOUR_ETHEREUM_RPC_URL]";const ERC20_ADDRESS = "[YOUR_TOKEN_ADDRESS]";const ERC20_ABI = [ "function approve(address spender, uint256 amount) external" ];const DEPOSIT_BOX_ERC20_ADDRESS = "[DEPOSIT_BOX_ERC20_ADDRESS]";const DEPOSIT_BOX_ERC20_ABI = [ "function depositERC20(string calldata schainName, address erc20OnMainnet, uint256 amount) external" ];const SKALE_CHAIN_NAME = "[SKALE_CHAIN_NAME]"; // e.g elated-tan-skat (europa mainnnet);const ONE_HUNDRED_TOKENS = parseEther("100"); // 100 tokens in wei format
// Setup the RPC Provider to connect to Ethereumconst provider = new JsonRpcProvider(ETHEREUM_RPC_URL);
// Setup the wallet with your private key and default to the Ethereum providerconst wallet = new Wallet(PRIVATE_KEY, provider);
// Setup the smart contracts which default to being signed by your wallet and connected on Ethereumconst depositBoxContract = new Contract(DEPOSIT_BOX_ERC20_ADDRESS, DEPOSIT_BOX_ERC20_ABI, wallet);const erc20TokenContract = new Contract(ERC20_ADDRESS, ERC20_ABI, wallet);
// 1. Approve the bridged to move ERC-20const approvalTx = await erc20TokenContract.approve(DEPOSIT_BOX_ERC20_ADDRESS, ONE_HUNDRED_TOKENS);await approvalTx.wait(1); // Wait 1 blocks for confirmation, ~15 seconds
// 2. Deposit ERC-20 into bridge, will receive on same address on SKALEconst depositTx = await depositBoxContract.depositERC20(SKALE_CHAIN_NAME, ERC20_ADDRESS, ONE_HUNDRED_TOKENS);await depositTx.wait(1);
// Success! Now watch for delivery on SKALE Chainconsole.log("Success!");
import { Contract, JsonRpcProvider, Wallet, parseEther } from "ethers"; // npm add ethers
const PRIVATE_KEY = "[YOUR_PRIVATE_KEY]";const ETHEREUM_RPC_URL = "[YOUR_ETHEREUM_RPC_URL]";const ERC20_ADDRESS = "[YOUR_TOKEN_ADDRESS]";const ERC20_ABI = [ "function approve(address spender, uint256 amount) external" ];const DEPOSIT_BOX_ERC20_ADDRESS = "[DEPOSIT_BOX_ERC20_ADDRESS]";const DEPOSIT_BOX_ERC20_ABI = [ "function depositERC20Direct(string calldata schainName, address erc20OnMainnet, uint256 amount, address receiver) external" ];const SKALE_CHAIN_NAME = "[SKALE_CHAIN_NAME]"; // e.g elated-tan-skat (europa mainnnet);const ONE_HUNDRED_TOKENS = parseEther("100"); // 100 tokens in wei formatconst CUSTOM_RECEIVER = "[CUSTOM_RECEIVER_WALLET_ADDRESS]";
// Setup the RPC Provider to connect to Ethereumconst provider = new JsonRpcProvider(ETHEREUM_RPC_URL);
// Setup the wallet with your private key and default to the Ethereum providerconst wallet = new Wallet(PRIVATE_KEY, provider);
// Setup the smart contracts which default to being signed by your wallet and connected on Ethereumconst depositBoxContract = new Contract(DEPOSIT_BOX_ERC20_ADDRESS, DEPOSIT_BOX_ERC20_ABI, wallet);const erc20TokenContract = new Contract(ERC20_ADDRESS, ERC20_ABI, wallet);
// 1. Approve the bridged to move ERC-20const approvalTx = await erc20TokenContract.approve(DEPOSIT_BOX_ERC20_ADDRESS, ONE_HUNDRED_TOKENS);await approvalTx.wait(1); // Wait 1 blocks for confirmation, ~15 seconds
// 2. Deposit ERC-20 into bridge, will receive on the CUSTOM_RECEIVER address on SKALEconst depositTx = await depositBoxContract.depositERC20Direct(SKALE_CHAIN_NAME, ERC20_ADDRESS, ONE_HUNDRED_TOKENS, CUSTOM_RECEIVER);await depositTx.wait(1);
// Success! Now watch for delivery on SKALE Chainconsole.log("Success!");
Bridge to Ethereum (from SKALE)
Section titled “Bridge to Ethereum (from SKALE)”SKALE’s decentralized bridge offers a simple two-step process to bridge from any SKALE Chain to Ethereum Mainnet.
-
The first step, which only has to be done if you don’t have a sufficient balance to exit, is to fill up your gas wallet on Ethereum
-
The second step is to initiate the bridge (technically known as an exit) on the SKALE Chain
Pre-pay for your Exit
Section titled “Pre-pay for your Exit”This step is optional IF the user has already filled up their gas wallet and has sufficient balance left.
You can check if the wallet is an activeUser
on the CommunityLocker 0xD2aaa00300000000000000000000000000000000 smart contract on the SKALE Chain. If active, no need to fill the pool again.
import { Contract, JsonRpcProvider, Wallet, parseEther } from "ethers"; // npm add ethers
const PRIVATE_KEY = "[YOUR_PRIVATE_KEY]";const ETHEREUM_RPC_URL = "[YOUR_ETHEREUM_RPC_URL]";const COMMUNITY_POOL_ADDRESS = "[COMMUNIY_POOL_ADDRESS]";const COMMUNITY_POOL_ABI = [ "function rechargeUserWallet(string calldata schainName, address user) external" ];const SKALE_CHAIN_NAME = "[SKALE_CHAIN_NAME]"; // e.g elated-tan-skat (europa mainnnet);
// Setup the RPC Provider to connect to Ethereumconst provider = new JsonRpcProvider(ETHEREUM_RPC_URL);
// Setup the wallet with your private key and default to the Ethereum providerconst wallet = new Wallet(PRIVATE_KEY, provider);
// Setup the smart contracts which default to being signed by your wallet and connected on Ethereumconst communityPoolContract = new Contract(COMMUNITY_POOL_ADDRESS, COMMUNITY_POOL_ABI, wallet);
const rechargeTx = await communityPoolContract.rechargeUserWallet( SKALE_CHAIN_NAME, wallet.address, { value: parseEther("0.02") // Recharge by 0.02 ETH });
await rechargeTx.wait(5); // wait 5 blocks for full finality
// Success! You can now bridge from SKALE to Ethereum!console.log("Success!");
Bridge to Ethereum
Section titled “Bridge to Ethereum”Once the above prepayment steps are completed, you can proceed with the bridging.
Bridging from SKALE simply requires the exitToMainERC20
function to be called with the corresponding token and amount to initiate the transfer back to Ethereum.
import { Contract, JsonRpcProvider, Wallet, parseEther } from "ethers"; // npm add ethers
const PRIVATE_KEY = "[YOUR_PRIVATE_KEY]";const SKALE_RPC_URL = "[YOUR_SKALE_RPC_URL]";const ERC20_ADDRESS = "[YOUR_TOKEN_ADDRESS]";const ERC20_ABI = [ "function approve(address spender, uint256 amount) external" ];const TOKEN_MANAGER_ERC20_ADDRESS = "0xD2aAA00500000000000000000000000000000000"; // DO NOT CHANGE THISconst TOKEN_MANAGER_ERC20_ABI = [ "function exitToMainERC20(address contractOnMainnet, uint256 amount) external" ];const ONE_HUNDRED_TOKENS = parseEther("100"); // 100 tokens in wei format
// Setup the RPC Provider to connect to Ethereumconst provider = new JsonRpcProvider(SKALE_RPC_URL);
// Setup the wallet with your private key and default to the Ethereum providerconst wallet = new Wallet(PRIVATE_KEY, provider);
// Setup the smart contracts which default to being signed by your wallet and connected on Ethereumconst tokenManagerERC20Contract = new Contract(TOKEN_MANAGER_ERC20_ADDRESS, TOKEN_MANAGER_ERC20_ABI, wallet);const erc20TokenContract = new Contract(ERC20_ADDRESS, ERC20_ABI, wallet);
// 1. Approve the bridge to move ERC-20 on your behalfconst approvalTx = await erc20TokenContract.approve(TOKEN_MANAGER_ERC20_ADDRESS, ONE_HUNDRED_TOKENS);await approvalTx.wait(1); // Wait 1 blocks for confirmation, ~1 seconds
// 2. Transfer ERC-20 into bridge, will recieve on the same address on Ethereumconst exitTx = await tokenManagerERC20Contract.exitToMainERC20(ERC20_ADDRESS, ONE_HUNDRED_TOKENS);await exitTx.wait(1);
// Success! Now watch for delivery on Ethereumconsole.log("Success!");