diff --git a/.changeset/real-chicken-sleep.md b/.changeset/real-chicken-sleep.md new file mode 100644 index 0000000000..1fa363cf29 --- /dev/null +++ b/.changeset/real-chicken-sleep.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Added `deposit` action, apply and undo alias util functions in ZKsync extension diff --git a/site/pages/zksync/actions/deposit.md b/site/pages/zksync/actions/deposit.md new file mode 100644 index 0000000000..76f9607c53 --- /dev/null +++ b/site/pages/zksync/actions/deposit.md @@ -0,0 +1,426 @@ +--- +description: Transfers the specified token from the associated account on the L1 network to the target account on the L2 network. +--- + +# deposit + +Transfers the specified token from the associated account on the L1 network to the target account on the L2 network. +The token can be either ETH or any ERC20 token. For ERC20 tokens, enough approved tokens must be associated with +the specified L1 bridge (default one or the one defined in `bridgeAddress`). +In this case, depending on is the chain ETH-based or not `approveToken` or `approveBaseToken` +can be enabled to perform token approval. If there are already enough approved tokens for the L1 bridge, +token approval will be skipped. + +## Usage + +:::code-group + +```ts [example.ts] +import { account, walletClient, zksyncClient } from './config' +import { legacyEthAddress } from 'viem/zksync' + +// deposit ETH +const hash = await walletClient.deposit({ + account, + client: zksyncClient, + token: legacyEthAddress, + amount: 7_000_000_000n, + to: account.address, + refundRecipient: account.address, +}) + +// deposit ERC20 +const txHash = await walletClient.deposit({ + account, + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: account.address, + approveToken: true, + refundRecipient: account.address, +}) +``` + +```ts [config.ts] +import { createWalletClient, createPublicClient, custom } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { zksync, mainnet } from 'viem/chains' +import { publicActionsL2, walletActionsL1 } from 'viem/zksync' + +export const zksyncClient = createPublicClient({ + chain: zksync, + transport: custom(window.ethereum) +}).extend(publicActionsL2()) + +export const walletClient = createWalletClient({ + chain: mainnet, + transport: custom(window.ethereum) +}).extend(walletActionsL1()) + +// JSON-RPC Account +export const [account] = await walletClient.getAddresses() +// Local Account +export const account = privateKeyToAccount(...) +``` + +::: + +### Account Hoisting + +If you do not wish to pass an `account` to every `deposit`, you can also hoist the Account on the Wallet Client (see `config.ts`). + +[Learn more](/docs/clients/wallet#account). + +:::code-group + +```ts [example.ts] +import { walletClient, zksyncClient } from './config' +import { legacyEthAddress } from 'viem/zksync' + +// deposit ETH +const hash = await walletClient.deposit({ + client: zksyncClient, + token: legacyEthAddress, + amount: 7_000_000_000n, + to: walletClient.account.address, + refundRecipient: walletClient.account.address, +}) + +// deposit ERC20 +const txHash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, +}) +``` + +```ts [config.ts (JSON-RPC Account)] +import { createWalletClient, custom } from 'viem' +import { zksync } from 'viem/chains' +import { publicActionsL2, walletActionsL1 } from 'viem/zksync' + +export const zksyncClient = createPublicClient({ + chain: zksync, + transport: custom(window.ethereum) +}).extend(publicActionsL2()) + +// Retrieve Account from an EIP-1193 Provider. // [!code focus] +const [account] = await window.ethereum.request({ // [!code focus] + method: 'eth_requestAccounts' // [!code focus] +}) // [!code focus] + +export const walletClient = createWalletClient({ + account, + transport: custom(window.ethereum) // [!code focus] +}).extend(walletActionsL1()) +``` + +```ts [config.ts (Local Account)] +import { createWalletClient, custom } from 'viem' +import { zksync } from 'viem/chains' +import { privateKeyToAccount } from 'viem/accounts' +import { publicActionsL2, walletActionsL1 } from 'viem/zksync' + +export const zksyncClient = createPublicClient({ + chain: zksync, + transport: custom(window.ethereum) +}).extend(publicActionsL2()) + +export const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), // [!code focus] + transport: custom(window.ethereum) +}).extend(walletActionsL1()) +``` + +::: + +## Returns + +[`Hash`](/docs/glossary/types#hash) + +The [Transaction](/docs/glossary/terms#transaction) hash. + +## Parameters + +### client + +- **Type:** `Client` + +The L2 client for fetching data from L2 chain. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, // [!code focus] + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, +}) +``` + +### token + +- **Type:** `Address` + +The address of the token to deposit. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', // [!code focus] + amount: 20n, + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, +}) +``` + +### amount + +- **Type:** `bigint` + +The amount of the token to deposit. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, // [!code focus] + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, +}) +``` + +### to (optional) + +- **Type:** `Address` +- **Default:** `walletClient.account` + +The address that will receive the deposited tokens on L2. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, // [!code focus] + approveToken: true, + refundRecipient: walletClient.account.address, +}) +``` + +### operatorTip (optional) + +- **Type:** `bigint` + +The tip the operator will receive on top of the base cost of the transaction. +Currently, ZKsync node do not consider this tip. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + operatorTip: 100_000n, // [!code focus] + approveToken: true, + refundRecipient: walletClient.account.address, +}) +``` + +### l2GasLimit (optional) + +- **Type:** `bigint` + +Maximum amount of L2 gas that transaction can consume during execution on L2. + +```ts +const hash = await walletClient.requestExecute({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + l2GasLimit: 900_000n, // [!code focus] + approveToken: true, + refundRecipient: walletClient.account.address, +}) +``` + +### gasPerPubdataByte (optional) + +- **Type:** `bigint` + +The L2 gas price for each published L1 calldata byte. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, + gasPerPubdataByte: 250_000_000_000n // [!code focus] +}) +``` + +### refundRecipient (optional) + +- **Type:** `Address` +- **Default:** `walletClient.account` + +The address on L2 that will receive the refund for the transaction. +If the transaction fails, it will also be the address to receive `amount`. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, // [!code focus] +}) +``` + +### bridgeAddress (optional) + +- **Type:** `Address` +- **Default:** ZKsync L1 shared bridge + +The address of the bridge contract to be used. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, + bridgeAddress: '0xFC073319977e314F251EAE6ae6bE76B0B3BAeeCF' // [!code focus] +}) +``` + +### customBridgeData (optional) + +- **Type:** `Hex` + +Additional data that can be sent to a bridge. + +```ts +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, + bridgeAddress: '0xFC073319977e314F251EAE6ae6bE76B0B3BAeeCF', + customBridgeData: '0x...' // [!code focus], +}) +``` + +### approveToken (optional) + +- **Type:** `boolean | TransactionRequest` + +Whether token approval should be performed under the hood. +Set this flag to true (or provide transaction overrides) if the bridge does +not have sufficient allowance. The approval transaction is executed only if +the bridge lacks sufficient allowance; otherwise, it is skipped. + +::: code-group + +```ts [boolean.ts] +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: true, // [!code focus], + refundRecipient: walletClient.account.address, + bridgeAddress: '0xFC073319977e314F251EAE6ae6bE76B0B3BAeeCF', +}) +``` + +```ts [overrides.ts] +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: { + maxFeePerGas: 200_000_000_000n // [!code focus], + }, + refundRecipient: walletClient.account.address, + bridgeAddress: '0xFC073319977e314F251EAE6ae6bE76B0B3BAeeCF', +}) +``` + +::: + +### approveBaseToken (optional) + +- **Type:** `boolean | TransactionRequest` + +Whether base token approval should be performed under the hood. +Set this flag to true (or provide transaction overrides) if the bridge does +not have sufficient allowance. The approval transaction is executed only if +the bridge lacks sufficient allowance; otherwise, it is skipped. + +::: code-group + +```ts [boolean.ts] +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveBaseToken: true, // [!code focus], + refundRecipient: walletClient.account.address, + bridgeAddress: '0xFC073319977e314F251EAE6ae6bE76B0B3BAeeCF', +}) +``` + +```ts [overrides.ts] +const hash = await walletClient.deposit({ + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveBaseToken: { + maxFeePerGas: 200_000_000_000n // [!code focus], + }, + refundRecipient: walletClient.account.address, + bridgeAddress: '0xFC073319977e314F251EAE6ae6bE76B0B3BAeeCF', +}) +``` + +::: + +### chain (optional) + +- **Type:** [`Chain`](/docs/glossary/types#chain) +- **Default:** `walletClient.chain` + +The target chain. If there is a mismatch between the wallet's current chain & the target chain, an error will be thrown. + +```ts +import { zksync } from 'viem/chains' // [!code focus] + +const hash = await walletClient.deposit({ + chain: zksync, // [!code focus] + client: zksyncClient, + token: '0x70a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55', + amount: 20n, + to: walletClient.account.address, + approveToken: true, + refundRecipient: walletClient.account.address, +}) +``` \ No newline at end of file diff --git a/site/sidebar.ts b/site/sidebar.ts index 5fe1616ba7..1cfeea5653 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -1826,6 +1826,10 @@ export const sidebar = { text: 'finalizeWithdrawal', link: '/zksync/actions/finalizeWithdrawal', }, + { + text: 'deposit', + link: '/zksync/actions/deposit', + }, ], }, { diff --git a/src/zksync/actions/deposit.test.ts b/src/zksync/actions/deposit.test.ts new file mode 100644 index 0000000000..47f17ee10f --- /dev/null +++ b/src/zksync/actions/deposit.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from 'vitest' +import { anvilMainnet, anvilZksync } from '~test/src/anvil.js' +import { accounts } from '~test/src/constants.js' +import { mockRequestReturnData } from '~test/src/zksync.js' +import { privateKeyToAccount } from '~viem/accounts/privateKeyToAccount.js' +import { publicActions } from '~viem/clients/decorators/public.js' +import type { EIP1193RequestFn } from '~viem/types/eip1193.js' +import { legacyEthAddress } from '~viem/zksync/constants/address.js' +import { publicActionsL2 } from '~viem/zksync/decorators/publicL2.js' +import { deposit } from './deposit.js' + +const request = (async ({ method, params }) => { + if (method === 'eth_sendRawTransaction') + return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + if (method === 'eth_sendTransaction') + return '0x9afe47f3d95eccfc9210851ba5f877f76d372514a26b48bad848a07f77c33b87' + if (method === 'eth_estimateGas') return 158774n + if (method === 'eth_gasPrice') return 150_000_000n + if (method === 'eth_maxPriorityFeePerGas') return 100_000_000n + if (method === 'eth_call') + return '0x00000000000000000000000070a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55' + if (method === 'eth_getTransactionCount') return 1n + if (method === 'eth_getBlockByNumber') return anvilMainnet.forkBlockNumber + if (method === 'eth_chainId') return anvilMainnet.chain.id + return anvilMainnet.getClient().request({ method, params } as any) +}) as EIP1193RequestFn + +const baseClient = anvilMainnet.getClient({ batch: { multicall: false } }) +baseClient.request = request +const client = baseClient.extend(publicActions) + +const baseClientWithAccount = anvilMainnet.getClient({ + batch: { multicall: false }, + account: true, +}) +baseClientWithAccount.request = request +const clientWithAccount = baseClientWithAccount.extend(publicActions) + +const baseClientL2 = anvilZksync.getClient() +baseClientL2.request = (async ({ method, params }) => { + if (method === 'eth_call') + return '0x00000000000000000000000070a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55' + if (method === 'eth_estimateGas') return 158774n + return ( + (await mockRequestReturnData(method)) ?? + (await anvilZksync.getClient().request({ method, params } as any)) + ) +}) as EIP1193RequestFn +const clientL2 = baseClientL2.extend(publicActionsL2()) + +test.skip('default', async () => { + const account = privateKeyToAccount(accounts[0].privateKey) + expect( + deposit(client, { + client: clientL2, + account, + token: legacyEthAddress, + to: account.address, + amount: 7_000_000_000n, + refundRecipient: account.address, + }), + ).toBeDefined() +}) + +test('default: account hoisting', async () => { + const account = privateKeyToAccount(accounts[0].privateKey) + expect( + deposit(clientWithAccount, { + client: clientL2, + token: legacyEthAddress, + to: account.address, + amount: 7_000_000_000n, + refundRecipient: account.address, + }), + ).toBeDefined() +}) + +test('errors: no account provided', async () => { + const account = privateKeyToAccount(accounts[0].privateKey) + await expect(() => + deposit(client, { + client: clientL2, + token: legacyEthAddress, + to: account.address, + amount: 7_000_000_000n, + refundRecipient: account.address, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [AccountNotFoundError: Could not find an Account to execute with this Action. + Please provide an Account with the \`account\` argument on the Action, or by supplying an \`account\` to the Client. + + Docs: https://viem.sh/docs/actions/wallet/sendTransaction + Version: viem@x.y.z] + `) +}) diff --git a/src/zksync/actions/deposit.ts b/src/zksync/actions/deposit.ts new file mode 100644 index 0000000000..159eefcb4a --- /dev/null +++ b/src/zksync/actions/deposit.ts @@ -0,0 +1,856 @@ +import { type Address, parseAbi, parseAbiParameters } from 'abitype' +import { ZeroAddress } from 'ethers' +import type { Hex } from 'viem' +import type { Account } from '../../accounts/types.js' +import { + type EstimateGasParameters, + estimateGas, +} from '../../actions/public/estimateGas.js' +import { readContract } from '../../actions/public/readContract.js' +import { waitForTransactionReceipt } from '../../actions/public/waitForTransactionReceipt.js' +import { + type SendTransactionErrorType, + type SendTransactionParameters, + type SendTransactionReturnType, + sendTransaction, +} from '../../actions/wallet/sendTransaction.js' +import { + type WriteContractParameters, + writeContract, +} from '../../actions/wallet/writeContract.js' +import type { Client } from '../../clients/createClient.js' +import { publicActions } from '../../clients/decorators/public.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import { erc20Abi } from '../../constants/abis.js' +import { AccountNotFoundError } from '../../errors/account.js' +import { ClientChainNotConfiguredError } from '../../errors/chain.js' +import type { GetAccountParameter } from '../../types/account.js' +import type { + Chain, + DeriveChain, + GetChainParameter, +} from '../../types/chain.js' +import type { UnionEvaluate, UnionOmit } from '../../types/utils.js' +import { + type FormattedTransactionRequest, + encodeAbiParameters, + encodeFunctionData, + isAddressEqual, + parseAccount, +} from '../../utils/index.js' +import { bridgehubAbi } from '../constants/abis.js' +import { + ethAddressInContracts, + legacyEthAddress, +} from '../constants/address.js' +import { requiredL1ToL2GasPerPubdataLimit } from '../constants/number.js' +import { + BaseFeeHigherThanValueError, + type BaseFeeHigherThanValueErrorType, +} from '../errors/bridge.js' +import type { ChainEIP712 } from '../types/chain.js' +import type { BridgeContractAddresses } from '../types/contract.js' +import { applyL1ToL2Alias } from '../utils/bridge/applyL1ToL2Alias.js' +import { estimateGasL1ToL2 } from './estimateGasL1ToL2.js' +import { getBridgehubContractAddress } from './getBridgehubContractAddress.js' +import { getDefaultBridgeAddresses } from './getDefaultBridgeAddresses.js' +import { getL1Allowance } from './getL1Allowance.js' + +export type DepositParameters< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined, + accountL2 extends Account | undefined = Account | undefined, + _derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>, +> = UnionEvaluate< + UnionOmit<FormattedTransactionRequest<_derivedChain>, 'data' | 'to' | 'from'> +> & + Partial<GetChainParameter<chain, chainOverride>> & + Partial<GetAccountParameter<account>> & { + /** L2 client. */ + client: Client<Transport, chainL2, accountL2> + /** The address of the token to deposit. */ + token: Address + /** The amount of the token to deposit. */ + amount: bigint + /** The address that will receive the deposited tokens on L2. + Defaults to the sender address.*/ + to?: Address + /** (currently not used) The tip the operator will receive on top of + the base cost of the transaction. */ + operatorTip?: bigint + /** Maximum amount of L2 gas that transaction can consume during execution on L2. */ + l2GasLimit?: bigint + /** The L2 gas price for each published L1 calldata byte. */ + gasPerPubdataByte?: bigint + /** The address on L2 that will receive the refund for the transaction. + If the transaction fails, it will also be the address to receive `amount`. */ + refundRecipient?: Address + /** The address of the bridge contract to be used. + Defaults to the default ZKsync L1 shared bridge. */ + bridgeAddress?: Address + /** Additional data that can be sent to a bridge. */ + customBridgeData?: Hex + /** Whether token approval should be performed under the hood. + Set this flag to true (or provide transaction overrides) if the bridge does + not have sufficient allowance. The approval transaction is executed only if + the bridge lacks sufficient allowance; otherwise, it is skipped. */ + approveToken?: + | boolean + | UnionEvaluate< + UnionOmit< + FormattedTransactionRequest<_derivedChain>, + 'data' | 'to' | 'from' + > + > + /** Whether base token approval should be performed under the hood. + Set this flag to true (or provide transaction overrides) if the bridge does + not have sufficient allowance. The approval transaction is executed only if + the bridge lacks sufficient allowance; otherwise, it is skipped. */ + approveBaseToken?: + | boolean + | UnionEvaluate< + UnionOmit< + FormattedTransactionRequest<_derivedChain>, + 'data' | 'to' | 'from' + > + > + } + +export type DepositReturnType = SendTransactionReturnType + +export type DepositErrorType = + | SendTransactionErrorType + | BaseFeeHigherThanValueErrorType + +/** + * Transfers the specified token from the associated account on the L1 network to the target account on the L2 network. + * The token can be either ETH or any ERC20 token. For ERC20 tokens, enough approved tokens must be associated with + * the specified L1 bridge (default one or the one defined in `bridgeAddress`). + * In this case, depending on is the chain ETH-based or not `approveToken` or `approveBaseToken` + * can be enabled to perform token approval. If there are already enough approved tokens for the L1 bridge, + * token approval will be skipped. + * + * @param client - Client to use + * @param parameters - {@link DepositParameters} + * @returns hash - The [Transaction](https://viem.sh/docs/glossary/terms#transaction) hash. {@link DepositReturnType} + * + * @example + * import { createPublicClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zksync, mainnet } from 'viem/chains' + * import { deposit, legacyEthAddress, publicActionsL2 } from 'viem/zksync' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: http(), + * }) + * + * const clientL2 = createPublicClient({ + * chain: zksync, + * transport: http(), + * }).extend(publicActionsL2()) + * + * const account = privateKeyToAccount('0x…') + * + * const hash = await deposit(client, { + * client: clientL2, + * account, + * token: legacyEthAddress, + * to: account.address, + * amount: 1_000_000_000_000_000_000n, + * refundRecipient: account.address, + * }) + * + * @example Account Hoisting + * import { createPublicClient, createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zksync, mainnet } from 'viem/chains' + * import { legacyEthAddress, publicActionsL2 } from 'viem/zksync' + * + * const walletClient = createWalletClient({ + * chain: mainnet, + * transport: http(), + * account: privateKeyToAccount('0x…'), + * }) + * + * const clientL2 = createPublicClient({ + * chain: zksync, + * transport: http(), + * }).extend(publicActionsL2()) + * + * const hash = await deposit(walletClient, { + * client: clientL2, + * account, + * token: legacyEthAddress, + * to: walletClient.account.address, + * amount: 1_000_000_000_000_000_000n, + * refundRecipient: walletClient.account.address, + * }) + */ +export async function deposit< + chain extends Chain | undefined, + account extends Account | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined, + accountL2 extends Account | undefined = Account | undefined, + _derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>, +>( + client: Client<Transport, chain, account>, + parameters: DepositParameters< + chain, + account, + chainOverride, + chainL2, + accountL2 + >, +): Promise<DepositReturnType> { + let { + account: account_ = client.account, + chain: chain_ = client.chain, + client: l2Client, + token, + amount, + approveToken, + approveBaseToken, + gas, + } = parameters + + const account = account_ ? parseAccount(account_) : client.account + if (!account) + throw new AccountNotFoundError({ + docsPath: '/docs/actions/wallet/sendTransaction', + }) + if (!l2Client.chain) throw new ClientChainNotConfiguredError() + + if (isAddressEqual(token, legacyEthAddress)) token = ethAddressInContracts + + const bridgeAddresses = await getDefaultBridgeAddresses(l2Client) + const bridgehub = await getBridgehubContractAddress(l2Client) + const baseToken = await readContract(client, { + address: bridgehub, + abi: bridgehubAbi, + functionName: 'baseToken', + args: [BigInt(l2Client.chain.id)], + }) + + const { mintValue, tx } = await getL1DepositTx( + client, + account, + { ...parameters, token }, + bridgeAddresses, + bridgehub, + baseToken, + ) + + await approveTokens( + client, + chain_, + tx.bridgeAddress, + baseToken, + mintValue, + account, + token, + amount, + approveToken, + approveBaseToken, + ) + + if (!gas) { + const baseGasLimit = await estimateGas(client, { + account: account.address, + to: bridgehub, + value: tx.value, + data: tx.data, + } as EstimateGasParameters) + gas = scaleGasLimit(baseGasLimit) + } + + return await sendTransaction(client, { + chain: chain_, + account, + gas, + ...tx, + } as SendTransactionParameters) +} + +async function getL1DepositTx< + chain extends Chain | undefined, + account extends Account | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined, + accountL2 extends Account | undefined = Account | undefined, + _derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>, +>( + client: Client<Transport, chain, account>, + account: Account, + parameters: DepositParameters< + chain, + account, + chainOverride, + chainL2, + accountL2, + _derivedChain + >, + bridgeAddresses: BridgeContractAddresses, + bridgehub: Address, + baseToken: Address, +) { + let { + account: _account, + chain: _chain, + client: l2Client, + token, + amount, + to, + operatorTip = 0n, + l2GasLimit, + gasPerPubdataByte = requiredL1ToL2GasPerPubdataLimit, + refundRecipient = ZeroAddress as Address, + bridgeAddress, + customBridgeData, + value, + gasPrice, + maxFeePerGas, + maxPriorityFeePerGas, + approveToken: _approveToken, + approveBaseToken: _approveBaseToken, + ...rest + } = parameters + + if (!l2Client.chain) throw new ClientChainNotConfiguredError() + + to ??= account.address + let gasPriceForEstimation = maxFeePerGas || gasPrice + if (!gasPriceForEstimation) { + const estimatedFee = await getFeePrice(client) + gasPriceForEstimation = estimatedFee.maxFeePerGas + maxFeePerGas = estimatedFee.maxFeePerGas + maxPriorityFeePerGas ??= estimatedFee.maxPriorityFeePerGas + } + + const { l2GasLimit_, baseCost } = await getL2BridgeTxFeeParams( + client, + l2Client, + bridgehub, + gasPriceForEstimation, + account.address, + token, + amount, + to, + gasPerPubdataByte, + baseToken, + l2GasLimit, + bridgeAddress, + customBridgeData, + ) + l2GasLimit = l2GasLimit_ + let mintValue: bigint + let data: Hex + + const isETHBasedChain = isAddressEqual(baseToken, ethAddressInContracts) + if ( + (isETHBasedChain && isAddressEqual(token, ethAddressInContracts)) || // ETH on ETH-based chain + isAddressEqual(token, baseToken) // base token on custom chain + ) { + // Deposit base token + mintValue = baseCost + operatorTip + amount + let providedValue = isETHBasedChain ? value : mintValue + if (!providedValue || providedValue === 0n) providedValue = mintValue + if (baseCost > providedValue) + throw new BaseFeeHigherThanValueError(baseCost, providedValue) + + value = isETHBasedChain ? providedValue : 0n + bridgeAddress = bridgeAddresses.sharedL1 // required for approval of base token on custom chain + + data = encodeFunctionData({ + abi: bridgehubAbi, + functionName: 'requestL2TransactionDirect', + args: [ + { + chainId: BigInt(l2Client.chain.id), + mintValue: providedValue, + l2Contract: to, + l2Value: amount, + l2Calldata: '0x', + l2GasLimit, + l2GasPerPubdataByteLimit: gasPerPubdataByte, + factoryDeps: [], + refundRecipient, + }, + ], + }) + } else if (isAddressEqual(baseToken, ethAddressInContracts)) { + // Deposit token on ETH-based chain + mintValue = baseCost + BigInt(operatorTip) + value = mintValue + if (baseCost > mintValue) + throw new BaseFeeHigherThanValueError(baseCost, mintValue) + + bridgeAddress ??= bridgeAddresses.sharedL1 + + data = encodeFunctionData({ + abi: bridgehubAbi, + functionName: 'requestL2TransactionTwoBridges', + args: [ + { + chainId: BigInt(l2Client.chain.id), + mintValue, + l2Value: 0n, + l2GasLimit, + l2GasPerPubdataByteLimit: gasPerPubdataByte, + refundRecipient, + secondBridgeAddress: bridgeAddress, + secondBridgeValue: 0n, + secondBridgeCalldata: encodeAbiParameters( + parseAbiParameters('address x, uint256 y, address z'), + [token, amount, to], + ), + }, + ], + }) + } else if (isAddressEqual(token, ethAddressInContracts)) { + // Deposit ETH on custom chain + mintValue = baseCost + operatorTip + value = amount + if (baseCost > mintValue) + throw new BaseFeeHigherThanValueError(baseCost, mintValue) + + bridgeAddress = bridgeAddresses.sharedL1 + + data = encodeFunctionData({ + abi: bridgehubAbi, + functionName: 'requestL2TransactionTwoBridges', + args: [ + { + chainId: BigInt(l2Client.chain.id), + mintValue, + l2Value: 0n, + l2GasLimit, + l2GasPerPubdataByteLimit: gasPerPubdataByte, + refundRecipient, + secondBridgeAddress: bridgeAddress, + secondBridgeValue: amount, + secondBridgeCalldata: encodeAbiParameters( + parseAbiParameters('address x, uint256 y, address z'), + [ethAddressInContracts, 0n, to], + ), + }, + ], + }) + } else { + // Deposit token on custom chain + mintValue = baseCost + operatorTip + value ??= 0n + if (baseCost > mintValue) + throw new BaseFeeHigherThanValueError(baseCost, mintValue) + + bridgeAddress ??= bridgeAddresses.sharedL1 + + data = encodeFunctionData({ + abi: bridgehubAbi, + functionName: 'requestL2TransactionTwoBridges', + args: [ + { + chainId: BigInt(l2Client.chain.id), + mintValue, + l2Value: 0n, + l2GasLimit, + l2GasPerPubdataByteLimit: gasPerPubdataByte, + refundRecipient, + secondBridgeAddress: bridgeAddress, + secondBridgeValue: 0n, + secondBridgeCalldata: encodeAbiParameters( + parseAbiParameters('address x, uint256 y, address z'), + [token, amount, to], + ), + }, + ], + }) + } + + return { + mintValue, + tx: { + bridgeAddress, + to: bridgehub, + data, + value, + gasPrice, + maxFeePerGas, + maxPriorityFeePerGas, + ...rest, + }, + } +} + +async function approveTokens< + chain extends Chain | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + _derivedChain extends Chain | undefined = DeriveChain<chain, chainOverride>, +>( + client: Client<Transport, chain>, + chain: Chain | null | undefined, + bridgeAddress: Address, + baseToken: Address, + mintValue: bigint, + account: Account, + token: Address, + amount: bigint, + approveToken?: + | boolean + | UnionEvaluate< + UnionOmit< + FormattedTransactionRequest<_derivedChain>, + 'data' | 'to' | 'from' + > + >, + approveBaseToken?: + | boolean + | UnionEvaluate< + UnionOmit< + FormattedTransactionRequest<_derivedChain>, + 'data' | 'to' | 'from' + > + >, +) { + if (isAddressEqual(baseToken, ethAddressInContracts)) { + // Deposit token on ETH-based chain + if (approveToken) { + const overrides = typeof approveToken === 'boolean' ? {} : approveToken + const allowance = await getL1Allowance(client, { + token, + bridgeAddress, + account, + }) + if (allowance < amount) { + const hash = await writeContract(client, { + chain, + account, + address: token, + abi: erc20Abi, + functionName: 'approve', + args: [bridgeAddress, amount], + ...overrides, + } satisfies WriteContractParameters as any) + await waitForTransactionReceipt(client, { hash }) + } + } + return + } + + if (isAddressEqual(token, ethAddressInContracts)) { + // Deposit ETH on custom chain + if (approveBaseToken) { + const overrides = typeof approveToken === 'boolean' ? {} : approveToken + const allowance = await getL1Allowance(client, { + token: baseToken, + bridgeAddress, + account, + }) + if (allowance < mintValue) { + const hash = await writeContract(client, { + chain, + account, + address: baseToken, + abi: erc20Abi, + functionName: 'approve', + args: [bridgeAddress, mintValue], + ...overrides, + } satisfies WriteContractParameters as any) + await waitForTransactionReceipt(client, { hash }) + } + return + } + } + + if (isAddressEqual(token, baseToken)) { + // Deposit base token on custom chain + if (approveToken || approveBaseToken) { + const overrides = + typeof approveToken === 'boolean' + ? {} + : (approveToken ?? typeof approveBaseToken === 'boolean') + ? {} + : approveBaseToken + const allowance = await getL1Allowance(client, { + token: baseToken, + bridgeAddress, + account, + }) + if (allowance < mintValue) { + const hash = await writeContract(client, { + chain, + account, + address: baseToken, + abi: erc20Abi, + functionName: 'approve', + args: [bridgeAddress, mintValue], + ...overrides, + } satisfies WriteContractParameters as any) + await waitForTransactionReceipt(client, { hash }) + } + } + return + } + + // Deposit token on custom chain + if (approveBaseToken) { + const overrides = typeof approveToken === 'boolean' ? {} : approveToken + const allowance = await getL1Allowance(client, { + token: baseToken, + bridgeAddress, + account, + }) + if (allowance < mintValue) { + const hash = await writeContract(client, { + chain, + account, + address: baseToken, + abi: erc20Abi, + functionName: 'approve', + args: [bridgeAddress, mintValue], + ...overrides, + } satisfies WriteContractParameters as any) + await waitForTransactionReceipt(client, { hash }) + } + } + + if (approveToken) { + const overrides = typeof approveToken === 'boolean' ? {} : approveToken + const allowance = await getL1Allowance(client, { + token, + bridgeAddress, + account, + }) + if (allowance < amount) { + const hash = await writeContract(client, { + chain, + account, + address: token, + abi: erc20Abi, + functionName: 'approve', + args: [bridgeAddress, amount], + ...overrides, + } satisfies WriteContractParameters as any) + await waitForTransactionReceipt(client, { hash }) + } + } +} + +async function getL2BridgeTxFeeParams< + chain extends Chain | undefined, + chainL2 extends ChainEIP712 | undefined, +>( + client: Client<Transport, chain>, + l2Client: Client<Transport, chainL2>, + bridgehub: Address, + gasPrice: bigint, + from: Address, + token: Address, + amount: bigint, + to: Address, + gasPerPubdataByte: bigint, + baseToken: Address, + l2GasLimit?: bigint, + bridgeAddress?: Address, + customBridgeData?: Hex, +) { + if (!l2Client.chain) throw new ClientChainNotConfiguredError() + + let l2GasLimit_ = l2GasLimit + if (!l2GasLimit_) + l2GasLimit_ = bridgeAddress + ? await getL2GasLimitFromCustomBridge( + client, + l2Client, + from, + token, + amount, + to, + gasPerPubdataByte, + bridgeAddress, + customBridgeData, + ) + : await getL2GasLimitFromDefaultBridge( + client, + l2Client, + from, + token, + amount, + to, + gasPerPubdataByte, + baseToken, + ) + + const baseCost = await readContract(client, { + address: bridgehub, + abi: bridgehubAbi, + functionName: 'l2TransactionBaseCost', + args: [BigInt(l2Client.chain.id), gasPrice, l2GasLimit_, gasPerPubdataByte], + }) + return { l2GasLimit_, baseCost } +} + +async function getL2GasLimitFromDefaultBridge< + chain extends Chain | undefined, + chainL2 extends ChainEIP712 | undefined, +>( + client: Client<Transport, chain>, + l2Client: Client<Transport, chainL2>, + from: Address, + token: Address, + amount: bigint, + to: Address, + gasPerPubdataByte: bigint, + baseToken: Address, +) { + if (isAddressEqual(token, baseToken)) { + return await estimateGasL1ToL2(l2Client, { + chain: l2Client.chain, + account: from, + from, + to, + value: amount, + data: '0x', + gasPerPubdata: gasPerPubdataByte, + }) + } + const value = 0n + const bridgeAddresses = await getDefaultBridgeAddresses(l2Client) + const l1BridgeAddress = bridgeAddresses.sharedL1 + const l2BridgeAddress = bridgeAddresses.sharedL2 + const bridgeData = await encodeDefaultBridgeData(client, token) + + const calldata = encodeFunctionData({ + abi: parseAbi([ + 'function finalizeDeposit(address _l1Sender, address _l2Receiver, address _l1Token, uint256 _amount, bytes _data)', + ]), + functionName: 'finalizeDeposit', + args: [ + from, + to, + isAddressEqual(token, legacyEthAddress) ? ethAddressInContracts : token, + amount, + bridgeData, + ], + }) + + return await estimateGasL1ToL2(l2Client, { + chain: l2Client.chain, + account: applyL1ToL2Alias(l1BridgeAddress), + to: l2BridgeAddress, + data: calldata, + gasPerPubdata: gasPerPubdataByte, + value, + }) +} + +async function getL2GasLimitFromCustomBridge< + chain extends Chain | undefined, + chainL2 extends ChainEIP712 | undefined, +>( + client: Client<Transport, chain>, + l2Client: Client<Transport, chainL2>, + from: Address, + token: Address, + amount: bigint, + to: Address, + gasPerPubdataByte: bigint, + bridgeAddress: Address, + customBridgeData?: Hex, +) { + let customBridgeData_ = customBridgeData + if (!customBridgeData_ || customBridgeData_ === '0x') + customBridgeData_ = await encodeDefaultBridgeData(client, token) + + const l2BridgeAddress = await readContract(client, { + address: token, + abi: parseAbi([ + 'function l2BridgeAddress(uint256 _chainId) view returns (address)', + ]), + functionName: 'l2BridgeAddress', + args: [BigInt(l2Client.chain!.id)], + }) + + const calldata = encodeFunctionData({ + abi: parseAbi([ + 'function finalizeDeposit(address _l1Sender, address _l2Receiver, address _l1Token, uint256 _amount, bytes _data)', + ]), + functionName: 'finalizeDeposit', + args: [from, to, token, amount, customBridgeData_], + }) + + return await estimateGasL1ToL2(l2Client, { + chain: l2Client.chain, + account: from, + from: applyL1ToL2Alias(bridgeAddress), + to: l2BridgeAddress, + data: calldata, + gasPerPubdata: gasPerPubdataByte, + }) +} + +async function encodeDefaultBridgeData<chain extends Chain | undefined>( + client: Client<Transport, chain>, + token: Address, +) { + let token_ = token + if (isAddressEqual(token, legacyEthAddress)) token_ = ethAddressInContracts + let name = 'Ether' + let symbol = 'ETH' + let decimals = 18n + if (!isAddressEqual(token_, ethAddressInContracts)) { + name = await readContract(client, { + address: token_, + abi: erc20Abi, + functionName: 'name', + args: [], + }) + symbol = await readContract(client, { + address: token_, + abi: erc20Abi, + functionName: 'symbol', + args: [], + }) + decimals = BigInt( + await readContract(client, { + address: token_, + abi: erc20Abi, + functionName: 'decimals', + args: [], + }), + ) + } + + const nameBytes = encodeAbiParameters([{ type: 'string' }], [name]) + const symbolBytes = encodeAbiParameters([{ type: 'string' }], [symbol]) + const decimalsBytes = encodeAbiParameters([{ type: 'uint256' }], [decimals]) + + return encodeAbiParameters( + [{ type: 'bytes' }, { type: 'bytes' }, { type: 'bytes' }], + [nameBytes, symbolBytes, decimalsBytes], + ) +} + +function scaleGasLimit(gasLimit: bigint): bigint { + return (gasLimit * BigInt(12)) / BigInt(10) +} + +async function getFeePrice<chain extends Chain | undefined>( + client: Client<Transport, chain>, +) { + const client_ = client.extend(publicActions) + const block = await client_.getBlock() + const baseFee = + typeof block.baseFeePerGas !== 'bigint' + ? await client_.getGasPrice() + : block.baseFeePerGas + const maxPriorityFeePerGas = await client_.estimateMaxPriorityFeePerGas() + + return { + maxFeePerGas: (baseFee * 3n) / 2n + maxPriorityFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas, + } +} diff --git a/src/zksync/constants/address.ts b/src/zksync/constants/address.ts index 91b9cc45b4..5851a557ab 100644 --- a/src/zksync/constants/address.ts +++ b/src/zksync/constants/address.ts @@ -17,3 +17,11 @@ export const l2BaseTokenAddress = export const l1MessengerAddress = '0x0000000000000000000000000000000000008008' as const + +export const bootloaderFormalAddress = + '0x0000000000000000000000000000000000008001' as const + +export const l1ToL2AliasOffset = + '0x1111000000000000000000000000000000001111' as const + +export const addressModulo = 2n ** 160n diff --git a/src/zksync/decorators/walletL1.test.ts b/src/zksync/decorators/walletL1.test.ts index 5966c67f7a..f967aeac4d 100644 --- a/src/zksync/decorators/walletL1.test.ts +++ b/src/zksync/decorators/walletL1.test.ts @@ -1,8 +1,13 @@ import { expect, test } from 'vitest' import { anvilMainnet, anvilZksync } from '~test/src/anvil.js' -import { mockRequestReturnData } from '~test/src/zksync.js' -import type { EIP1193RequestFn } from '~viem/types/eip1193.js' -import { publicActionsL2, walletActionsL1 } from '~viem/zksync/index.js' +import { accounts, mockRequestReturnData } from '~test/src/zksync.js' +import { privateKeyToAccount } from '~viem/accounts/privateKeyToAccount.js' +import type { EIP1193RequestFn } from '~viem/index.js' +import { + legacyEthAddress, + publicActionsL2, + walletActionsL1, +} from '~viem/zksync/index.js' const baseClient = anvilMainnet.getClient({ batch: { multicall: false }, @@ -16,6 +21,7 @@ baseClient.request = (async ({ method, params }) => { return '0x00000000000000000000000070a0F165d6f8054d0d0CF8dFd4DD2005f0AF6B55' if (method === 'eth_getTransactionCount') return 1n if (method === 'eth_gasPrice') return 150_000_000n + if (method === 'eth_maxPriorityFeePerGas') return 100_000_000n if (method === 'eth_getBlockByNumber') return anvilMainnet.forkBlockNumber if (method === 'eth_chainId') return anvilMainnet.chain.id return anvilMainnet.getClient().request({ method, params } as any) @@ -56,3 +62,16 @@ test('finalizeWithdrawal', async () => { }), ).toBeDefined() }) + +test('deposit', async () => { + const account = privateKeyToAccount(accounts[0].privateKey) + expect( + await client.deposit({ + client: zksyncClient, + token: legacyEthAddress, + to: account.address, + refundRecipient: account.address, + amount: 7_000_000_000n, + }), + ).toBeDefined() +}) diff --git a/src/zksync/decorators/walletL1.ts b/src/zksync/decorators/walletL1.ts index f02674b21d..3125685c42 100644 --- a/src/zksync/decorators/walletL1.ts +++ b/src/zksync/decorators/walletL1.ts @@ -2,6 +2,11 @@ import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import type { Account } from '../../types/account.js' import type { Chain } from '../../types/chain.js' +import { + type DepositParameters, + type DepositReturnType, + deposit, +} from '../actions/deposit.js' import { type FinalizeWithdrawalParameters, type FinalizeWithdrawalReturnType, @@ -18,6 +23,56 @@ export type WalletActionsL1< chain extends Chain | undefined = Chain | undefined, account extends Account | undefined = Account | undefined, > = { + /** + * Transfers the specified token from the associated account on the L1 network to the target account on the L2 network. + * The token can be either ETH or any ERC20 token. For ERC20 tokens, enough approved tokens must be associated with + * the specified L1 bridge (default one or the one defined in `bridgeAddress`). + * In this case, depending on is the chain ETH-based or not `approveToken` or `approveBaseToken` + * can be enabled to perform token approval. If there are already enough approved tokens for the L1 bridge, + * token approval will be skipped. + * + * @param parameters - {@link DepositParameters} + * @returns hash - The [Transaction](https://viem.sh/docs/glossary/terms#transaction) hash. {@link DepositReturnType} + * + * @example + * import { createPublicClient, createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { zksync, mainnet } from 'viem/chains' + * import { walletActionsL1, legacyEthAddress, publicActionsL2 } from 'viem/zksync' + * + * const walletClient = createWalletClient({ + * chain: mainnet, + * transport: http(), + * account: privateKeyToAccount('0x…'), + * }).extend(walletActionsL1()) + * + * const clientL2 = createPublicClient({ + * chain: zksync, + * transport: http(), + * }).extend(publicActionsL2()) + * + * const hash = await walletClient.deposit({ + * client: clientL2, + * account, + * token: legacyEthAddress, + * to: walletClient.account.address, + * amount: 1_000_000_000_000_000_000n, + * refundRecipient: walletClient.account.address, + * }) + */ + deposit: < + chainOverride extends Chain | undefined = undefined, + chainL2 extends ChainEIP712 | undefined = ChainEIP712 | undefined, + accountL2 extends Account | undefined = Account | undefined, + >( + parameters: DepositParameters< + chain, + account, + chainOverride, + chainL2, + accountL2 + >, + ) => Promise<DepositReturnType> /** * Initiates the withdrawal process which withdraws ETH or any ERC20 token * from the associated account on L2 network to the target account on L1 network. @@ -113,6 +168,7 @@ export function walletActionsL1() { >( client: Client<transport, chain, account>, ): WalletActionsL1<chain, account> => ({ + deposit: (args) => deposit(client, args), finalizeWithdrawal: (args) => finalizeWithdrawal(client, args), requestExecute: (args) => requestExecute(client, args), }) diff --git a/src/zksync/index.ts b/src/zksync/index.ts index f5de26a3e7..b5fe026787 100644 --- a/src/zksync/index.ts +++ b/src/zksync/index.ts @@ -28,6 +28,12 @@ export { type HashBytecodeErrorType, hashBytecode, } from './utils/hashBytecode.js' +export { + type DepositErrorType, + type DepositReturnType, + type DepositParameters, + deposit, +} from './actions/deposit.js' export { type EstimateFeeParameters, type EstimateFeeReturnType, diff --git a/src/zksync/utils/bridge/applyL1ToL2Alias.test.ts b/src/zksync/utils/bridge/applyL1ToL2Alias.test.ts new file mode 100644 index 0000000000..81a6c1a26e --- /dev/null +++ b/src/zksync/utils/bridge/applyL1ToL2Alias.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'vitest' +import { applyL1ToL2Alias } from './applyL1ToL2Alias.js' + +test('default', async () => { + const l1ContractAddress = '0x702942B8205E5dEdCD3374E5f4419843adA76Eeb' + const l2ContractAddress = applyL1ToL2Alias(l1ContractAddress) + expect(l2ContractAddress.toLowerCase()).equals( + '0x813A42B8205E5DedCd3374e5f4419843ADa77FFC'.toLowerCase(), + ) +}) + +test('pad zero to left', async () => { + const l1ContractAddress = '0xeeeeffffffffffffffffffffffffffffffffeeef' + const l2ContractAddress = applyL1ToL2Alias(l1ContractAddress) + expect(l2ContractAddress.toLowerCase()).equals( + '0x0000000000000000000000000000000000000000'.toLowerCase(), + ) +}) diff --git a/src/zksync/utils/bridge/applyL1ToL2Alias.ts b/src/zksync/utils/bridge/applyL1ToL2Alias.ts new file mode 100644 index 0000000000..d760c78b54 --- /dev/null +++ b/src/zksync/utils/bridge/applyL1ToL2Alias.ts @@ -0,0 +1,29 @@ +import type { Address } from '../../../accounts/index.js' +import { pad, toHex } from '../../../utils/index.js' +import { addressModulo, l1ToL2AliasOffset } from '../../constants/address.js' + +/** + * Converts the address that submitted a transaction to the inbox on L1 to the `msg.sender` viewed on L2. + * Returns the `msg.sender` of the `L1->L2` transaction as the address of the contract that initiated the transaction. + * + * All available cases: + * - During a normal transaction, if contract `A` calls contract `B`, the `msg.sender` is `A`. + * - During `L1->L2` communication, if an EOA `X` calls contract `B`, the `msg.sender` is `X`. + * - During `L1->L2` communication, if a contract `A` calls contract `B`, the `msg.sender` is `applyL1ToL2Alias(A)`. + * + * @param address - The address of the contract. + * @returns address - The transformed address representing the `msg.sender` on L2. + * + * @example + * import { applyL1ToL2Alias } from 'viem/zksync' + * + * const l1ContractAddress = "0x702942B8205E5dEdCD3374E5f4419843adA76Eeb"; + * const l2ContractAddress = utils.applyL1ToL2Alias(l1ContractAddress); + * // l2ContractAddress = "0x813A42B8205E5DedCd3374e5f4419843ADa77FFC" + */ +export function applyL1ToL2Alias(address: Address): Address { + return pad( + toHex((BigInt(address) + BigInt(l1ToL2AliasOffset)) % addressModulo), + { size: 20 }, + ) +} diff --git a/src/zksync/utils/bridge/undoL1ToL2Alias.test.ts b/src/zksync/utils/bridge/undoL1ToL2Alias.test.ts new file mode 100644 index 0000000000..2064e3f23f --- /dev/null +++ b/src/zksync/utils/bridge/undoL1ToL2Alias.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from 'vitest' +import { undoL1ToL2Alias } from './undoL1ToL2Alias.js' + +test('default', async () => { + const l2ContractAddress = '0x813A42B8205E5DedCd3374e5f4419843ADa77FFC' + const l1ContractAddress = undoL1ToL2Alias(l2ContractAddress) + expect(l1ContractAddress.toLowerCase()).equals( + '0x702942B8205E5dEdCD3374E5f4419843adA76Eeb'.toLowerCase(), + ) +}) + +test('pad zero to left', async () => { + const l2ContractAddress = '0x1111000000000000000000000000000000001111' + const l1ContractAddress = undoL1ToL2Alias(l2ContractAddress) + expect(l1ContractAddress.toLowerCase()).equals( + '0x0000000000000000000000000000000000000000'.toLowerCase(), + ) +}) + +test('offset is greater than the address', async () => { + const l2ContractAddress = '0x100' + const l1ContractAddress = undoL1ToL2Alias(l2ContractAddress) + + expect(l1ContractAddress.toLowerCase()).equals( + '0xeeeeffffffffffffffffffffffffffffffffefef'.toLowerCase(), + ) +}) diff --git a/src/zksync/utils/bridge/undoL1ToL2Alias.ts b/src/zksync/utils/bridge/undoL1ToL2Alias.ts new file mode 100644 index 0000000000..6b9ff03cd3 --- /dev/null +++ b/src/zksync/utils/bridge/undoL1ToL2Alias.ts @@ -0,0 +1,22 @@ +import type { Address } from '../../../accounts/index.js' +import { pad, toHex } from '../../../utils/index.js' +import { addressModulo, l1ToL2AliasOffset } from '../../constants/address.js' + +/** + * Converts and returns the `msg.sender` viewed on L2 to the address that submitted a transaction to the inbox on L1. + * + * @param address - The sender address viewed on L2. + * @returns address - The hash of the L2 priority operation. + * + * @example + * import { undoL1ToL2Alias } from 'viem/zksync' + * + * const l2ContractAddress = "0x813A42B8205E5DedCd3374e5f4419843ADa77FFC"; + * const l1ContractAddress = utils.undoL1ToL2Alias(l2ContractAddress); + * // const l1ContractAddress = "0x702942B8205E5dEdCD3374E5f4419843adA76Eeb" + */ +export function undoL1ToL2Alias(address: Address): Address { + let result = BigInt(address) - BigInt(l1ToL2AliasOffset) + if (result < 0n) result += addressModulo + return pad(toHex(result), { size: 20 }) +}