> For the complete documentation index, see [llms.txt](https://docs.giwa.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.giwa.io/giwa-chain/en/get-started/bridging/erc-20.md).

# Bridging ERC-20 Token

### Requirements

Make sure the following are installed.

* [node](https://nodejs.org/ko)
* [pnpm](https://pnpm.io/)

### Set up development environment

In this tutorial, we’ll use viem. [Viem](https://viem.sh/) is a Node.js library, so we’ll start by creating a Node.js project.

{% stepper %}
{% step %}

#### Create a project folder

```bash
mkdir giwa-bridging-erc20
cd giwa-bridging-erc20
```

{% endstep %}

{% step %}

#### Initialize the project

```bash
pnpm init
```

{% endstep %}

{% step %}

#### Install dependencies

```bash
pnpm add -D tsx @types/node
pnpm add viem@^2.38.0
```

{% endstep %}
{% endstepper %}

### Prepare a wallet

You’ll need a wallet to bridge an ERC-20 token.

{% stepper %}
{% step %}

#### Get Sepolia ETH

You’ll need ETH on both Ethereum Sepolia and GIWA Sepolia to bridge tokens.&#x20;

* [Ethereum Sepolia Faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)
* [GIWA Sepolia Faucet](https://faucet.giwa.io/)

{% hint style="info" %}
Don’t have a wallet yet? Use [cast](https://getfoundry.sh/introduction/installation/) command to create one.
{% endhint %}
{% endstep %}

{% step %}

#### Set the Private Key environment variable

This tutorial requires multiple transaction signatures. To handle this, you’ll need to set your wallet private key as an environment variable.

```bash
export TEST_PRIVATE_KEY=0x...
```

{% endstep %}
{% endstepper %}

### Configure the Chain Client

Set up chain client for ERC-20 bridging.

{% code title="src/config.ts" lineNumbers="true" %}

```typescript
import {defineChain, createPublicClient, http, createWalletClient} from "viem";
import {privateKeyToAccount} from "viem/accounts";
import {publicActionsL1, publicActionsL2, walletActionsL1, walletActionsL2} from "viem/op-stack";
import {sepolia, giwaSepolia} from "viem/chains";

// Prepare wallet
export const PRIVATE_KEY = process.env.TEST_PRIVATE_KEY as `0x${string}`;
export const account = privateKeyToAccount(PRIVATE_KEY);

// Client for reading Ethereum Sepolia chain data
export const publicClientL1 = createPublicClient({
  chain: sepolia,
  transport: http(),
}).extend(publicActionsL1())

// Client for sending transactions on Ethereum Sepolia
export const walletClientL1 = createWalletClient({
  account,
  chain: sepolia,
  transport: http(),
}).extend(walletActionsL1());

// Client for reading GIWA Sepolia chain data
export const publicClientL2 = createPublicClient({
  chain: giwaSepolia,
  transport: http(),
}).extend(publicActionsL2());

// Client for sending transactions on GIWA Sepolia
export const walletClientL2 = createWalletClient({
  account,
  chain: giwaSepolia,
  transport: http(),
}).extend(walletActionsL2());

```

{% endcode %}

### Set contract addresses and ABIs

We’ve deployed a test ERC-20 on Ethereum and GIWA for bridging. The L2 token below is the bridged version of the L1 token.

* [L1 Token](https://eth-sepolia.blockscout.com/address/0x50B1eF6e0fe05a32F3E63F02f3c0151BD9004C7c)
* [L2 Token ](https://sepolia-explorer.giwa.io/address/0xB11E5c9070a57C0c33Df102436C440a2c73a4c38)

Bridging ERC-20 token can be done using [L1StandardBridge](/giwa-chain/en/network-information/contracts.md#l1-contracts) contract. Let's define ABIs for required functions.

{% hint style="info" %}
What is an ABI?

An ABI (Application Binary Interface) is the interface required to interact with a smart contract. It defines the contract’s function names, parameters, and return values. Clients, SDKs, and other tools use the ABI to make calls to the contract.
{% endhint %}

{% code title="src/contract.ts" lineNumbers="true" %}

```typescript
import {giwaSepolia} from "./config";
import {erc20Abi, parseAbi} from "viem";
import {sepolia} from "viem/chains";

// Contract addresses for ERC-20 bridging
export const l1TokenAddress = '0x50B1eF6e0fe05a32F3E63F02f3c0151BD9004C7c';
export const l2TokenAddress = '0xB11E5c9070a57C0c33Df102436C440a2c73a4c38';
export const l1StandardBridgeAddress = giwaSepolia.contracts.l1StandardBridge[sepolia.id].address;
export const l2StandardBridgeAddress = giwaSepolia.contracts.l2StandardBridge.address;

// ABIs for the ERC-20 token, L1StandardBridge and L2StandardBridge
export const testTokenAbi = [
  ...erc20Abi,
  ...parseAbi([
    'function claimFaucet() external'
  ]),
];
export const l1StandardBridgeAbi = parseAbi([
  'function depositERC20To(address _l1Token, address _l2Token, address _to, uint256 _amount, uint32 _minGasLimit, bytes calldata _extraData) external'
]);
export const l2StandardBridgeAbi = parseAbi([
  'function withdrawTo(address _l2Token, address _to, uint256 _amount, uint32 _minGasLimit, bytes calldata _extraData) external'
])

```

{% endcode %}

### Get L1 faucet tokens

To make a deposit (Ethereum -> GIWA), you’ll need faucet tokens on the Ethereum Sepolia network. The L1 token defined above includes a `claimFaucet` function. Run the code below to call `claimFaucet` and receive your L1 faucet tokens.

{% stepper %}
{% step %}

#### Writing the code

{% code title="src/get\_l1\_token.ts" lineNumbers="true" %}

```typescript
import {walletClientL1, account} from "./config";
import {l1TokenAddress, testTokenAbi } from "./contract";

async function main() {
  console.log('Getting tokens from faucet...');
  const claimHash = await walletClientL1.writeContract({
    address: l1TokenAddress,
    abi: testTokenAbi,
    functionName: 'claimFaucet',
    account,
  });
  console.log(`Claim Faucet transaction hash on L1: ${claimHash}`);
  
  const claimReceipt = await publicClientL1.waitForTransactionReceipt({ hash: claimHash });
  console.log('L1 transaction confirmed:', claimReceipt);
}

main().then();

```

{% endcode %}
{% endstep %}

{% step %}

#### Running

```bash
node --import=tsx src/get_l1_token.ts
```

{% endstep %}
{% endstepper %}

### Deposit ERC-20 Token (Ethereum -> GIWA)

Now let’s bridge the faucet tokens from Ethereum to GIWA.

When you run the code below, your faucet tokens on Ethereum will actually be sent to the [L1StandardBridge](/giwa-chain/en/network-information/contracts.md#l1-contracts) contract, and the same amount will be credited to your GIWA wallet. This process is called **Lock-and-Mint** .

{% stepper %}
{% step %}

#### Flow

1. Grant approval to [L1StandardBridge](/giwa-chain/en/network-information/contracts.md#l1-contracts) contract so that it can transfer your ERC-20 tokens
2. Send the deposit transaction on L1
3. The corresponding L2 deposit transaction is created by the sequencer
4. Deposit complete
   {% endstep %}

{% step %}

#### Writing the code

{% code title="src/deposit\_erc20.ts" lineNumbers="true" %}

```typescript
import {
  account,
  publicClientL1,
  walletClientL1
} from "./config";
import {
  l1StandardBridgeAbi,
  l1StandardBridgeAddress,
  l1TokenAddress, l2TokenAddress,
  testTokenAbi
} from "./contract";
import {formatEther, parseUnits} from "viem";
import {getL2TransactionHashes} from "viem/op-stack";

async function main() {
  // Before bridging to GIWA, check your L1 ERC-20 token balance.
  const l1TokenBalance = await publicClientL1.readContract({
    address: l1TokenAddress,
    abi: testTokenAbi,
    functionName: 'balanceOf',
    args: [account.address],
  });
  console.log(`L1 Token Balance: ${formatUnits(l1TokenBalance, 18)} FAUCET`);

  // During bridging, your ERC-20 tokens must be transferred to the L1StandardBridge contract.
  // This is not a direct transfer by you; it happens internally when L1StandardBridge functions execute.
  // First call approve to grant transfer permission to the L1StandardBridge contract.
  const approveHash = await walletClientL1.writeContract({
    address: l1TokenAddress,
    abi: testTokenAbi,
    functionName: 'approve',
    args: [l1StandardBridgeAddress, parseUnits('1', 18)],
  });
  console.log(`Approve transaction hash on L1: ${approveHash}`);

  // Wait until the L1 transaction above is fully processed.
  const approveReceipt = await publicClientL1.waitForTransactionReceipt({ hash: approveHash });
  console.log('L1 transaction confirmed:', approveReceipt);

  // Send the deposit transaction on L1.
  // In this process, your ERC-20 tokens are sent to the L1StandardBridge contract.
  const depositHash = await walletClientL1.writeContract({
    address: l1StandardBridgeAddress,
    abi: l1StandardBridgeAbi,
    functionName: 'depositERC20To',
    args: [
      l1TokenAddress,
      l2TokenAddress,
      account.address,
      parseUnits('1', 18),
      200000,
      '0x',
    ],
  });
  console.log(`Deposit transaction hash on L1: ${depositHash}`);

  // Wait until the L1 transaction above is fully processed.
  const depositReceipt = await publicClientL1.waitForTransactionReceipt({ hash: depositHash });
  console.log('L1 transaction confirmed:', depositReceipt);

  // From the L1 receipt, precompute the L2 deposit transaction hash.
  const [l2Hash] = getL2TransactionHashes(depositReceipt);
  console.log(`Corresponding L2 transaction hash: ${l2Hash}`);
  
  // Wait until the L2 deposit transaction above is processed.
  // This takes roughly 1–3 minutes.
  const l2Receipt = await publicClientL2.waitForTransactionReceipt({
    hash: l2Hash,
  });
  console.log('L2 transaction confirmed:', l2Receipt);
  console.log('Deposit completed successfully!');
}

main().then();

```

{% endcode %}
{% endstep %}

{% step %}

#### Running

```bash
node --import=tsx src/deposit_erc20.ts
```

{% endstep %}
{% endstepper %}

### Withdraw ERC-20 Token (GIWA -> Ethereum)

Have you successfully sent ERC-20 from Ethereum to GIWA? Now let’s bridge it back in the opposite direction — from GIWA to Ethereum.

When you run the code below, your ERC-20 tokens on GIWA are burned, and the same amount is released to your Ethereum wallet. In this case, the tokens locked in the [L1StandardBridge](/giwa-chain/en/network-information/contracts.md#l1-contracts) contract during the Deposit are now unlocked. This process is called **Burn-and-Unlock**.

{% stepper %}
{% step %}

#### Flow

1. Send the withdrawal initiation transaction on L2
2. Send the withdrawal prove transaction on L1
3. Send the withdrawal finalize transaction on L1
4. Withdrawal complete
   {% endstep %}

{% step %}

#### Writing the code

{% code title="src/withdraw\_erc20.ts" lineNumbers="true" %}

```typescript
import {account, publicClientL1, walletClientL1, publicClientL2, walletClientL2} from "./config";
import {l2StandardBridgeAbi, l2StandardBridgeAddress, l2TokenAddress, testTokenAbi} from "./contract";
import {formatUnits, parseUnits} from "viem";

async function main() {
  // Before bridging back to Ethereum, check your L2 ERC-20 token balance.
  const l2TokenBalance = await publicClientL2.readContract({
    address: l2TokenAddress,
    abi: testTokenAbi,
    functionName: 'balanceOf',
    args: [account.address],
  });
  console.log(`L2 Token Balance: ${formatUnits(l2TokenBalance, 18)} FAUCET`);

  // Send the withdrawal transaction on L2.
  // In this process, your ERC-20 tokens are burned.
  const withdrawalHash = await walletClientL2.writeContract({
    address: l2StandardBridgeAddress,
    abi: l2StandardBridgeAbi,
    functionName: 'withdrawTo',
    args: [
      l2TokenAddress,
      account.address,
      parseUnits('0.5', 18),
      200000,
      '0x',
    ],
  });
  console.log(`Withdrawal transaction hash on L2: ${withdrawalHash}`);

  // Wait until the L2 transaction above is fully processed.
  const withdrawalReceipt = await publicClientL2.waitForTransactionReceipt({ hash: withdrawalHash });
  console.log('L2 transaction confirmed:', withdrawalReceipt);

  // Wait until the L2 withdrawal can be proven on L1.
  // This can take up to 2 hours.
  const { output, withdrawal } = await publicClientL1.waitToProve({
    receipt: withdrawalReceipt,
    targetChain: walletClientL2.chain
  });

  // Build parameters to send the prove transaction on L1.
  const proveArgs = await publicClientL2.buildProveWithdrawal({
    output,
    withdrawal,
  });

  // Prove the withdrawal on L1.
  const proveHash = await walletClientL1.proveWithdrawal(proveArgs);
  console.log(`Prove transaction hash on L1: ${proveHash}`);

  // Wait until the L1 transaction above is fully processed.
  const proveReceipt = await publicClientL1.waitForTransactionReceipt({ hash: proveHash });
  console.log('Prove transaction confirmed:', proveReceipt);

  // Wait until the withdrawal can be finalized.
  // This period is called the challenge period and takes about 7 days.
  await publicClientL1.waitToFinalize({
    targetChain: walletClientL2.chain,
    withdrawalHash: withdrawal.withdrawalHash,
  });

  // Finalize the withdrawal on L1.
  const finalizeHash = await walletClientL1.finalizeWithdrawal({
    targetChain: walletClientL2.chain,
    withdrawal,
  });
  console.log(`Finalize transaction hash on L1: ${finalizeHash}`);

  // Wait until the L1 transaction above is fully processed.
  const finalizeReceipt = await publicClientL1.waitForTransactionReceipt({
    hash: finalizeHash
  });
  console.log('Finalize transaction confirmed:', finalizeReceipt);

  // Read withdrawal status on L1.
  // Withdrawals take a long time to complete, so you can query status with this when needed.
  const status = await publicClientL1.getWithdrawalStatus({
    receipt: withdrawalReceipt,
    targetChain: walletClientL2.chain,
  });
  console.log('Withdrawal completed successfully!');
}

main().then();

```

{% endcode %}
{% endstep %}

{% step %}

#### Running

```bash
node --import=tsx src/withdraw_erc20.ts
```

{% endstep %}
{% endstepper %}

## Learn more

Interested in bridging data in addition to assets like ETH and ERC-20 tokens? Read the [OP Stack docs](https://docs.optimism.io/app-developers/tutorials/bridging/cross-dom-solidity) and build your own implementation.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.giwa.io/giwa-chain/en/get-started/bridging/erc-20.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
