> ## Documentation Index
> Fetch the complete documentation index at: https://docs.optimism.io/llms.txt
> Use this file to discover all available pages before exploring further.

# OP Mainnet으로 ERC-20 토큰 브리징하기

> @eth-optimism/viem 및 viem 패키지를 사용하여 Layer 1(Ethereum 또는 Sepolia)과 Layer 2(OP Mainnet 또는 OP Sepolia) 간에 ERC-20 토큰을 전송하는 방법을 알아봅니다.

이 튜토리얼에서는 [@eth-optimism/viem](https://www.npmjs.com/package/@eth-optimism/viem)과 [viem](https://viem.sh/op-stack)을 사용하여 L1(Ethereum 또는 Sepolia)과 L2(OP Mainnet 또는 OP Sepolia) 간에 ERC-20 토큰을 브리징하는 방법을 설명합니다.
`@eth-optimism/viem` 및 `viem` 패키지를 사용하면 JavaScript 기반 애플리케이션에 브리징 기능을 쉽게 추가할 수 있습니다.
또한 토큰에 접근할 수 없게 만드는 일반적인 실수를 방지하기 위한 몇 가지 안전장치도 제공합니다.

내부적으로 `@eth-optimism/viem` 패키지는 [Standard Bridge](https://docs.optimism.io/ko/app-developers/guides/bridging/standard-bridge.md) 컨트랙트를 사용하여 토큰을 전송합니다.
bridge가 내부적으로 어떻게 동작하는지 자세히 알아보려면 [Standard Bridge 가이드](https://docs.optimism.io/ko/app-developers/guides/bridging/standard-bridge.md)를 꼭 확인하십시오.

> [!NOTE]
>
> Standard Bridge는 bridge 회계 오류를 일으킬 수 있으므로 [**전송 수수료 토큰(fee on transfer tokens)**](https://github.com/d-xo/weird-erc20#fee-on-transfer)이나 [**리베이싱 토큰(rebasing tokens)**](https://github.com/d-xo/weird-erc20#balance-modifications-outside-of-transfers-rebasingairdrops)을 **지원하지 않습니다**.

## 지원되는 네트워크

Viem은 모든 [OP Stack 네트워크](https://viem.sh/op-stack/chains)를 지원합니다.
기본적으로 포함되지 않은 네트워크를 사용하려면 Viem의 체인 [구성](https://viem.sh/op-stack/chains#configuration)에 추가할 수 있습니다.

## 의존성

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

## 데모 프로젝트 생성

이 튜토리얼에서는 @eth-optimism/viem 라이브러리를 사용합니다.
@eth-optimism/viem는 [Node.js](https://nodejs.org/en/) 라이브러리이므로, 이를 사용하려면 Node.js 프로젝트를 생성해야 합니다.

**1. 프로젝트 폴더 만들기**

```bash theme={null}
mkdir bridging-erc20-tokens
cd bridging-erc20-tokens
```

**2. 프로젝트 초기화**

```bash theme={null}
pnpm init
```

**3. 의존성 설치**

```bash theme={null}
pnpm add @eth-optimism/viem viem
```

## Sepolia 및 OP Sepolia에서 ETH 받기

이 튜토리얼에서는 Sepolia에서 OP Sepolia로 토큰을 브리징하는 방법을 설명합니다.
두 testnet 모두에서 약간의 ETH가 필요합니다.

> [!NOTE]
>
> [이 faucet](https://sepoliafaucet.com)을 사용하여 Sepolia에서
> ETH를 받을 수 있습니다.
>
> [Superchain Faucet](https://console.optimism.io/faucet?utm_source=op-docs\&utm_medium=docs)
> 을 사용하여 OP Sepolia에서 ETH를 받을 수 있습니다.

## 환경에 개인 키 추가

트랜잭션에 서명하려면 개인 키가 필요합니다.
`export` 명령으로 개인 키를 환경 변수로 설정하십시오.
이 개인 키가 both Sepolia and OP Sepolia에서 ETH를 보유한 주소에 해당하는지 확인하십시오.

> [!NOTE]
>
> 이 튜토리얼을 위해 새 지갑을 만들고 싶으신가요?
> [`cast`](https://book.getfoundry.sh/getting-started/installation)가 설치되어 있다면 터미널에서 `cast wallet new`를 실행하여 새 지갑을 만들고 개인 키를 얻을 수 있습니다.

```bash theme={null}
export TUTORIAL_PRIVATE_KEY=0x...
```

## Node REPL 시작

Node REPL을 사용하여 @eth-optimism/viem와 상호작용합니다.
Node REPL을 시작하려면 터미널에서 다음 명령을 실행하십시오.

```bash theme={null}
node
```

그러면 JavaScript 코드를 실행할 수 있는 Node REPL 프롬프트가 열립니다.

## 의존성 가져오기

Node REPL 세션에 몇 가지 의존성을 가져와야 합니다.
`@eth-optimism/viem` 패키지는 ESM 모듈을 사용하므로, Node.js REPL에서 사용하려면 await와 함께 동적 import를 사용해야 합니다.
방법은 다음과 같습니다.

**1. @eth-optimism/viem 패키지 가져오기**

```js theme={null}

const viem = await import('viem');
const { createPublicClient, createWalletClient, http, formatEther, parseEther} = viem;
const accounts = await import('viem/accounts');
const { privateKeyToAccount } = accounts;
const viemChains = await import('viem/chains');
const { optimismSepolia, sepolia } = viemChains;
const opActions = await import('@eth-optimism/viem/actions');
const { depositERC20, withdrawOptimismERC20 } = opActions;
```

## 세션 변수 설정

이 튜토리얼 전반에 걸쳐 몇 가지 변수가 필요합니다.
지금 설정해 보겠습니다.

**1. 개인 키 불러오기**

이 단계에서는 앞서 설정한 환경 변수에서 개인 키를 가져와 Viem이 트랜잭션 서명에 사용할 수 있는 계정 객체로 변환합니다.

개인 키는 L1 및 L2 네트워크 모두에서 트랜잭션을 승인하는 데 필수적입니다.
보안상의 이유로 하드코딩하지 않고 환경 변수에서 가져옵니다.

```js theme={null}
const PRIVATE_KEY = process.env.TUTORIAL_PRIVATE_KEY;
const account = privateKeyToAccount(PRIVATE_KEY);
```

**2. RPC provider 및 지갑 생성**

여기서는 네 가지 클라이언트를 생성하여 두 네트워크에 대한 연결을 설정합니다.

1. L1 Public Client: Sepolia 네트워크에서 데이터를 읽기 위한 클라이언트
2. L1 Wallet Client: Sepolia에서 트랜잭션에 서명하고 전송하기 위한 클라이언트
3. L2 Public Client: OP Sepolia에서 데이터를 읽기 위한 클라이언트
4. L2 Wallet Client: OP Sepolia에서 트랜잭션에 서명하고 전송하기 위한 클라이언트

각 클라이언트는 적절한 체인 정보와 RPC 엔드포인트로 구성됩니다.
이러한 이중 네트워크 설정을 통해 동일한 계정으로 두 레이어와 원활하게 상호작용할 수 있습니다.
`<YOUR_API_KEY>`를 RPC provider에서 발급받은 API 키로 바꾸십시오.

```js theme={null}
const L1_RPC_URL = 'https://ethereum-sepolia-rpc.publicnode.com';
const L2_RPC_URL = 'https://sepolia.optimism.io';

const publicClientL1 = createPublicClient({
  chain: sepolia,
  transport: http(L1_RPC_URL),
});

const walletClientL1 = createWalletClient({
  account,
  chain: sepolia,
  transport: http(L1_RPC_URL),
});

const publicClientL2 = createPublicClient({
  chain: optimismSepolia,
  transport: http(L2_RPC_URL),
});

const walletClientL2 = createWalletClient({
  account,
  chain: optimismSepolia,
  transport: http(L2_RPC_URL),
});
```

**3. L1 및 L2 ERC-20 주소 설정**

두 네트워크에 있는 ERC-20 토큰의 주소를 정의합니다.
이 토큰들은 L1(Sepolia)과 L2(OP Sepolia) 모두에 대응하는 구현이 있도록 특별히 배포된 테스트 토큰입니다.

L2 토큰은 L1 대응 토큰으로부터의 입금(디파짓)을 인식하도록 구성되어 있습니다.
또한 입금 및 출금 작업을 단순화하기 위해 전체 단위(10^18 wei)를 나타내는 상수 `oneToken`을 정의합니다.

```js theme={null}
const l1Token = "0x5589BB8228C07c4e15558875fAf2B859f678d129";
const l2Token = "0xD08a2917653d4E460893203471f0000826fb4034";
```

> [!NOTE]
>
> [Standard Bridge용 L2 토큰 생성](https://docs.optimism.io/ko/app-developers/tutorials/bridging/standard-bridge-standard-token.md) 튜토리얼에서 넘어왔다면
> 여기에 직접 만든 ERC-20 토큰의 주소를 대신 사용할 수 있습니다.

## L1 토큰 받기

L2로 브리징할 수 있는 L1 토큰이 필요합니다.
[`0x5589BB8228C07c4e15558875fAf2B859f678d129`](https://sepolia.etherscan.io/address/0x5589BB8228C07c4e15558875fAf2B859f678d129)에 있는 L1 테스트 토큰에는 토큰을 쉽게 받을 수 있는 `faucet` 함수가 있습니다.

**1. ERC20 ABI 설정**

ABI(Application Binary Interface)는 스마트 컨트랙트 함수와 상호작용하는 방법을 정의합니다. 이 ERC-20 ABI에는 다음과 같은 몇 가지 중요한 함수가 포함되어 있습니다.

* `balanceOf`: 임의의 주소에 대한 토큰 잔액을 확인할 수 있습니다
* `faucet`: 이 테스트 토큰에 있는 특수 함수로, 호출자에게 새 토큰을 발행합니다
* `approve`: bridge가 사용자를 대신하여 토큰을 전송할 수 있도록 권한을 부여하는 데 필요합니다
* `allowance`: bridge에 승인한 토큰 수량을 확인합니다
* `decimals` 및 `symbol`: 토큰 메타데이터를 제공합니다

이 포괄적인 ABI는 L1과 L2 모두에서 토큰을 관리하는 데 필요한 모든 것을 제공합니다.

```js theme={null}
const erc20ABI = [
  {
    inputs: [
      {
        internalType: "address",
        name: "account",
        type: "address",
      },
    ],
    name: "balanceOf",
    outputs: [
      {
        internalType: "uint256",
        name: "",
        type: "uint256",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [],
    name: "faucet",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "spender",
        type: "address"
      },
      {
        internalType: "uint256",
        name: "value",
        type: "uint256"
      }
    ],
    name: "approve",
    outputs: [
      {
        internalType: "bool",
        name: "",
        type: "bool"
      }
    ],
    stateMutability: "nonpayable",
    type: "function"
  },
];
```

**2. 토큰 요청**

이제 L1 테스트 토큰 컨트랙트의 `faucet` 함수를 호출하여 테스트용 무료 토큰을 받습니다.
이 트랜잭션은 지갑 주소로 새 토큰을 직접 발행합니다.

이 함수는 매개변수가 필요하지 않으며, 호출한 사람에게 미리 정해진 수량을 지급합니다.
나중에 참조할 수 있도록 트랜잭션 해시를 저장하고 트랜잭션이 확인될 때까지 기다립니다.

```js theme={null}
console.log('Getting tokens from faucet...');
const tx = await walletClientL1.writeContract({
  address: l1Token,
  abi: erc20ABI,
  functionName: 'faucet',
  account,
});
console.log('Faucet transaction:', tx);
```

**3. 토큰 잔액 확인**

faucet을 사용한 후 L1 토큰 컨트랙트의 `balanceOf` 함수를 호출하여 토큰 잔액을 확인합니다.

이 단계는 브리징 과정을 진행하기 전에 토큰을 성공적으로 받았는지 확인합니다.
잔액은 가장 작은 단위(wei)로 반환되지만, 이 토큰은 소수점 18자리를 사용하므로 `viem`의 `formatEther` 유틸리티 함수를 사용하여 더 읽기 쉬운 형태로 변환합니다.

```js theme={null}
const l1Balance = await publicClientL1.readContract({
  address: l1Token,
  abi: erc20ABI,
  functionName: 'balanceOf',
  args: [account.address]
});
console.log(`L1 Balance after receiving faucet: ${formatEther(l1Balance)}`);
```

## 토큰 입금하기

이제 L1에 토큰이 있으므로 해당 토큰을 `L1StandardBridge` 컨트랙트에 입금할 수 있습니다.
그러면 그 대가로 L2에서 같은 수량의 토큰을 받게 됩니다.

**1. 입금할 수량 정의**

기본 단위(wei)로 1개의 전체 토큰을 나타내는 변수 `oneToken`을 정의합니다.
ERC-20 토큰은 일반적으로 소수점 18자리를 사용하므로 1 토큰은 10^18 wei와 같습니다.

이 상수를 사용하면 트랜잭션에서 정확한 토큰 수량을 다룰 수 있어 반올림 오류를 피하고 정확한 값을 전송할 수 있습니다.
이 값은 입금과 출금 모두에 사용합니다.

```js theme={null}
const oneToken = parseEther('1')
```

**2. Standard Bridge가 토큰에 접근하도록 허용**

ERC-20 토큰은 사용자를 대신하여 토큰을 전송하려면 2단계 과정이 필요합니다.
먼저 토큰 컨트랙트의 `approve` 함수를 호출하여 bridge 컨트랙트가 토큰을 사용할 수 있도록 권한을 부여해야 합니다.

체인 구성에서 가져온 bridge 주소와 브리징하려는 정확한 수량을 지정합니다.
bridge가 토큰을 이동하려면 먼저 이 승인 트랜잭션이 확인되어야 합니다.

```js theme={null}
const bridgeAddress = optimismSepolia.contracts.l1StandardBridge[sepolia.id].address;
const approveTx = await walletClientL1.writeContract({
  address: l1Token,
  abi: erc20ABI,
  functionName: 'approve',
  args: [bridgeAddress, oneToken],
});
console.log('Approval transaction:', approveTx);
```

**3. 승인 대기**

승인 트랜잭션을 제출한 후 L1에서 확인될 때까지 기다려야 합니다.
`waitForTransactionReceipt` 함수를 사용하여 트랜잭션이 블록에 포함될 때까지 모니터링합니다.

영수증(receipt)은 트랜잭션이 포함된 블록 등 확인 세부 정보를 제공합니다.
이 단계를 통해 토큰을 브리징하기 전에 승인이 확정되었는지 확인합니다.

```js theme={null}
await publicClientL1.waitForTransactionReceipt({ hash: approveTx });
```

**4. 토큰 입금**

이제 `@eth-optimism/viem` 패키지의 `depositERC20` 함수를 사용하여 실제 브리징 작업을 실행할 수 있습니다.

이 함수는 `L1StandardBridge` 컨트랙트와의 모든 복잡한 상호작용을 대신 처리합니다.
다음을 제공합니다.

* L1 및 L2 토큰의 주소
* 브리징할 수량
* 대상 체인(OP Sepolia)
* L2에서의 수신자로서 지갑 주소
* L2 트랜잭션의 최소 gas 한도

이 간소화된 과정을 통해 토큰이 L2로 안전하게 전송됩니다.

```js theme={null}
console.log('Depositing tokens to L2...');
const depositTx = await depositERC20(walletClientL1, {
  tokenAddress: l1Token,
  remoteTokenAddress: l2Token,
  amount: oneToken,
  targetChain: optimismSepolia,
  to: account.address,
  minGasLimit: 200000,
});
console.log(`Deposit transaction hash: ${depositTx}`);
```

> [!NOTE]
>
> 스마트 컨트랙트 지갑을 사용하고 있습니까? 안전 조치로서, `recipient`를 지정하지 않고
> 스마트 컨트랙트 지갑에서 ETH를 입금하려고 하면 `depositERC20`이 실패합니다.
> 이 문제를 해결하려면 `depositERC20` 호출에 `recipient` 옵션을 추가하십시오.
> `depositERC20`에 전달할 수 있는 옵션에 대한 자세한 내용은 [@eth-optimism/viem
> 문서](https://github.com/ethereum-optimism/ecosystem/tree/main/packages/viem)를 확인하십시오.

**5. 입금이 릴레이될 때까지 대기**

입금을 시작한 후 L1 트랜잭션이 확인될 때까지 기다려야 합니다.
이 함수는 트랜잭션이 L1 블록에 포함될 때까지 추적합니다.

이를 통해 L1에서 입금이 수락되었음을 확인할 수 있지만, 트랜잭션이 Optimism sequencer에 의해 처리되어야 하므로 토큰이 L2에 표시되기까지 약간의 지연(일반적으로 몇 분)이 여전히 있다는 점에 유의하십시오.

```js theme={null}
const depositReceipt = await publicClientL1.waitForTransactionReceipt({ hash: depositTx });
console.log(`Deposit confirmed in block ${depositReceipt.blockNumber}`);
```

**6. L1에서 토큰 잔액 확인**

입금 트랜잭션이 확인된 후 L1에서 토큰 잔액을 다시 확인하여 토큰이 차감되었는지 확인합니다.

해당 토큰은 이제 `L1StandardBridge` 컨트랙트에 에스크로되어 있으므로, 잔액은 브리징한 수량만큼 줄어들어야 합니다.
이 단계는 브리징 과정의 첫 번째 부분이 성공적으로 완료되었는지 확인하는 데 도움이 됩니다.

```js theme={null}
const l1BalanceAfterDeposit = await publicClientL1.readContract({
  address: l1Token,
  abi: erc20ABI,
  functionName: 'balanceOf',
  args: [account.address]
});
console.log(`L1 Balance after deposit: ${formatEther(l1BalanceAfterDeposit)}`);
```

**7. L2에서 토큰 잔액 확인**

L2 트랜잭션이 처리될 때까지 잠시 기다린 후 L2에서 토큰 잔액을 확인하여 브리징된 토큰을 받았는지 확인합니다.

새로 발행된 L2 토큰은 L1에서 사용한 것과 동일한 주소의 지갑에 표시되어야 합니다.
이 단계는 L1에서 L2로의 bridge 작업이 완전히 성공했음을 확인합니다.

```js theme={null}
const l2Balance = await publicClientL2.readContract({
  address: l2Token,
  abi: erc20ABI,
  functionName: 'balanceOf',
  args: [account.address]
});
console.log(`L2 Balance after withdrawal: ${formatEther(l2Balance)}`);
```

## 토큰 출금하기

방금 L1에서 L2로 토큰을 브리징했습니다.
잘하셨습니다!
이제 과정을 반대로 반복하여 L2에서 L1로 토큰을 브리징합니다.

**1. 출금 시작**

토큰을 L1으로 다시 옮기려면 `@eth-optimism/viem` 패키지의 `withdrawOptimismERC20` 함수를 사용합니다.
이 함수는 `L2StandardBridge` 컨트랙트와 상호작용하여 출금 과정을 시작합니다.
다음을 지정합니다.

* L2 토큰 주소
* 출금할 수량(이 튜토리얼에서는 토큰 절반을 사용합니다)
* L1에서의 수신자로서 사용자 주소
* 트랜잭션의 최소 gas 한도

입금과 달리 L2에서 L1으로의 출금은 즉시 이루어지지 않으며, 보안상의 이유로 7일간의 챌린지 기간을 포함한 여러 단계의 과정이 필요합니다.

```js theme={null}
console.log('Withdrawing tokens back to L1...');
const withdrawTx = await withdrawOptimismERC20(walletClientL2, {
  tokenAddress: l2Token,
  amount: oneToken / 2n, 
  to: account.address,
  minGasLimit: 200000,
});
console.log(`Withdrawal transaction hash: ${withdrawTx}`);
```

**2. 트랜잭션 영수증 대기**

입금과 마찬가지로 출금 트랜잭션이 L2에서 확인될 때까지 기다립니다.
이 영수증은 출금이 시작되었음을 확인해 줍니다.

트랜잭션 로그에는 이후 출금 검증 과정에서 사용될 중요한 정보가 담겨 있습니다.
이것은 출금의 첫 번째 단계일 뿐이며, 토큰은 이제 L2에서 잠겨 있지만 아직 L1에서 사용할 수는 없습니다.

```js theme={null}
const withdrawReceipt = await publicClientL2.waitForTransactionReceipt({ hash: withdrawTx });
console.log(`Withdrawal initiated in L2 block ${withdrawReceipt.blockNumber}`);
```

> [!NOTE]
>
> 이 단계는 몇 분 정도 걸릴 수 있습니다. 기다리는 동안 잠시 쉬어도
> 좋습니다.

**3. L2에서 토큰 잔액 확인**

출금 트랜잭션이 확인된 후 L2에서 토큰 잔액을 다시 확인하여 토큰이 차감되었는지 확인합니다.
이제 L2 잔액은 출금을 시작한 수량만큼 줄어들어야 합니다.

이 시점에서 출금 과정은 시작되었지만 토큰은 아직 L1에서 사용할 수 없습니다. "증명(prove)" 및 "최종 확정(finalize)" 출금 단계를 계속 진행하려면 [ETH 출금](https://docs.optimism.io/ko/app-developers/tutorials/bridging/cross-dom-bridge-eth.md#withdraw-eth)을 참조하십시오.

```js theme={null}
const l2Balance = await publicClientL2.readContract({
  address: l2Token,
  abi: erc20ABI,
  functionName: 'balanceOf',
  args: [account.address]
});
console.log(`L2 Balance after withdrawal initiation: ${formatEther(l2Balance)}`);
```

## 다음 단계

축하합니다!
방금 `@eth-optimism/viem` 패키지를 사용하여 토큰을 입금하고 출금했습니다.
이제 `@eth-optimism/viem` 패키지를 사용하여 L1과 L2 간에 ERC-20 토큰을 전송하는 애플리케이션을 작성할 수 있을 것입니다.
이 튜토리얼에서는 Sepolia와 OP Sepolia를 사용했지만, Ethereum과 OP Mainnet에서도 동일한 과정이 적용됩니다.
