Managing ERC20

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

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

Setup and Map ERC20 Transfers

The following one-time setup for each ERC20 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.

To see a complete Javascript sandbox version of setup, map, and transfer, see https://codesandbox.io/s/erc20-transfer-skale-interchain-messaging-agent-forked-w1t5m

1. Review/Modify the token contract

First, review the Mainnet ERC20 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.22 <0.6.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Mintable.sol";

interface tokenRecipient {
    function receiveApproval(address _from, uint256 _value, address _token, bytes calldata _extraData) external;
}

contract MyERC20 is ERC20, ERC20Detailed, ERC20Mintable{

    constructor(
    uint256 _amount,
    string memory _name,
    string memory _symbol,
    uint256 _decimals

  )
    ERC20Detailed(_name, _symbol, 18)
    public
  {
    require(_amount > 0, "amount has to be greater than 0");
    _mint(msg.sender, _amount.mul(10 ** uint256(_decimals)));
    }
}

Example Modified SKALE Chain contract

pragma solidity >=0.4.22 <0.6.0;

import "@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Mintable.sol";

contract MyERC20 is ERC20Burnable, ERC20Detailed, ERC20Mintable {

    constructor(
      string memory _name,
      string memory _symbol,
      uint256 _decimals
    )
    ERC20Detailed(_name, _symbol, 18)
    public
    {
    }
}

If you aren’t using OpenZeppelin’s framework, then you can simply manually add Mintable and Burnable functions, and finally add MINTER_ROLE access control.

2. Add a Minter Role

Now you need to add the pre-deployed TokenManagerERC20 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

include::example$add-minter-role-erc20.js

3. Register Ethereum Mainnet contract to IMA

Third, you need to register the Ethereum Mainnet token contract into IMA on Ethereum Mainnet using the addERC20TokenByOwner method in the DepositBoxERC20 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 linkERC20TokenMainnet(ima) {
    let schainName = "YOUR_SKALE_CHAIN_NAME";
    let erc20OnMainnet = "ADDRESS_OF_ERC20_TOKEN_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
    };

    const isERC20AddedMainnet = await ima.mainnet.isERC20Added(schainName, erc20OnMainnet);
    if (!isERC20AddedMainnet) { // check if token is already added
        await ima.mainnet.addERC20TokenByOwner(schainName, erc20OnMainnet, txOpts);
    }
}
const Web3 = require("web3");
const Tx = require("ethereumjs-tx").Transaction;

export function registerOnMainnet() {
    let rinkebyABIs = require("./contracts/rinkeby_ABIs.json");
    let rinkebyERC20ABI = require("./contracts/rinkeby_ERC20_ABI.json");

    let privateKey = Buffer.from(
        "SCHAIN_OWNER_PRIVATE_KEY",
        "hex"
    );
    let erc20OwnerForMainnet =
        process.env.REACT_APP_INSECURE_SCHAIN_OWNER_ACCOUNT;

    let rinkeby = process.env.REACT_APP_INSECURE_RINKEBY;
    let schainName = process.env.REACT_APP_INSECURE_CHAIN_NAME;
    let chainId = process.env.REACT_APP_INSECURE_RINKEBY_CHAIN_ID;

    const depositBoxAddress = rinkebyABIs.deposit_box_erc20_address;
    const depositBoxABI = rinkebyABIs.deposit_box_erc20_abi;

    const erc20AddressOnMainnet = rinkebyERC20ABI.erc20_address;

    const web3ForMainnet = new Web3(rinkeby);

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

    /**
     * Uses the SKALE DepositBoxERC20
     * contract function addERC20TokenByOwner
     */
    let addERC20TokenByOwner = DepositBox.methods
        .addERC20TokenByOwner(schainName, erc20AddressOnMainnet)
        .encodeABI();

    web3ForMainnet.eth.getTransactionCount(erc20OwnerForMainnet).then((nonce) => {
        const rawTxAddERC20TokenByOwner = {
            chainId: chainId,
            from: erc20OwnerForMainnet,
            nonce: "0x" + nonce.toString(16),
            data: addERC20TokenByOwner,
            to: depositBoxAddress,
            gas: 6500000,
            gasPrice: 100000000000,
            value: web3ForMainnet.utils.toHex(
                web3ForMainnet.utils.toWei("0", "ether")
            )
        };

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

        txAddERC20TokenByOwner.sign(privateKey);

        const serializedTxDeposit = txAddERC20TokenByOwner.serialize();

        //send signed transaction (addERC20TokenByOwner)
        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 addERC20TokenByOwner method in TokenManagerERC20 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 linkERC20TokenSchain(ima) {
    let erc20OnMainnet = "[ADDRESS_OF_ERC20_TOKEN_ON_MAINNET]";
    let erc20OnSchain = "[ADDRESS_OF_ERC20_TOKEN_ON_SCHAIN]";

    const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

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

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

    const isERC20AddedSchain = await this.schain.isERC20Added(erc20OnMainnet);
    if (isERC20AddedSchain === ZERO_ADDRESS) { // check if token is already added
        await this.schain.addERC20TokenByOwner(erc20OnMainnet, erc20OnSchain, txOpts);
    }
}
include::example$erc20-register-schain-web3.js

For a Javascript sandbox version of registering contracts on your SKALE Chain, see https://codesandbox.io/s/erc20-transfer-skale-interchain-messaging-agent-forked-w1t5m?file=/src/schain_addERC20TokenByOwner.js

Show All Mapped Tokens

To view all mapped tokens (including those mapped manually and automatically)

  • IMA-JS

  • Web3 JS

let erc20Len = await ima.mainnet.getSchainToAllERC20Length(SCHAIN_NAME);
let allErc20Tokens = await ima.mainnet.getSchainToAllERC20(SCHAIN_NAME, 0, erc20Len);

You can use the getSchainToAllERC20Length() and getSchainToAllERC20() functions to return the length and an array of a set of mapped tokens. Note that getSchainToAllERC20() can only return a maximum set of 10 tokens at a time.

const Web3 = require('web3');

let rinkebyABIs = "[YOUR_SKALE_ABIs_ON_RINKEBY]";

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

const depositBoxAddress = rinkebyABIs.deposit_box_erc20_address;
const depositBoxABI = rinkebyABIs.deposit_box_erc20_abi;

const web3ForMainnet = new Web3(rinkeby);

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

let mappedNumber = depositBox.methods
  .getSchainToAllERC20Length(
    schainName
  )
  .call();

let mappedArray = Array();
for (let i = 0; i < (mappedNumber + 9) / 10; i++) {
    let nextIndex = ((i + 1) * 10 < mappedNumber ? (I + 1) * 10 : mappedNumber);
    let mappedArrayPart = depositBox.methods.getSchainToAllERC20(schainName, i * 10, nextIndex).call();
    mappedArrayPart.forEach( (address) => { mappedArray.push(address) });
}

Get Started with ERC20 Transfer

The Interchain Messaging Agent can be used for managing ERC20 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 before initiating transfers.

1. Deposit ERC20 on Ethereum

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

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

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

  • 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 ERC20 contract
    const abiData = require("[ERC20_ABI_ON_ETHEREUM]");
    return new ima.mainnet.web3.eth.Contract(
        abiData.erc20_abi,
        abiData.erc20_address);
}

export async function depositERC20(ima) {
    let tokenName = "[ERC20_TOKEN_NAME";
    let schainName = "[YOUR_SKALE_CHAIN_NAME]";
    let amount = "[AMOUNT_IN_WEI]";

    let contractObject = initTestTokenContract(ima);
    ima.mainnet.addERC20Token(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 balanceSchain = await ima.schain.getERC20Balance(tokenName, address); // get sChain balance before the transfer

    await ima.mainnet.approveERC20Transfers(
        tokenName,
        amount,
        txOpts
    );

    await ima.mainnet.depositERC20(
        schainName,
        tokenName,
        txOpts
    );

    // optional
    await ima.schain.waitERC20BalanceChange(tokenName, address, balanceSchain); // wait for the balance 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 rinkebyERC20ABI = "[YOUR_ERC20_ABI_ON_RINKEBY]";

let privateKeyForMainnet = Buffer.from("[YOUR_MAINNET_ACCOUNT_PRIVATE_KEY]", 'hex')

let accountForMainnet = "[YOUR_MAINNET_ACCOUNT_ADDRESS]";
let accountForSchain = "[YOUR_SCHAIN_ACCOUNT_ADDRESS]";

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

const depositBoxAddress = rinkebyABIs.deposit_box_erc20_address;
const depositBoxABI = rinkebyABIs.deposit_box_erc20_abi;

const erc20ABI = rinkebyERC20ABI.erc20_abi;
const erc20Address = rinkebyERC20ABI.erc20_address;

const web3ForMainnet = new Web3(rinkeby);

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

let contractERC20 = new web3ForMainnet.eth.Contract(erc20ABI, erc20Address);

let approve = contractERC20.methods
  .approve(
    depositBoxAddress,
    web3ForMainnet.utils.toHex(web3ForMainnet.utils.toWei("1", "ether"))
  )
  .encodeABI();

let deposit = depositBox.methods
  .depositERC20(
    schainName,
    erc20Address,
    web3ForMainnet.utils.toHex(web3ForMainnet.utils.toWei("1", "ether"))
  )
  .encodeABI();

web3ForMainnet.eth.getTransactionCount(accountForMainnet).then(nonce => {
  //create raw transaction
  const rawTxApprove = {
    chainId: chainId,
    from: accountForMainnet,
    nonce: "0x" + nonce.toString(16),
    data: approve,
    to: erc20Address,
    gas: 6500000,
    gasPrice: 100000000000
  };

  //sign transaction
  const txApprove = new Tx(rawTxApprove, {
    chain: "rinkeby",
    hardfork: "petersburg"
  });
  txApprove.sign(privateKeyForMainnet);

  const serializedTxApprove = txApprove.serialize();

  //send signed transaction (approve)
  web3ForMainnet.eth
    .sendSignedTransaction("0x" + serializedTxApprove.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(privateKeyForMainnet);

          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 ERC20 tokens back to Ethereum, you will need to use the exitToMainERC20 function within the TokenManagerERC20 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:

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

  • a sufficient balance of ETH (1,000,000 gas * gas price in wei at time of transaction submission) in the Community Pool to initiate the exit to Ethereum See Funding Exits.

  • not been locked by the CommunityLocker.

  • not have exceeded the total SKALE Chain exit message rate (default is 5 minutes per SKALE Chain exit message). See Community Locker. To get the last message timestamp, perform an eth_call to CommunityLocker.lastMessageTimeStamp.

The TokenManagerERC20 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.

  • 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 ERC20 contract
    const abiData = require("[ERC20_ABI_ON_SCHAIN]");
    return new ima.schain.web3.eth.Contract(
      abiData.erc20_abi,
      abiData.erc20_address);
  }

  export async function exit(ima) {
    let tokenName = "[ERC20_TOKEN_NAME]";
    let mainnetERC20TokenAddress = "[ERC20_MAINNET_TOKEN_ADDRESS]";
    let amount = "[AMOUNT_IN_WEI]";

    let contractObject = initTestTokenContract(ima);
    ima.schain.addERC20Token(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 balanceMainnet = await ima.mainnet.getERC20Balance(tokenName, address); // get Mainnet balance before the transfer

    await ima.schain.approveERC20Transfers(
      tokenName,
      "MAX_APPROVAL_IN_WEI",
      txOpts
    );

    await ima.schain.withdrawERC20(
      mainnetERC20TokenAddress,
      amount,
      txOpts
    );

    // optional
    await ima.mainnet.waitERC20BalanceChange(tokenName, address, balanceMainnet); // wait for the balance to be changed on the Mainnet side
  }
const Web3 = require("web3");
import Common from "ethereumjs-common";
const Tx = require("ethereumjs-tx").Transaction;

let schainABIs = "[YOUR_SKALE_CHAIN_ABIs]";
let rinkebyERC20ABI = "[YOUR_RINKEBY_ERC20_ABI]";
let schainERC20ABI = "[YOUR_SKALE_CHAIN_ERC20_ABI]";

let privateKeyForSchain = Buffer.from("YOUR_SCHAIN_ADDRESS_PRIVATE_KEY", 'hex')

let accountForSchain = "[YOUR_SCHAIN_ADDRESS]";
let schainEndpoint = "[YOUR_SKALE_CHAIN_ENDPOINT]";
let chainId = "YOUR_SCHAIN_CHAIN_ID";

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

const tokenManagerAddress = schainABIs.token_manager_erc20_address;
const tokenManagerABI = schainABIs.token_manager_erc20_abi;

const schainERC20ABI = schainERC20ABI.erc20_abi;
const schainERC20Address = schainERC20ABI.erc20_address;

const erc20AddressOnMainnet = rinkebyERC20ABI.erc20_address;

const web3ForSchain = new Web3(schainEndpoint);

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

let contractERC20 = new web3ForSchain.eth.Contract(
  schainERC20ABI,
  schainERC20Address
);

//approve the ERC20 transfer
let approve = contractERC20.methods
  .approve(
    tokenManagerAddress,
    web3ForSchain.utils.toHex(web3ForSchain.utils.toWei("1", "ether"))
  )
  .encodeABI();

/**
   * Uses the SKALE TokenManager
   * contract function exitToMainERC20
   */
let exit = tokenManager.methods
  .exitToMainERC20(
    erc20AddressOnMainnet,
    web3ForSchain.utils.toHex(web3ForSchain.utils.toWei("1", "ether"))
  )
  .encodeABI();

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

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

  //sign transaction (approval)
  const txApprove = new Tx(rawTxApprove, { common: customCommon });
  txApprove.sign(privateKeyForSchain);

  //serialize transaction  (approval)
  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 = {
          chainId: chainId,
          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(privateKeyForSchain);

        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);
});