Bridge ERC-721 Tokens
SKALE’s Interchain Messaging Agent (IMA) Bridge allows for direct communication and bridging between SKALE Chains (without ever going to Mainnet).
The following walks you through setting up an ERC-721 token between two SKALE Chains and how to programatically bridge.
Important Information
Section titled “Important Information”- When a SKALE Chain is created, there are no ERC-721 tokens mapped by default
- A token being bridged between two chains should have its supply issued (i.e minted) on one chain. The second SKALE Chain mints by design via IMA, however, the token should not be mintable any other way
- Tokens being bridged from SKALE Chain to SKALE Chain are locked in TokenManagerERC721 on the origin chain
- Tokens being bridged from SKALE Chain to SKALE Chain are minted by IMA on the destination chain
Bridge Setup
Section titled “Bridge Setup”1. Prepare the ERC-721
Section titled “1. Prepare the ERC-721”ERC-721 tokens that are being bridged between SKALE Chains should have the mint function that is locked down to TokenManagerERC721.
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 YourInterchainNFT.sol for an example.
// SPDX-License-Identifier: MITpragma solidity 0.8.19;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";/** * @title InterchainERC721 * @dev ERC-721 with role-based minting and dynamic token URI. */contract InterchainERC721 is AccessControl, ERC721 {
using Strings for uint256;
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); string private _baseTokenURI;
constructor( string memory contractName, string memory contractSymbol, string memory baseTokenURI ) ERC721(contractName, contractSymbol) { _setRoleAdmin(MINTER_ROLE, 0xD2aaa00600000000000000000000000000000000); // example admin role _grantRole(MINTER_ROLE, _msgSender()); _baseTokenURI = baseTokenURI; }
function mint(address to, uint256 tokenId) external returns (bool) { require(hasRole(MINTER_ROLE, _msgSender()), "Sender is not a Minter"); _safeMint(to, tokenId); return true; }
/// @notice Override to return dynamic token URI function tokenURI(uint256 tokenId) public view override returns (string memory) { require(_exists(tokenId), "ERC721: URI query for nonexistent token"); return string(abi.encodePacked(_baseTokenURI, tokenId.toString(), ".json"); }
/// @notice Allows admin to update base URI function setBaseURI(string calldata newBaseURI) external onlyRole(MINTER_ROLE) { _baseTokenURI = newBaseURI; }}
2. Deploy the ERC-721 on SKALE
Section titled “2. Deploy the ERC-721 on SKALE”Utilize your preferred tooling i.e Foundry, Hardhat, Remix, etc. to deploy your IMA compatible ERC-721 token to the SKALE Chain you want to be able to bridge assets too.
// SPDX-License-Identifier: MITpragma solidity 0.8.19;
import { InterchainERC721 } from "./InterchainERC721.sol";
contract InterchainNFT is InterchainERC721("NFT Name", "NFT Symbol", "ipfs://<cid>/") {}
InterchainERC721.sol is inherited from the code above
3. Map the SKALE Token
Section titled “3. Map the SKALE Token”Add the token on SKALE Chain TokenManagerERC721
Section titled “Add the token on SKALE Chain TokenManagerERC721”- DST_SCHAIN_NAME is the name of the sChain that the transaction should execute on
- ORIGIN_SCHAIN_NAME is the name of the sChain that the token is being mapped from
- 0x_ORIGIN_TOKEN is the original token address on the ORIGIN_SCHAIN_NAME
- 0x_DST_TOKEN is the destination token address on the DST_SCHAIN_NAME
npx msig encodeData [DST_SCHAIN_NAME] TokenManagerERC721 addERC20TokenByOwner [ORIGIN_SCHAIN_NAME] [0x_ORIGIN_TOKEN] [0x_DST_TOKEN]
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 TokenManagerERC721 on SKALE Chain - event ERC721TokenAdded(SchainHash indexed chainHash, address indexed erc721OnMainChain, address indexed erc721OnSchain);
Bridging ERC-721
Section titled “Bridging ERC-721”The following does not require you to setup your own token. This works with ANY ERC-721 token that is mapped from a SKALE Chain to other any SKALE Chain as long as the actual ERC-721 token on each side does not have additional restrictions around who can transfer.
The flow for bridging an ERC-721 from sChain to sChain follows a very similar flow to a standard ERC-721 transfer:
- Approve the bridge contract on the ERC-721 token to allow it to move your NFTs
- Call the bridge directly to transfer the specific ERC-721 from SKALE Chain -> 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 NFT corresponding to the NFT (by id) locked on the origin SKALE Chain during the bridge
Bridge Tokens
Section titled “Bridge Tokens”The following will help you bridge an NFT from one SKALE Chain to another.
import { Contract, JsonRpcProvider, Wallet } from "ethers"; // npm add ethers
const PRIVATE_KEY = "[YOUR_PRIVATE_KEY]";const ORIGIN_SCHAIN_RPC_URL = "[YOUR_RPC_URL]";const ERC721_ADDRESS = "[ORIGIN_TOKEN_ADDRESS]";const ERC721_ABI = [ "function approve(address spender, uint256 tokenId) external" ];const TOKEN_MANAGER_ERC721_ADDRESS = "0xD2aaa00600000000000000000000000000000000"; // DO NOT CHANGE THISconst TOKEN_MANAGER_ERC721_ABI = [ "function transferToSchainERC721(string calldata targetSchainName, address contractOnMainnet, uint256 tokenId) external" ];const DST_SKALE_CHAIN_NAME = "[DST_SKALE_CHAIN_NAME]"; // e.g green-giddy-denebola (nebula mainnnet);const TOKEN_ID = BigInt(1);
// Setup the RPC Provider to connect to Ethereumconst provider = new JsonRpcProvider(ORIGIN_SCHAIN_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 tokenManagerContract = new Contract(TOKEN_MANAGER_ERC721_ADDRESS, TOKEN_MANAGER_ERC721_ABI, wallet);const tokenContract = new Contract(ERC721_ADDRESS, ERC721_ABI, wallet);
// 1. Approve the bridge to move ERC-721 Token Id #1const approvalTx = await tokenContract.approve(TOKEN_MANAGER_ERC721_ADDRESS, TOKEN_ID);await approvalTx.wait(1); // Wait 1 blocks for confirmation, ~1 seconds
// 2. Deposit ERC-721 Token Id #1 into bridge, will receive on same address on SKALEconst bridgeTx = await tokenManagerContract.transferToSchainERC721(DST_SKALE_CHAIN_NAME, ERC721_ADDRESS, TOKEN_ID);await bridgeTx.wait(1); // Wait 1 blocks for confirmation, ~1 seconds
// Success! Now watch for delivery on Destination Chainconsole.log("Success!");