Managing ERC721

As a SKALE Chain Owner, there are two main steps to managing ERC721 through IMA:

Once you have completed step 1 to setup and map your ERC721 tokens, you can then setup transfer flow to allow end-users to transfer ERC721 tokens between Ethereum and your SKALE Chain.

Setup and Map ERC721 Transfers

The following one-time setup for each ERC721 token is required for SKALE Chains with a default access control policy (default settings are: whitelisting enabled, automatic deployment disabled). For more information on IMA access control, see here.

1. Review the token contract

First, review the Mainnet ERC721 token implementation and (if needed) modify a SKALE Chain version of the contract to include Mintable and Burnable functions. These functions are required to dynamically mint and burn the token on the SKALE chain in response to deposit and exit on the Mainnet.

Example Mainnet contract

pragma solidity >=0.4.24 <0.6.0;

import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721Mintable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721Burnable.sol";

contract MyERC721 is ERC721Full, ERC721Mintable, ERC721Burnable {
    constructor(
        string memory _name,
        string memory _symbol
    )
    ERC721Full(_name, _symbol)
    public
    {
    }

    /**
    * Custom accessor to create a unique token
    */
    function mintUniqueTokenTo(
        address _to,
        uint256 _tokenId,
        string  memory _tokenURI
    ) public
    {
        super._mint(_to, _tokenId);
        super._setTokenURI(_tokenId, _tokenURI);
    }
}

Example Modified SKALE Chain contract

pragma solidity >=0.4.24 <0.6.0;

import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721Mintable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721Burnable.sol";

contract MyERC721 is ERC721Full, ERC721Mintable, ERC721Burnable {
    constructor(
        string memory _name,
        string memory _symbol
    )
    ERC721Full(_name, _symbol)
    public
    {
    }

    /**
    * Custom accessor to create a unique token
    */
    function mintUniqueTokenTo(
        address _to,
        uint256 _tokenId,
        string  memory _tokenURI
    ) public
    {
        super._mint(_to, _tokenId);
        super._setTokenURI(_tokenId, _tokenURI);
    }
}

If you aren’t using OpenZeppelin’s framework, then you can manually add [Mintable](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/c3178ff942f9f487b9fda2c648aa19e633560adb/contracts/token/ERC721/ERC721.sol#L256) and [Burnable](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/c3178ff942f9f487b9fda2c648aa19e633560adb/contracts/token/ERC721/ERC721.sol#L278) functions, and finally [add MINTER_ROLE access control](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v2.5.1/contracts/access/roles/MinterRole.sol). Be sure that the main mint and burn functions have public visibility with the private _mint and _burn functions as internal visibility. For a further example, see [IMA ERC721 Custom Token](https://github.com/skalenetwork/IMA/blob/develop/proxy/test-tokens/contracts/ERC721Custom.sol).

For a set of examples for IMA SKALE-Chain side suitable tokens, see the IMA test-tokens folder.

2. Add a Minter Role

Now you need to add the pre-deployed TokenManagerERC721 contact on your SKALE Chain as the MINTER_ROLE for the modified SKALE Chain contract. With OpenZeppelin’s framework, you simply need to execute an AddMinter transaction on the SKALE chain token contract.

Example Add Minter Role

  • Web3 JS

import Common from "ethereumjs-common";
const Tx = require("ethereumjs-tx").Transaction;
const Web3 = require("web3");

let schainABIs = "[YOUR_SKALE_CHAIN_ABIs]";
let schainERC721ABI = "[YOUR_SCHAIN_ERC721_ABI]";
let chainId = "[YOUR_SKALE_CHAIN_CHAIN_ID]";

const customCommon = Common.forCustomChain(
    "mainnet",
    {
      name: "skale-network",
      chainId: chainId
    },
    "istanbul"
  );

let contractOwnerPrivateKey = new Buffer("[YOUR_PRIVATE_KEY]", 'hex');

let contractOwnerAccount = "[CONTRACT_OWNER_ACCOUNT]"; // SKALE Chain owner or authorized deployer account

let schainEndpoint = "[YOUR_SKALE_CHAIN_ENDPOINT]";

const erc721ABI = schainERC721ABI.erc721_abi;
const erc721Address = schainERC721ABI.erc721_address;

const tokenManagerAddress = schainABIs.token_manager_erc721_address;

const web3ForSchain = new Web3(schainEndpoint);

let schainERC721Contract = new web3ForSchain.eth.Contract(
  erc721ABI,
  erc721Address
);

let addMinter = schainERC721Contract.methods
    .addMinter(tokenManagerAddress)
    .encodeABI();

  web3ForSchain.eth.getTransactionCount(contractOwnerAccount).then((nonce) => {
    //create raw transaction
    const rawTxAddMinter = {
      from: contractOwnerAccount,
      nonce: nonce,
      data: addMinter,
      to: erc721Address,
      gasPrice: 100000000000,
      gas: 8000000
    };
    //sign transaction
    const txAddMinter = new Tx(rawTxAddMinter, { common: customCommon });
    txAddMinter.sign(contractOwnerPrivateKey);

    const serializedTxAddMinter = txAddMinter.serialize();

    //send signed transaction (add minter)
    web3ForSchain.eth
      .sendSignedTransaction("0x" + serializedTxAddMinter.toString("hex"))
      .on("receipt", (receipt) => {
        console.log(receipt);
      })
      .catch(console.error);
  });

3. Register Mainnet contract to IMA

Third, you need to register the Mainnet token contract into IMA on Mainnet using the addERC721TokenByOwner method in the DepositBoxERC721 contract:

  • IMA-JS

  • Web3 JS

In all ima-js code samples we’re assuming that library is already inited. See init instructions here. For the 2 objects usage drop ima. prefix.

// import & init ima-js here

export async function linkERC721TokenMainnet(ima) {
  let schainName = "[YOUR_SKALE_CHAIN_NAME]";
  const erc721OnMainnet = "[ADDRESS_OF_ERC721_ON_MAINNET]";

  let address = "[YOUR_ADDRESS]";
  let privateKey = "[YOUR_PRIVATE_KEY]";

  let txOpts = {
    address: address,
    privateKey: privateKey // remove privateKey from txOpts to use Metamask signing
  };

  await mainnet.addERC721TokenByOwner(schainName, erc721OnMainnet, txOpts);
}
const Web3 = require("web3");
const Tx = require("ethereumjs-tx").Transaction;

let rinkebyABIs = "[YOUR_RINKEBY_ABIs]";
let rinkebyERC721ABI = "[YOUR_RINKEBY_ERC721_ABI]";

let privateKey = new Buffer("[YOUR_PRIVATE_KEY]", 'hex');

let erc721OwnerForMainnet = "[YOUR_ERC721_MAINNET_OWNER]";

let rinkeby = "[YOUR_RINKEBY_ENDPOINT]";
let schainName = "[YOUR_SKALE_CHAIN_NAME]";
let chainId = "[YOUR_RINKEBY_CHAIN_ID]";

const depositBoxAddress = rinkebyABIs.deposit_box_erc721_address;
const depositBoxABI = rinkebyABIs.deposit_box_erc721_abi;

const erc721AddressOnMainnet = rinkebyERC721ABI.erc721_address;

const web3ForMainnet = new Web3(rinkeby);

let DepositBox = new web3ForMainnet.eth.Contract(
  depositBoxABI,
  depositBoxAddress
);

let addERC721TokenByOwner = DepositBox.methods
    .addERC721TokenByOwner(schainName, erc721AddressOnMainnet)
    .encodeABI();

  web3ForMainnet.eth.getTransactionCount(erc721OwnerForMainnet).then((nonce) => {
    const rawTxAddERC721TokenByOwner = {
      chainId: chainId,
      from: erc721OwnerForMainnet,
      nonce: "0x" + nonce.toString(16),
      data: addERC721TokenByOwner,
      to: depositBoxAddress,
      gas: 6500000,
      gasPrice: 100000000000
    };

    //sign transaction
    const txAddERC721TokenByOwner = new Tx(rawTxAddERC721TokenByOwner, {
        chain: "rinkeby",
        hardfork: "petersburg"
      });

    txAddERC721TokenByOwner.sign(privateKey);

    const serializedTxDeposit = txAddERC721TokenByOwner.serialize();

    //send signed transaction (addERC721TokenByOwner)
    web3ForMainnet.eth
      .sendSignedTransaction("0x" + serializedTxDeposit.toString("hex"))
      .on("receipt", (receipt) => {
        console.log(receipt);
      })
      .catch(console.error);
  });

4. Register SKALE Chain contract to IMA

Finally, you need to register the (modified) token contract on the SKALE chain IMA using the addERC721TokenByOwner method in TokenManagerERC721 contract. Note that you need to register the contract on Mainnet first, so that the registration on the SKALE Chain can reference the Mainnet token address.

  • IMA-JS

  • Web3 JS

In all ima-js code samples we’re assuming that library is already inited. See init instructions here. For the 2 objects usage drop ima. prefix.

// import & init ima-js here

export async function linkERC721TokenSchain(ima) {
  let erc721OnMainnet = "[ADDRESS_OF_ERC721_TOKEN_ON_MAINNET]";
  let erc721OnSchain = "[ADDRESS_OF_ERC721_TOKEN_ON_SCHAIN]";

  let address = "[YOUR_ADDRESS]";
  let privateKey = "[YOUR_PRIVATE_KEY]";

  let txOpts = {
    address: address,
    privateKey: privateKey // remove privateKey from txOpts to use Metamask signing
  };

  await ima.schain.addERC721TokenByOwner(erc721OnMainnet, erc721OnSchain, txOpts);
}
import Common from "ethereumjs-common";
const Web3 = require("web3");
const Tx = require("ethereumjs-tx").Transaction;

let schainABIs = "[YOUR_SKALE_CHAIN_ABIs]";
let schainERC721ABI = "[YOUR_SCHAIN_ERC721_ABI]";
let rinkebyERC721ABI = "[YOUR_RINKEBY_ERC721_ABI]";

let privateKey = new Buffer("[YOUR_PRIVATE_KEY]", 'hex');

let erc721OwnerForSchain = "[YOUR_SCHAIN_ADDRESS]";

let schainEndpoint = "[YOUR_SKALE_CHAIN_ENDPOINT]";
let chainId = "[YOUR_SKALE_CHAIN_CHAIN_ID]";

const customCommon = Common.forCustomChain(
    "mainnet",
    {
      name: "skale-network",
      chainId: chainId
    },
    "istanbul"
  );

const tokenManagerAddress = schainABIs.token_manager_erc721_address;
const tokenManagerABI = schainABIs.token_manager_erc721_abi;

const erc721AddressOnMainnet = rinkebyERC721ABI.erc721_address;
const erc721AddressOnSchain = schainERC721ABI.erc721_address;

const web3ForSchain = new Web3(schainEndpoint);

let TokenManager = new web3ForSchain.eth.Contract(
    tokenManagerABI,
    tokenManagerAddress
);

let addERC721TokenByOwner = TokenManager.methods
    .addERC721TokenByOwner(
      erc721AddressOnMainnet,
      erc721AddressOnSchain
    )
    .encodeABI();

  web3ForSchain.eth.getTransactionCount(erc721OwnerForSchain).then((nonce) => {
    const rawTxAddERC721TokenByOwner = {
      from: erc721OwnerForSchain,
      nonce: "0x" + nonce.toString(16),
      data: addERC721TokenByOwner,
      to: tokenManagerAddress,
      gas: 6500000,
      gasPrice: 100000000000
    };

    //sign transaction
    const txAddERC721TokenByOwner = new Tx(rawTxAddERC721TokenByOwner, {
      common: customCommon
    });

    txAddERC721TokenByOwner.sign(privateKey);

    const serializedTxDeposit = txAddERC721TokenByOwner.serialize();

    web3ForSchain.eth
      .sendSignedTransaction("0x" + serializedTxDeposit.toString("hex"))
      .on("receipt", (receipt) => {
        console.log(receipt);
      })
      .catch(console.error);
  });

Get Started with ERC721 Transfer

The Interchain Messaging Agent can be used for managing ERC721 tokens between Ethereum and SKALE. The following steps guide you through a complete transfer from Ethereum to SKALE and back. Be sure to follow any one-time setup and mapping steps described here.

1. Deposit ERC721 on Ethereum

To send ERC721 tokens from a user’s wallet to the IMA Deposit Box on Ethereum, you will need to use the depositERC721 function within the DepositBoxERC721 IMA contract on Ethereum.

This method is called from Ethereum to move ERC721 tokens into a Deposit Box.

The DepositBoxERC721 IMA contract is currently deployed to the Rinkeby testnet. To get the ABIs to interact with IMA on Rinkeby, check out the current release page.

Example Code

  • IMA-JS

  • Web3 JS

In all ima-js code samples we’re assuming that library is already inited. See init instructions here. For the 2 objects usage drop ima. prefix. See IMA-JS deposit.js sandbox

// import & init ima-js here

export function initTestTokenContract(ima) {
  // initialize ERC721 contract
  const abiData = require("[ERC721_ABI_ON_ETHEREUM]");
  return new ima.mainnet.web3.eth.Contract(
    abiData.erc721_abi,
    abiData.erc721_address);
}

export async function depositERC721(ima) {
  let tokenName = "[ERC721_TOKEN_NAME";
  let erc721TokenId = "[ID_OF_THE_TOKEN_TO_TRANSFER]";
  let schainName = "[YOUR_SKALE_CHAIN_NAME]";

  let contractObject = initTestTokenContract(ima);
  ima.mainnet.addERC721Token(tokenName, contractObject);

  let address = "[YOUR_ADDRESS]";
  let privateKey = "[YOUR_PRIVATE_KEY]";

  let txOpts = { // transaction options
    address: address,
    privateKey: privateKey // remove privateKey from txOpts to use Metamask signing
  };

  const erc721OwnerMainnet = await ima.mainnet.getERC721OwnerOf(tokenName, erc721TokenId); // get owner on the Mainnet before the transfer
  const erc721OwnerSchain = await ima.schain.getERC721OwnerOf(tokenName, erc721TokenId); // get owner on the sChain before the transfer

  const depositBoxAddress = ima.mainnet.contracts.depositBoxERC721.options.address;
  if (erc721OwnerMainnet != depositBoxAddress) {
    await ima.mainnet.approveERC721Transfer(tokenName, erc721TokenId, txOpts); // approve transfer only if token is not already locked
  }

  await ima.mainnet.depositERC721(schainName, tokenName, address, erc721TokenId, txOpts);

  // optional
  await ima.schain.waitERC721OwnerChange(tokenName, erc721TokenId, erc721OwnerSchain); // wait for the owner to be changed on the sChain side
}
const Web3 = require('web3');
const Tx = require('ethereumjs-tx').Transaction;

let rinkebyABIs = "[YOUR_SKALE_ABIs_ON_RINKEBY]";
let rinkebyERC721ABI = "[YOUR_ERC721_ABI_ON_RINKEBY]";

let privateKey = new Buffer("[YOUR_PRIVATE_KEY]", "hex");
let accountForMainnet = "[YOUR_ACCOUNT_ADDRESS]";
let accountForSchain = "[YOUR_ACCOUNT_ADDRESS]";

let rinkeby = "[RINKEBY_ENDPOINT]";
let schainName = "[YOUR_SKALE_CHAIN_NAME]";
let chainId = "YOUR_RINKEBY_CHAIN_ID";

let mintId = "[ERC721_MINT_ID]";

const depositBoxAddress = rinkebyABIs.deposit_box_erc721_address;
const depositBoxABI = rinkebyABIs.deposit_box_erc721_abi;

const erc721ABI = rinkebyERC721ABI.erc721_abi;
const erc721Address = rinkebyERC721ABI.erc721_address;

const web3ForMainnet = new Web3(rinkeby);

let depositBox = new web3ForMainnet.eth.Contract(
depositBoxABI,
depositBoxAddress
);

let contractERC721 = new web3ForMainnet.eth.Contract(
erc721ABI,
erc721Address
);

/**
   * Uses the openzeppelin ERC721
   * contract function approve
   * https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721
   */
let approve = contractERC721.methods
  .approve(depositBoxAddress, process.env.REACT_APP_MINT_ID)
  .encodeABI();

let deposit = depositBox.methods
.depositERC721(schainName, erc721Address, accountForSchain, mintId)
.encodeABI();

web3ForMainnet.eth.getTransactionCount(accountForMainnet).then((nonce) => {
//create raw transaction
const rawTxApprove = {
  chainId: chainId,
  from: accountForMainnet,
  nonce: "0x" + nonce.toString(16),
  data: approve,
  to: erc721Address,
  gas: 6500000,
  gasPrice: 100000000000
};
//sign transaction
const txApprove = new Tx(rawTxApprove, {
      chain: "rinkeby",
      hardfork: "petersburg"
    });
txTransfer.sign(privateKey);

const serializedTxTransfer = txApprove.serialize();

//send signed transaction (approve)
web3ForMainnet.eth
  .sendSignedTransaction("0x" + serializedTxTransfer.toString("hex"))
  .on("receipt", (receipt) => {
    console.log(receipt);
    web3ForMainnet.eth
      .getTransactionCount(accountForMainnet)
      .then((nonce) => {
        const rawTxDeposit = {
          chainId: chainId,
          from: accountForMainnet,
          nonce: "0x" + nonce.toString(16),
          data: deposit,
          to: depositBoxAddress,
          gas: 6500000,
          gasPrice: 100000000000
        };

        //sign transaction
        const txDeposit = new Tx(rawTxDeposit, {
          chain: "rinkeby",
          hardfork: "petersburg"
        });

        txDeposit.sign(privateKey);

        const serializedTxDeposit = txDeposit.serialize();

        //send signed transaction (deposit)
        web3ForMainnet.eth
          .sendSignedTransaction("0x" + serializedTxDeposit.toString("hex"))
          .on("receipt", receipt => {
            console.log(receipt);
          })
          .catch(console.error);
      });
  })
  .catch(console.error);
});

2. Exit from SKALE Chain

To send ERC721 tokens back to Ethereum, you will need to use the exitToMainERC721 function within the TokenManagerERC721 IMA contract on the SKALE Chain.

This method is called from the SKALE Chain to send funds and move the token back to Ethereum.

Note that the SKALE Chain user must have:

  • skETH to conduct the exitToMain transaction on the SKALE Chain TokenManager contract.

  • a sufficient balance of ETH in the Community Pool to initiate the exit to Ethereum *See Funding Exits.

The TokenManagerERC721 IMA contract is pre-deployed to your SKALE Chain. Please reach out to your account manager to receive the ABIs specific for your SKALE Chain.

Example Code

  • IMA-JS

  • Web3 JS

In all ima-js code samples we’re assuming that library is already inited. See init instructions here. For the 2 objects usage drop ima. prefix. See IMA-JS deposit.js sandbox

// import & init ima-js here

export function initTestTokenContract(ima) {
  // initialize ERC721 contract
  const abiData = require("[ERC721_ABI_ON_SCHAIN]");
  return new ima.schain.web3.eth.Contract(
    abiData.erc721_abi,
    abiData.erc721_address);
}

export async function exit(ima) {
  let tokenName = "[ERC721_TOKEN_NAME";
  let erc721TokenId = "[ID_OF_THE_TOKEN_TO_TRANSFER]";
  let schainName = "[YOUR_SKALE_CHAIN_NAME]";

  let contractObject = initTestTokenContract(ima);
  ima.schain.addERC721Token(tokenName, contractObject);

  let address = "[YOUR_ADDRESS]";
  let privateKey = "[YOUR_PRIVATE_KEY]";

  let txOpts = { // transaction options
    address: address,
    privateKey: privateKey // remove privateKey from txOpts to use Metamask signing
  };

  const erc721OwnerMainnet = await ima.mainnet.getERC721OwnerOf(tokenName, erc721TokenId); // get owner on the Mainnet before the transfer

  await ima.schain.approveERC721Transfer(tokenName, erc721TokenId, txOpts);

  // 1. for single object
  await ima.withdrawERC721(tokenName, address, erc721TokenId, txOpts);
  // 2. for separate sChain object
  await ima.schain.withdrawERC721("[ADDRESS_OF_ERC721_ON_MAINNET]", address, erc721TokenId, txOpts);

  // optional
  await ima.mainnet.waitERC721OwnerChange(tokenName, erc721TokenId, erc721OwnerMainnet); // wait for the owner to be changed on the Mainnet side
}
const Web3 = require('web3');
const Common = require('ethereumjs-common');
const Tx = require('ethereumjs-tx').Transaction;

let schainABIs = "[YOUR_SKALE_CHAIN_ABIs]");
let rinkebyERC721ABI = "[YOUR_RINKEBY_ERC721_ABI]";
let schainERC721ABI = "[YOUR_SKALE_CHAIN_ERC721_ABI]";

let privateKey = new Buffer('[YOUR_PRIVATE_KEY]', 'hex');
let accountForMainnet = "[YOUR_MAINNET_ACCOUNT_ADDRESS]";
let accountForSchain = "[YOUR_SCHAIN_ACCOUNT_ADDRESS]";
let schainEndpoint = "[YOUR_SKALE_CHAIN_ENDPOINT]";
let chainId = "[YOUR_SKALE_CHAIN_CHAIN_ID]";

const customCommon = Common.forCustomChain(
    "mainnet",
    {
      name: "skale-network",
      chainId: chainId
    },
    "istanbul"
  );

let mintId = "[ERC721_MINT_ID]";

const tokenManagerAddress = schainABIs.token_manager_erc721_address;
const tokenManagerABI = schainABIs.token_manager_erc721_abi;

const erc721ABI = schainERC721ABI.erc721_abi;

const erc721Address = schainERC721ABI.erc721_address;
const erc721AddressRinkeby = rinkebyERC721ABI.erc721_address;

const web3ForSchain = new Web3(schainEndpoint);

let tokenManager = new web3ForSchain.eth.Contract(
  tokenManagerABI,
  tokenManagerAddress
);

let contractERC721 = new web3ForSchain.eth.Contract(
  erc721ABI,
  erc721Address
);

/**
   * Uses the openzeppelin ERC721
   * contract function transfer
   * https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC721
   */
let approve = contractERC721.methods
  .approve(tokenManagerAddress, process.env.REACT_APP_MINT_ID)
  .encodeABI();

let exit = tokenManager.methods
  .exitToMainERC721(
    erc721AddressRinkeby,
    accountForMainnet,
    mintId
  )
  .encodeABI();

//get nonce
web3ForSchain.eth.getTransactionCount(accountForSchain).then((nonce) => {

  //create raw transaction (approval)
  const rawTxApprove = {
    from: accountForSchain,
    nonce: "0x" + nonce.toString(16),
    data: approve,
    to: erc721Address,
    gasPrice: 100000000000,
    gas: 8000000
  };

  //sign transaction
  const TxApprove = new Tx(rawTxApprove, { common: customCommon });
    TxApprove.sign(privateKey);

  const serializedTxApprove = TxApprove.serialize();

  //send signed transaction (approval)
  web3ForSchain.eth
    .sendSignedTransaction("0x" + serializedTxApprove.toString("hex"))
    .on("receipt", receipt => {
      console.log(receipt);

      //get next nonce
      web3ForSchain.eth.getTransactionCount(accountForSchain).then(nonce => {

        //create raw transaction (exit)
        const rawTxExit = {
          from: accountForSchain,
          nonce: "0x" + nonce.toString(16),
          data: exit,
          to: tokenManagerAddress,
          gasPrice: 100000000000,
          gas: 8000000
        };

        //sign transaction (exit)
        const txExit = new Tx(rawTxExit, { common: customCommon });
        txExit.sign(privateKey);

        const serializedTxExit = txExit.serialize();

        //send signed transaction (exit)
        web3ForSchain.eth
          .sendSignedTransaction("0x" + serializedTxExit.toString("hex"))
          .on("receipt", receipt => {
            console.log(receipt);
          })
          .catch(console.error);
      });
    })
    .catch(console.error);
});