zksync-sdk / zksync2-js

MIT License
25 stars 29 forks source link

BUG: ERC-20 deposit / bridge fails to execute #2

Open dutterbutter opened 10 months ago

dutterbutter commented 10 months ago

Description:

When using the zksync-web3 SDK to deposit or bridge funds using ERC-20 tokens, the transaction hangs and fails to execute.

Steps to Reproduce:

1. Scaffold a new project

mkdir deposit-erc20-script && cd deposit-erc20-script
npm init -y 
npm i typescript ts-node ethers@^5.7.2 zksync-web3 dotenv

2. Include the .env file at the project root

Create a .env file in the project root containing your private key and the L1 RPC endpoint.

WALLET_PRIV_KEY=<YOUR_PRIVATE_KEY>
L1_RPC_ENDPOINT=<RPC_URL>

3. Create the deposit ERC-20 tokens script

Click to expand for code snippet! ``` import { Wallet, Provider, utils } from "zksync-web3"; import * as ethers from "ethers"; // load env file import dotenv from "dotenv"; dotenv.config(); // HTTP RPC endpoints const L1_RPC_ENDPOINT = process.env.L1_RPC_ENDPOINT || ""; // or an RPC endpoint from Infura/Chainstack/QuickNode/etc. const L2_RPC_ENDPOINT = process.env.L2_RPC_ENDPOINT || "https://testnet.era.zksync.dev"; // or the zkSync Era mainnet // ERC-20 Token address in L1 const TOKEN_ADDRESS = ""; // Amount of tokens const AMOUNT = "5"; const WALLET_PRIV_KEY = process.env.WALLET_PRIV_KEY || ""; if (!WALLET_PRIV_KEY ) { throw new Error("Wallet private key is not configured in env file"); } if (!L1_RPC_ENDPOINT) { throw new Error( "Missing L1 RPC endpoint. Check chainlist.org or an RPC node provider" ); } if (!TOKEN_ADDRESS ) { throw new Error("Missing address of the ERC-20 token in L1"); } async function main() { console.log(`Running script to bridge ERC-20 to L2`); // Initialize the wallet. const l1provider = new Provider(L1_RPC_ENDPOINT); const l2provider = new Provider(L2_RPC_ENDPOINT); const wallet = new Wallet(WALLET_PRIV_KEY, l2provider, l1provider); console.log(`L1 Balance is ${await wallet.getBalanceL1()}`); console.log(`L2 Balance is ${await wallet.getBalance()}`); // Deposit token to L2 const depositHandle = await wallet.deposit({ to: wallet.address, // can bridge to a different address in L2 token: TOKEN_ADDRESS, amount: ethers.utils.parseEther(AMOUNT), // assumes ERC-20 has 18 decimals // performs the ERC-20 approve action approveERC20: true, }); console.log(`Deposit transaction sent ${depositHandle.hash}`); console.log(`Please wait a few minutes for the deposit to be processed in L2`); } main().then().catch((error) => { console.error(error); process.exitCode = 1; }); ```

4. Run the script

npx ts-node deposit-erc20.ts

5. Expected Output

Running script to bridge ERC-20 to L2
L1 Balance is 19500035772482145
L2 Balance is 2969807401250000000
Deposit transaction sent 0xffb8e302430b0584e2e0104dd6295a03688c98ba7b6e9279b01dba65188cc444

Actual Output:

Running script to bridge ERC-20 to L2
L1 Balance is 8513092747259571425
L2 Balance is 983733765214875035

Identified Issue: l2TokenAddress call fails during deposit operation

The call to retrieve l2TokenAddress seems to be failing, and this call is made in a few places when we call wallet.deposit.

Example:

const l2WethToken = await bridgeContracts.weth.l2TokenAddress(transaction.token);

This call consistently fails with a CALL_EXCEPTION error.

Error: call revert exception [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="l2TokenAddress(address)", data="0x", errorArgs=null, errorName=null, errorSignature=null, reason=null, code=CALL_EXCEPTION, version=abi/5.7.0)
    at Logger.makeError (/Users/dustinbrickwood/Documents/dev/zk/zksync-era/node_modules/@ethersproject/logger/src.ts/index.ts:269:28)
    at Logger.throwError (/Users/dustinbrickwood/Documents/dev/zk/zksync-era/node_modules/@ethersproject/logger/src.ts/index.ts:281:20)
    at Interface.decodeFunctionResult (/Users/dustinbrickwood/Documents/dev/zk/zksync-era/node_modules/@ethersproject/abi/src.ts/interface.ts:427:23)
    at Contract.<anonymous> (/Users/dustinbrickwood/Documents/dev/zk/zksync-era/node_modules/@ethersproject/contracts/src.ts/index.ts:400:44)
    at step (/Users/dustinbrickwood/Documents/dev/zk/zksync-era/node_modules/@ethersproject/contracts/lib/index.js:48:23)
    at Object.next (/Users/dustinbrickwood/Documents/dev/zk/zksync-era/node_modules/@ethersproject/contracts/lib/index.js:29:53)
    at fulfilled (/Users/dustinbrickwood/Documents/dev/zk/zksync-era/node_modules/@ethersproject/contracts/lib/index.js:20:58)
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  reason: null,
  code: 'CALL_EXCEPTION',
  method: 'l2TokenAddress(address)',
  data: '0x',
  errorArgs: null,
  errorName: null,
  errorSignature: null,
  address: '0x0000000000000000000000000000000000000000',
  args: [ '0x07865c6E87B9F70255377e024ace6630C1Eaa37F' ],
  transaction: {
    data: '0xf5f1516800000000000000000000000007865c6e87b9f70255377e024ace6630c1eaa37f',
    to: '0x0000000000000000000000000000000000000000',
    from: '0x42baB21bB7c1E236D67B264685E28fbbeF49C19F'
  }
}

Context

When executing the deposit call from the script:

const depositHandle = await wallet.deposit({
  to: wallet.address, 
  token: TOKEN_ADDRESS,
  amount: ethers.utils.parseEther(AMOUNT),
  approveERC20: true,
});

The function internally references l2TokenAddress in a few places:

By commenting out the lines that invoke l2TokenAddress in the above-referenced locations, the deposit transaction executes as expected and successfully completes. This workaround suggests that the issue may be specifically related to how l2TokenAddress is being called or handled.

Deliverable

To close out this issue:

danijelTxFusion commented 10 months ago

The bug is related to l2TokenAddress method because it throws error for every token expcept for ETH. More precise, l2TokenAddress uses l2WethBridge.l2TokenAddress(token) to get wETHL2 based on provided wETHL1, which returns 0x for every token. This means that current implementation of l2TokenAddress on mainnet and testnet works only for ETH and breaks for any other token.

wETH on Mainnet

{
  "address": "0xf00dad97284d0c6f06dc4db3c32454d4292c6813",
  "l1Address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
  "l2Address": "0xf00dad97284d0c6f06dc4db3c32454d4292c6813",
  "name": "Wrapped Ether",
  "symbol": "WETH",
  "decimals": 18
}
const wETHL1 = await provider.l2TokenAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // breaks
const wETHL2 = await provider.l1TokenAddress("0xf00dad97284d0c6f06dc4db3c32454d4292c6813"); // breaks

The same bug applies to testnet and local-setup.

danijelTxFusion commented 10 months ago

Here's the PR that fixed the issue. Also the wETH bridges does not work because they always return '0x' when it comes to l1TokenAddress and l2TokenAddress methods.

mpavlovic-txfusion commented 9 months ago

It looks to me that the root cause of this issue was the fact that the zks_getBridgeContracts RPC method doesn't return the correct l1WethBridge and l2WethBridge addresses and instead it return the zero address. This RPC method is used by the sdk and the call to l2TokenAddress is sent to the incorrect weth bridge address, causing it to fail.

This is happening on both Testnet and Mainnet: Mainnet {"jsonrpc":"2.0","result":{"l1Erc20DefaultBridge":"0x57891966931eb4bb6fb81430e6ce0a03aabde063","l2Erc20DefaultBridge":"0x11f943b2c77b743ab90f4a0ae7d5a4e7fca3e102","l1WethBridge":"0x0000000000000000000000000000000000000000","l2WethBridge":"0x0000000000000000000000000000000000000000"},"id":1} Testnet {"jsonrpc":"2.0","result":{"l1Erc20DefaultBridge":"0x927ddfcc55164a59e0f33918d13a2d559bc10ce7","l2Erc20DefaultBridge":"0x00ff932a6d70e2b8f1eb4919e1e09c1923e7e57b","l1WethBridge":"0x0000000000000000000000000000000000000000","l2WethBridge":"0x0000000000000000000000000000000000000000"},"id":1}