diff --git a/examples/create-token-bridge-eth/index.ts b/examples/create-token-bridge-eth/index.ts index 2613f8fc..56847426 100644 --- a/examples/create-token-bridge-eth/index.ts +++ b/examples/create-token-bridge-eth/index.ts @@ -2,6 +2,8 @@ import { Chain, createPublicClient, http, defineChain } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { arbitrumSepolia } from 'viem/chains'; import { + createTokenBridgePrepareSetWethGatewayTransactionReceipt, + createTokenBridgePrepareSetWethGatewayTransactionRequest, createTokenBridgePrepareTransactionReceipt, createTokenBridgePrepareTransactionRequest, } from '@arbitrum/orbit-sdk'; @@ -90,12 +92,76 @@ async function main() { console.log( `Transaction hash for second retryable is ${orbitChainRetryableReceipts[1].transactionHash}`, ); + if (orbitChainRetryableReceipts[0].status !== 'success') { + throw new Error( + `First retryable status is not success: ${orbitChainRetryableReceipts[0].status}. Aborting...`, + ); + } + if (orbitChainRetryableReceipts[1].status !== 'success') { + throw new Error( + `Second retryable status is not success: ${orbitChainRetryableReceipts[1].status}. Aborting...`, + ); + } // fetching the TokenBridge contracts const tokenBridgeContracts = await txReceipt.getTokenBridgeContracts({ parentChainPublicClient, }); console.log(`TokenBridge contracts:`, tokenBridgeContracts); + + // verifying L2 contract existence + const orbitChainRouterBytecode = await orbitChainPublicClient.getBytecode({ + address: tokenBridgeContracts.orbitChainContracts.router, + }); + + if (!orbitChainRouterBytecode || orbitChainRouterBytecode == '0x') { + throw new Error( + `TokenBridge deployment seems to have failed since orbit chain contracts do not have code`, + ); + } + + // set weth gateway + const setWethGatewayTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ + rollup: process.env.ROLLUP_ADDRESS as `0x${string}`, + parentChainPublicClient, + orbitChainPublicClient, + account: rollupOwner.address, + retryableGasOverrides: { + gasLimit: { + percentIncrease: 200n, + }, + }, + }); + + // sign and send the transaction + const setWethGatewayTxHash = await parentChainPublicClient.sendRawTransaction({ + serializedTransaction: await rollupOwner.signTransaction(setWethGatewayTxRequest), + }); + + // get the transaction receipt after waiting for the transaction to complete + const setWethGatewayTxReceipt = createTokenBridgePrepareSetWethGatewayTransactionReceipt( + await parentChainPublicClient.waitForTransactionReceipt({ hash: setWethGatewayTxHash }), + ); + + console.log( + `Weth gateway set in ${getBlockExplorerUrl(parentChain)}/tx/${ + setWethGatewayTxReceipt.transactionHash + }`, + ); + + // Wait for retryables to execute + const orbitChainSetWethGatewayRetryableReceipt = await setWethGatewayTxReceipt.waitForRetryables({ + orbitPublicClient: orbitChainPublicClient, + }); + console.log(`Retryables executed`); + console.log( + `Transaction hash for retryable is ${orbitChainSetWethGatewayRetryableReceipt[0].transactionHash}`, + ); + if (orbitChainSetWethGatewayRetryableReceipt[0].status !== 'success') { + throw new Error( + `Retryable status is not success: ${orbitChainSetWethGatewayRetryableReceipt[0].status}. Aborting...`, + ); + } } main(); diff --git a/src/createRollupFetchCoreContracts.ts b/src/createRollupFetchCoreContracts.ts new file mode 100644 index 00000000..bb78d600 --- /dev/null +++ b/src/createRollupFetchCoreContracts.ts @@ -0,0 +1,35 @@ +import { Address, PublicClient } from 'viem'; +import { validParentChainId } from './types/ParentChain'; +import { CoreContracts } from './types/CoreContracts'; +import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash'; +import { createRollupPrepareTransactionReceipt } from './createRollupPrepareTransactionReceipt'; + +export type CreateRollupFetchCoreContractsParams = { + rollup: Address; + publicClient: PublicClient; +}; + +export async function createRollupFetchCoreContracts({ + rollup, + publicClient, +}: CreateRollupFetchCoreContractsParams): Promise { + const chainId = publicClient.chain?.id; + + if (!validParentChainId(chainId)) { + throw new Error('chainId is undefined'); + } + + // getting core contract addresses + const transactionHash = await createRollupFetchTransactionHash({ + rollup, + publicClient, + }); + + const transactionReceipt = createRollupPrepareTransactionReceipt( + await publicClient.waitForTransactionReceipt({ + hash: transactionHash, + }), + ); + + return transactionReceipt.getCoreContracts(); +} diff --git a/src/createTokenBridge-ethers.ts b/src/createTokenBridge-ethers.ts index bb4f8785..6ea79b36 100644 --- a/src/createTokenBridge-ethers.ts +++ b/src/createTokenBridge-ethers.ts @@ -192,6 +192,38 @@ const getEstimateForDeployingFactory = async ( return deployFactoryGasParams; }; +export const getEstimateForSettingGateway = async ( + l1ChainOwnerAddress: Address, + l1UpgradeExecutorAddress: Address, + l1GatewayRouterAddress: Address, + setGatewaysCalldata: `0x${string}`, + l1Provider: ethers.providers.Provider, + l2Provider: ethers.providers.Provider, +) => { + //// run retryable estimate for setting a token gateway in the router + const l1ToL2MsgGasEstimate = new L1ToL2MessageGasEstimator(l2Provider); + + const setGatewaysGasParams = await l1ToL2MsgGasEstimate.estimateAll( + { + from: l1UpgradeExecutorAddress, + to: l1GatewayRouterAddress, + l2CallValue: BigNumber.from(0), + excessFeeRefundAddress: l1ChainOwnerAddress, + callValueRefundAddress: l1ChainOwnerAddress, + data: setGatewaysCalldata, + }, + await getBaseFee(l1Provider), + l1Provider, + ); + + return { + gasLimit: setGatewaysGasParams.gasLimit.toBigInt(), + maxFeePerGas: setGatewaysGasParams.maxFeePerGas.toBigInt(), + maxSubmissionCost: setGatewaysGasParams.maxSubmissionCost.toBigInt(), + deposit: setGatewaysGasParams.deposit.toBigInt(), + }; +}; + const registerNewNetwork = async ( l1Provider: JsonRpcProvider, l2Provider: JsonRpcProvider, diff --git a/src/createTokenBridge.integration.test.ts b/src/createTokenBridge.integration.test.ts index dbadeb21..d54e6b70 100644 --- a/src/createTokenBridge.integration.test.ts +++ b/src/createTokenBridge.integration.test.ts @@ -3,9 +3,9 @@ import { createPublicClient, encodeFunctionData, http, - maxInt256, parseEther, zeroAddress, + parseAbi, } from 'viem'; import { execSync } from 'node:child_process'; @@ -14,12 +14,11 @@ import { getNitroTestnodePrivateKeyAccounts } from './testHelpers'; import { createTokenBridgePrepareTransactionRequest } from './createTokenBridgePrepareTransactionRequest'; import { createTokenBridgePrepareTransactionReceipt } from './createTokenBridgePrepareTransactionReceipt'; import { deployTokenBridgeCreator } from './createTokenBridge-testHelpers'; -import { - CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams, - createTokenBridgeEnoughCustomFeeTokenAllowance, -} from './createTokenBridgeEnoughCustomFeeTokenAllowance'; +import { CreateTokenBridgeEnoughCustomFeeTokenAllowanceParams } from './createTokenBridgeEnoughCustomFeeTokenAllowance'; import { createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest } from './createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest'; import { erc20 } from './contracts'; +import { createTokenBridgePrepareSetWethGatewayTransactionRequest } from './createTokenBridgePrepareSetWethGatewayTransactionRequest'; +import { createTokenBridgePrepareSetWethGatewayTransactionReceipt } from './createTokenBridgePrepareSetWethGatewayTransactionReceipt'; type TestnodeInformation = { rollup: `0x${string}`; @@ -161,6 +160,60 @@ it(`successfully deploys token bridge contracts through token bridge creator`, a expect(tokenBridgeContracts.orbitChainContracts.beaconProxyFactory).not.toEqual(zeroAddress); expect(tokenBridgeContracts.orbitChainContracts.upgradeExecutor).not.toEqual(zeroAddress); expect(tokenBridgeContracts.orbitChainContracts.multicall).not.toEqual(zeroAddress); + + // set weth gateway + const setWethGatewayTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ + rollup: testnodeInformation.rollup, + parentChainPublicClient: nitroTestnodeL1Client, + orbitChainPublicClient: nitroTestnodeL2Client, + account: l2RollupOwner.address, + retryableGasOverrides: { + gasLimit: { + base: 100_000n, + }, + }, + tokenBridgeCreatorAddressOverride: tokenBridgeCreator, + }); + + // sign and send the transaction + const setWethGatewayTxHash = await nitroTestnodeL1Client.sendRawTransaction({ + serializedTransaction: await l2RollupOwner.signTransaction(setWethGatewayTxRequest), + }); + + // get the transaction receipt after waiting for the transaction to complete + const setWethGatewayTxReceipt = createTokenBridgePrepareSetWethGatewayTransactionReceipt( + await nitroTestnodeL1Client.waitForTransactionReceipt({ hash: setWethGatewayTxHash }), + ); + + // checking retryables execution + const orbitChainSetGatewayRetryableReceipt = await setWethGatewayTxReceipt.waitForRetryables({ + orbitPublicClient: nitroTestnodeL2Client, + }); + expect(orbitChainSetGatewayRetryableReceipt).toHaveLength(1); + expect(orbitChainSetGatewayRetryableReceipt[0].status).toEqual('success'); + + // verify weth gateway (parent chain) + const registeredWethGatewayOnParentChain = await nitroTestnodeL1Client.readContract({ + address: tokenBridgeContracts.parentChainContracts.router, + abi: parseAbi(['function l1TokenToGateway(address) view returns (address)']), + functionName: 'l1TokenToGateway', + args: [tokenBridgeContracts.parentChainContracts.weth], + }); + expect(registeredWethGatewayOnParentChain).toEqual( + tokenBridgeContracts.parentChainContracts.wethGateway, + ); + + // verify weth gateway (orbit chain) + // Note: we pass the address of the token on the parent chain when asking for the registered gateway on the orbit chain + const registeredWethGatewayOnOrbitChain = await nitroTestnodeL2Client.readContract({ + address: tokenBridgeContracts.orbitChainContracts.router, + abi: parseAbi(['function l1TokenToGateway(address) view returns (address)']), + functionName: 'l1TokenToGateway', + args: [tokenBridgeContracts.parentChainContracts.weth], + }); + expect(registeredWethGatewayOnOrbitChain).toEqual( + tokenBridgeContracts.orbitChainContracts.wethGateway, + ); }); it(`successfully deploys token bridge contracts with a custom fee token through token bridge creator`, async () => { diff --git a/src/createTokenBridgePrepareSetWethGatewayTransactionReceipt.ts b/src/createTokenBridgePrepareSetWethGatewayTransactionReceipt.ts new file mode 100644 index 00000000..6a8df10d --- /dev/null +++ b/src/createTokenBridgePrepareSetWethGatewayTransactionReceipt.ts @@ -0,0 +1,56 @@ +import { PublicClient, TransactionReceipt } from 'viem'; +import { L1ToL2MessageStatus, L1TransactionReceipt } from '@arbitrum/sdk'; +import { TransactionReceipt as EthersTransactionReceipt } from '@ethersproject/abstract-provider'; + +import { publicClientToProvider } from './ethers-compat/publicClientToProvider'; +import { viemTransactionReceiptToEthersTransactionReceipt } from './ethers-compat/viemTransactionReceiptToEthersTransactionReceipt'; +import { ethersTransactionReceiptToViemTransactionReceipt } from './ethers-compat/ethersTransactionReceiptToViemTransactionReceipt'; + +type RedeemedRetryableTicket = { + status: L1ToL2MessageStatus.REDEEMED; + l2TxReceipt: EthersTransactionReceipt; +}; + +export type WaitForRetryablesParameters = { + orbitPublicClient: PublicClient; +}; + +export type WaitForRetryablesResult = [TransactionReceipt]; + +export type CreateTokenBridgeSetWethGatewayTransactionReceipt = TransactionReceipt & { + waitForRetryables(params: WaitForRetryablesParameters): Promise; +}; + +export function createTokenBridgePrepareSetWethGatewayTransactionReceipt( + txReceipt: TransactionReceipt, +): CreateTokenBridgeSetWethGatewayTransactionReceipt { + return { + ...txReceipt, + waitForRetryables: async function ({ + orbitPublicClient, + }: WaitForRetryablesParameters): Promise { + const ethersTxReceipt = viemTransactionReceiptToEthersTransactionReceipt(txReceipt); + const parentChainTxReceipt = new L1TransactionReceipt(ethersTxReceipt); + const orbitProvider = publicClientToProvider(orbitPublicClient); + const messages = await parentChainTxReceipt.getL1ToL2Messages(orbitProvider); + const messagesResults = await Promise.all(messages.map((message) => message.waitForStatus())); + + if (messagesResults.length !== 1) { + throw Error(`Unexpected number of retryable tickets: ${messagesResults.length}`); + } + + if (messagesResults[0].status !== L1ToL2MessageStatus.REDEEMED) { + throw Error(`Unexpected status for retryable ticket: ${messages[0].retryableCreationId}`); + } + + return ( + // these type casts are both fine as we already checked everything above + (messagesResults as unknown as [RedeemedRetryableTicket]) + // + .map((result) => + ethersTransactionReceiptToViemTransactionReceipt(result.l2TxReceipt), + ) as WaitForRetryablesResult + ); + }, + }; +} diff --git a/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts b/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts new file mode 100644 index 00000000..f42c5a6b --- /dev/null +++ b/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -0,0 +1,229 @@ +import { Address, PublicClient, encodeFunctionData, parseAbi } from 'viem'; + +import { isCustomFeeTokenChain } from './utils/isCustomFeeTokenChain'; +import { upgradeExecutorEncodeFunctionData } from './upgradeExecutor'; +import { createTokenBridgeFetchTokenBridgeContracts } from './createTokenBridgeFetchTokenBridgeContracts'; +import { createRollupFetchCoreContracts } from './createRollupFetchCoreContracts'; +import { publicClientToProvider } from './ethers-compat/publicClientToProvider'; +import { getEstimateForSettingGateway } from './createTokenBridge-ethers'; +import { GasOverrideOptions, applyPercentIncrease } from './utils/gasOverrides'; +import { Prettify } from './types/utils'; +import { validParentChainId } from './types/ParentChain'; +import { WithTokenBridgeCreatorAddressOverride } from './types/createTokenBridgeTypes'; + +export type TransactionRequestRetryableGasOverrides = { + gasLimit?: GasOverrideOptions; + maxFeePerGas?: GasOverrideOptions; + maxSubmissionCost?: GasOverrideOptions; +}; + +export type CreateTokenBridgePrepareRegisterWethGatewayTransactionRequestParams = Prettify< + WithTokenBridgeCreatorAddressOverride<{ + rollup: Address; + parentChainPublicClient: PublicClient; + orbitChainPublicClient: PublicClient; + account: Address; + retryableGasOverrides?: TransactionRequestRetryableGasOverrides; + }> +>; + +const parentChainGatewayRouterAbi = [ + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'l1TokenToGateway', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address[]', + name: '_token', + type: 'address[]', + }, + { + internalType: 'address[]', + name: '_gateway', + type: 'address[]', + }, + { + internalType: 'uint256', + name: '_maxGas', + type: 'uint256', + }, + { + internalType: 'uint256', + name: '_gasPriceBid', + type: 'uint256', + }, + { + internalType: 'uint256', + name: '_maxSubmissionCost', + type: 'uint256', + }, + ], + name: 'setGateways', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'payable', + type: 'function', + }, +]; + +export async function createTokenBridgePrepareSetWethGatewayTransactionRequest({ + rollup, + parentChainPublicClient, + orbitChainPublicClient, + account, + retryableGasOverrides, + tokenBridgeCreatorAddressOverride, +}: CreateTokenBridgePrepareRegisterWethGatewayTransactionRequestParams) { + const chainId = parentChainPublicClient.chain?.id; + + if (!validParentChainId(chainId)) { + throw new Error('chainId is undefined'); + } + + // check for custom fee token chain + if ( + await isCustomFeeTokenChain({ + rollup, + parentChainPublicClient, + }) + ) { + throw new Error('chain is custom fee token chain, no need to register the weth gateway.'); + } + + // get inbox address from rollup + const inbox = await parentChainPublicClient.readContract({ + address: rollup, + abi: parseAbi(['function inbox() view returns (address)']), + functionName: 'inbox', + }); + + // get token bridge contracts + const tokenBridgeContracts = await createTokenBridgeFetchTokenBridgeContracts({ + inbox, + parentChainPublicClient, + tokenBridgeCreatorAddressOverride, + }); + + // check whether the weth gateway is already registered in the router + const registeredWethGateway = await parentChainPublicClient.readContract({ + address: tokenBridgeContracts.parentChainContracts.router, + abi: parentChainGatewayRouterAbi, + functionName: 'l1TokenToGateway', + args: [tokenBridgeContracts.parentChainContracts.weth], + }); + if (registeredWethGateway === tokenBridgeContracts.parentChainContracts.wethGateway) { + throw new Error('weth gateway is already registered in the router.'); + } + + const rollupCoreContracts = await createRollupFetchCoreContracts({ + rollup, + publicClient: parentChainPublicClient, + }); + + // ethers providers + const parentChainProvider = publicClientToProvider(parentChainPublicClient); + const orbitChainProvider = publicClientToProvider(orbitChainPublicClient); + + // encode data for the setGateways call + // (we first encode dummy data, to get the retryable message estimates) + const setGatewaysDummyCalldata = encodeFunctionData({ + abi: parentChainGatewayRouterAbi, + functionName: 'setGateways', + args: [ + [tokenBridgeContracts.parentChainContracts.weth], + [tokenBridgeContracts.parentChainContracts.wethGateway], + 1n, // _maxGas + 1n, // _gasPriceBid + 1n, // _maxSubmissionCost + ], + }); + const retryableTicketGasEstimates = await getEstimateForSettingGateway( + account, + rollupCoreContracts.upgradeExecutor, + tokenBridgeContracts.parentChainContracts.router, + setGatewaysDummyCalldata, + parentChainProvider, + orbitChainProvider, + ); + + //// apply gas overrides + const gasLimit = + retryableGasOverrides && retryableGasOverrides.gasLimit + ? applyPercentIncrease({ + base: retryableGasOverrides.gasLimit.base ?? retryableTicketGasEstimates.gasLimit, + percentIncrease: retryableGasOverrides.gasLimit.percentIncrease, + }) + : retryableTicketGasEstimates.gasLimit; + + const maxFeePerGas = + retryableGasOverrides && retryableGasOverrides.maxFeePerGas + ? applyPercentIncrease({ + base: retryableGasOverrides.maxFeePerGas.base ?? retryableTicketGasEstimates.maxFeePerGas, + percentIncrease: retryableGasOverrides.maxFeePerGas.percentIncrease, + }) + : retryableTicketGasEstimates.maxFeePerGas; + + const maxSubmissionCost = + retryableGasOverrides && retryableGasOverrides.maxSubmissionCost + ? applyPercentIncrease({ + base: + retryableGasOverrides.maxSubmissionCost.base ?? + retryableTicketGasEstimates.maxSubmissionCost, + percentIncrease: retryableGasOverrides.maxSubmissionCost.percentIncrease, + }) + : retryableTicketGasEstimates.maxSubmissionCost; + + const deposit = gasLimit * maxFeePerGas + maxSubmissionCost; + + // (and then we encode the real data, to send the transaction) + const setGatewaysCalldata = encodeFunctionData({ + abi: parentChainGatewayRouterAbi, + functionName: 'setGateways', + args: [ + [tokenBridgeContracts.parentChainContracts.weth], + [tokenBridgeContracts.parentChainContracts.wethGateway], + gasLimit, // _maxGas + maxFeePerGas, // _gasPriceBid + maxSubmissionCost, // _maxSubmissionCost + ], + }); + + // prepare the transaction request with a call to the upgrade executor + const request = await parentChainPublicClient.prepareTransactionRequest({ + chain: parentChainPublicClient.chain, + to: rollupCoreContracts.upgradeExecutor, + data: upgradeExecutorEncodeFunctionData({ + functionName: 'executeCall', + args: [ + tokenBridgeContracts.parentChainContracts.router, // target + setGatewaysCalldata, // targetCallData + ], + }), + value: deposit, + account, + }); + + return { ...request, chainId }; +} diff --git a/src/index.ts b/src/index.ts index c18b0ba9..55d3ef61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import { createRollupFetchTransactionHash, CreateRollupFetchTransactionHashParams, } from './createRollupFetchTransactionHash'; +import { createRollupFetchCoreContracts } from './createRollupFetchCoreContracts'; import { setValidKeyset, SetValidKeysetParams } from './setValidKeyset'; import { setValidKeysetPrepareTransactionRequest, @@ -61,6 +62,8 @@ import { } from './createTokenBridgePrepareTransactionRequest'; import { createTokenBridgePrepareTransactionReceipt } from './createTokenBridgePrepareTransactionReceipt'; import { createTokenBridgeFetchTokenBridgeContracts } from './createTokenBridgeFetchTokenBridgeContracts'; +import { createTokenBridgePrepareSetWethGatewayTransactionRequest } from './createTokenBridgePrepareSetWethGatewayTransactionRequest'; +import { createTokenBridgePrepareSetWethGatewayTransactionReceipt } from './createTokenBridgePrepareSetWethGatewayTransactionReceipt'; import { prepareKeyset } from './prepareKeyset'; import * as utils from './utils'; @@ -84,6 +87,7 @@ export { CreateRollupTransactionReceipt, createRollupFetchTransactionHash, CreateRollupFetchTransactionHashParams, + createRollupFetchCoreContracts, setValidKeyset, SetValidKeysetParams, setValidKeysetPrepareTransactionRequest, @@ -113,4 +117,6 @@ export { CreateTokenBridgePrepareTransactionRequestParams, createTokenBridgePrepareTransactionReceipt, createTokenBridgeFetchTokenBridgeContracts, + createTokenBridgePrepareSetWethGatewayTransactionRequest, + createTokenBridgePrepareSetWethGatewayTransactionReceipt, };