DevelopersSKALE Developers

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

Javascript
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:

Javascript
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.

Javascript
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.

Live ERC721 IMA Demo

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

Javascript
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

Javascript
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);
});
Developers
  • Support & Learning
  • Languages &
Tooling
  • More
GET DEV UPDATES IN SKALE NEWSLETTER
JOIN THE CONVERSATION
© 2021 SKALE Labs. All right reserved.
skale.networkCookiesPrivacy Policy