Earn $10 in MATIC

Polygon for Builders

Learn how to create tokens and NFTs on Polygon, a side-chain of Ethereum and earn free MATIC tokens.

Lesson 6

• 10 mins

Cross Chain Token

Now that we know the fundamentals of Polygon and how all the tools come together, we will now venture into the technical side of things and learn how to transfer ERC-20 tokens into the Polygon network.  

This is a step-by-step tutorial guide on how to transfer ERC-20 custom token to the Polygon chain using the Polygon PoS SDK. 

Firstly, ensure that you have these three installed:

1.     Metamask

2.     Nodejs v14.17.6 LTS or higher installed

3.     Geth: version 1.10.8 

Before we get started, we need to ensure the contract is mapped. It means that there is already a token contract mirrored between the root (Ethereum) and child (Polygon) chain. 

If the token contract already exists on Polygon, there is no need to map the contract. You can check out the official docs to learn more about mapping.

SDK is used to transfer ERC-20 tokens that are deployed on the Goerli Testnet and Polygon Wallet UI is used for tokens that are deployed on the Ethereum Mainnet. 

Setting up Metamask

Goerli – Ethereum Testnet

Goerli Testnet is a pre-configured network setting that can be found on Metamask’s list of available networks.

You can fund your account with testnet Ether from Goerli Authenticated faucet or an alternative is goerli-faucet.slock.it.

Mumbai – Polygon testnet 

You can add the network manually by using the following information:

·        Network Name: Polygon Mumbai testnet

·        RPC URL: https://rpc-mumbai.maticvigil.com/

·        Chain ID: 80001

·        Currency Symbol: MATIC

·        Block Explorer URL: https://mumbai.polygonscan.com/

Or you can simply add the network by using mumbai.polygonscan.com and click on “Add Mumbai Network”. 

You can fund your account with testnet MATIC here.

Transfer token using SDK

There are two part for transferring token with the SDK: 

1.     Approve: The token holder has to approve the Ethereum Predicate Contract which will lock the amount of token they want to transfer to Polygon.

2.     Deposit: Then a function has to be called on the RootChainManager contract which will trigger the ChildChainManager contract on the Mumbai Testnet. The ChildChainManager contract will then call the deposit function of the Child token contract.

The Child contract is the copy of the Goerli testnet token contract in Mumbai Testnet. 


To interact with the testnet, user can either run a local node (slightly more difficult) or use the RPC endpoints of infrastructure providers like Datahub or Infura (much simpler).

You can also use infura for Goerli testnet and DataHub for Mumbai Testnet. 

Goerli Testnet 

The first thing is to install the Geth client then run: 

geth --goerli --http --syncmode=light --http.api="eth,net,web3,personal,txpool" --allow-insecure-unlock  --http.corsdomain "*"

The default endpoint is

You can get attached and see if everything is fine: 

geth attach

Mumbai Testnet

1.     Choose the Polygon service from the DataHub Services Dashboard

2.     Scroll down to see the Polygon endpoint URLs

3.     Copy the Mumbai Testnet JSONRPC URL that is located here

4.     Form the URL like so, replacing the text YOUR_API_KEY with the API key you got from DataHub: https://matic-mumbai–jsonrpc.datahub.figment.io/apikey/YOUR_API_KEY/

Installing Helpers 

Use these commands to install avd save the packages in the pojrect manifest, package.json: 

npm install @maticnetwork/maticjs --save
npm install @truffle/hdwallet-provider --save 

Approve ERC20 for Deposit 

To approve the Ethereum Predicate Contract, need to call the approveERC20ForDeposit function. The code is:

await maticPOSClient.approveERC20ForDeposit(rootToken, amount.toString(), {
        	gasPrice: "10000000000"

Deposit ERC-20 

To call the Ethereum Predicate Contract, need to call the depositERC20ForUser function. The code is: 

await maticPOSClient.depositERC20ForUser(rootToken, from, amount.toString(), {
  	gasPrice: "10000000000",

To bring it all together in JavaScript that can be executed either in a web browser or on the command line, we can add some constants and use an external file to hold the sensitive API keys and wallet seed phrases. This is a complete example of how to use maticjs and the HDWalletProvider class to communicate with a deployed smart contract on Polygon. Use the following code as a guide for building your own solution!

// main.js
import { HDWalletProvider } from '@truffle/hdwallet-provider';
import { MaticPOSClient } from '@maticnetwork/maticjs');
import { secrets } from './secrets.json' 
const from = "0xD8f24D419153E5D03d614C5155f900f4B5C8A65C";
const rootToken = "0xd2d40892B3EebdA85e4A2742A97CA787559BF92f";
const amount = 999 * (10 ** 18);
const parentProvider = new HDWalletProvider(secrets.seed, ''); // Local Geth client address
const maticProvider = new HDWalletProvider(secrets.seed, secrets.mumbai)  // DataHub Mumbai Testnet JSONRPC URL
const maticPOSClient = new MaticPOSClient({
  network: "testnet",
  version: "mumbai",
(async () => {
  try {
	let result = await maticPOSClient.approveERC20ForDeposit(
    	gasPrice: "10000000000",
	let result_2 = await maticPOSClient.depositERC20ForUser(
    	gasPrice: "10000000000",
  } catch (error) {

The expected output for approveERC20ForDeposit look like this: 

  blockHash: '0x9616fab5f19fb93580fe5dc71da9062168f1f1f5a4a5297094cad0b2b3e2dceb',
  blockNumber: 5513011,
  contractAddress: null,
  cumulativeGasUsed: 46263,
  effectiveGasPrice: '0x2540be400',
  from: '0xd8f24d419153e5d03d614c5155f900f4b5c8a65c',
  gasUsed: 46263,
  logsBloom: '0x0000000000000000000000000000000000000000000000800000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000010000000000000000000000',
  status: true,
  to: '0xd2d40892b3eebda85e4a2742a97ca787559bf92f',
  transactionHash: '0x3aba80ae8938ed1abbb18560cb061f4915d202a731e5e2ec443aded67169e28a',
  transactionIndex: 0,
  type: '0x0',
  events: {
	Approval: {
  	address: '0xd2d40892B3EebdA85e4A2742A97CA787559BF92f',
  	blockNumber: 5513011,
  	transactionHash: '0x3aba80ae8938ed1abbb18560cb061f4915d202a731e5e2ec443aded67169e28a',
  	transactionIndex: 0,
  	blockHash: '0x9616fab5f19fb93580fe5dc71da9062168f1f1f5a4a5297094cad0b2b3e2dceb',
  	logIndex: 0,
  	removed: false,
  	id: 'log_0e714fbf',
  	returnValues: [Result],
  	event: 'Approval',
  	signature: '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
  	raw: [Object]

Expected  output for depositERC20ForUser for user is:

  blockHash: '0x622989e0d1097ea59c557663bf4fa19b3064cfb858706021a6eecb11bb1c19b2',
  blockNumber: 5513012,
  contractAddress: null,
  cumulativeGasUsed: 89761,
  effectiveGasPrice: '0x2540be400',
  from: '0xd8f24d419153e5d03d614c5155f900f4b5c8a65c',
  gasUsed: 89761,
  logsBloom: '0x0200000000000000000000000000000800000040000000800000000000000000000080000000000000040008000000000000200000000000008000100020000000000000000000001000000a000000000000000000000100000000000000000000000000000008000000000400000014000000000000000000000010200000000000000000000000000000000200000000000000000000000000020000080000020000000200008000000000000000040000000000000800000000000000000000000002000000000000000000000002000000140000000000200000000000000010000000000000000000000000000000000000010000000000000000000000',
  status: true,
  to: '0xbbd7cbfa79faee899eaf900f13c9065bf03b1a74',
  transactionHash: '0x58a7f01edc2b9772f87fca57789f0912152615813e6231ab137e4759c8f6415f',
  transactionIndex: 0,
  type: '0x0',
  events: {
	'0': {
  	address: '0xdD6596F2029e6233DEFfaCa316e6A95217d4Dc34',
  	blockNumber: 5513012,
  	transactionHash: '0x58a7f01edc2b9772f87fca57789f0912152615813e6231ab137e4759c8f6415f',
  	transactionIndex: 0,
  	blockHash: '0x622989e0d1097ea59c557663bf4fa19b3064cfb858706021a6eecb11bb1c19b2',
  	logIndex: 0,
  	removed: false,
  	id: 'log_20b9b372',
  	returnValues: Result {},
  	event: undefined,
  	signature: null,
  	raw: [Object]
	'1': {

Just a few things to note: 

  • secrets.json: contains Seed, privateKey of the address (0xd8f2). And Mumbai API URL. ex:
"privateKey": "This should be the private key of an account specifically made for use on the Goerli testnet",
"seed": "This should be a Secret Recovery Phrase from Metamask and ONLY used on Ethereum testnets",
"mumbai": "https://matic-mumbai--jsonrpc.datahub.figment.io/apikey/YOUR_API_KEY/"
  • @truffle/hdwallet-provider: Handles signing transactions proccess 
  • from: The Goerli address we created token and want to send transactions with
  • rootToken: The ERC-20 contract address on the Goerli testnet
  • amount: the amount of token we want transfer. By default, open zeppelin V4 ERC20 contract uses a value of 18 for decimals. That is why 999 is multiplied by (10 ** 18)

Potential Errors

Whilst processing these, there may be some potential errors that may occur.

Not being able to run main.js

Example of the error message:

Error: execution reverted: ERC20: approve to the zero address 

This means that the contract probably has not been mapped yet.

Geth not running

An example of the error message of Geth not working will look something like this:

(node:3962) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:3962) [DEP0018] DeprecationWarning: Unhandled terminate the Node.js process with a non-zero exit code.
  	throw err
Error: PollingBlockTracker - encountered an error while attempting to update latest block:
Error: connect ECONNREFUSED 

No peers available

The error will look like this:

  code: -32000,
  message: 'getDeleteStateObject (0000000000000000000000000000000000000000) error: no suitable peers available'

All you would have to do to rectify the issue is to wait for a few minutes and try again.

Sync & Confirmation

The synchronization and confirmation take up to 5 minutes. Once it is done, the token balance should reflect in Metamask.

Step by step guide on transferring using Web UI:

Do note that the Web UI does not support testnets and it only works on the mainnets. 

Transferring via Web UI is rather simple. 

1.     Use wallet.polygon.technology

2.     Ensure Metamask is connected to the Ethereum Mainnet.

3.     Sign the signature request to have access to the wallet. (0 fees)

4.     Select DAI token as an example and click transfer.

5.     It takes approximately 7 mins to complete the transfer and another 5 minutes for Polygon to sync.

Credits to Mlibra the author of this guide. He is an expert in blockchain tech and you can check out his work over here.

Join Our Mailing List

Subscribe to get the latest updates on crypto education and resources.

Research Reports

Into the Metaverse – A Comprehensive Report


Avalanche: Scaling Towards Digitizing the World’s Assets in a Decentralized Manner

Beginner's Guide
Learn & Earn Crypto

Earn $5 in BTC


Learn the basics of cryptocurrency and how to protect yourself from crypto scams with this 6-part beginner-friendly course, created in collaboration with Luno Discover.

Earn $5 in USDC


Not sure what to do with your crypto? It’s time to learn some popular strategies for investing.

Notable Trending Projects
Polygon Bridge & Staking
4.71% 24H
-18.33% 7D
-0.31% 24H
-31.30% 7D
17.58% 24H
-7.54% 7D
Octopus Network
0.50% 24H
-7.73% 7D
Bastion Protocol
0.00% 24H
-30.33% 7D

About Us

Crypto and blockchain have changed one of the most important aspects of the world: money.

Chain Debrief aims to inform, educate, and connect the global investment community through our crypto guides, news, analyses, and opinion pieces.

What can we help you find?