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 51dd3790..bb68b267 100644 --- a/src/createTokenBridge-ethers.ts +++ b/src/createTokenBridge-ethers.ts @@ -38,6 +38,8 @@ const IInbox__factory = NamedFactoryInstance(IInbox); import IERC20Bridge from '@arbitrum/nitro-contracts/build/contracts/src/bridge/IERC20Bridge.sol/IERC20Bridge.json'; const IERC20Bridge__factory = NamedFactoryInstance(IERC20Bridge); import IERC20 from '@arbitrum/nitro-contracts/build/contracts/@openzeppelin/contracts/token/ERC20/IERC20.sol/IERC20.json'; +import { TransactionRequestGasOverrides } from './types/TransactionRequestGasOverrides'; +import { GasOverrides } from '@arbitrum/sdk/dist/lib/message/L1ToL2MessageGasEstimator'; const IERC20__factory = NamedFactoryInstance(IERC20); @@ -320,6 +322,59 @@ export 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, + gasOverrides?: TransactionRequestGasOverrides, +) => { + //// run retryable estimate for setting a token gateway in the router + const l1ToL2MsgGasEstimate = new L1ToL2MessageGasEstimator(l2Provider); + + // applying gas overrides + const gasOverridesForEstimation: GasOverrides = {}; + if (gasOverrides && gasOverrides.gasLimit) { + gasOverridesForEstimation.gasLimit = { + min: undefined, + percentIncrease: undefined, + }; + + if (gasOverrides.gasLimit.minimum) { + gasOverridesForEstimation.gasLimit.min = BigNumber.from(gasOverrides.gasLimit.minimum); + } + + if (gasOverrides.gasLimit.percentIncrease) { + gasOverridesForEstimation.gasLimit.percentIncrease = BigNumber.from( + gasOverrides.gasLimit.percentIncrease, + ); + } + } + + const setGatewaysGasParams = await l1ToL2MsgGasEstimate.estimateAll( + { + from: l1UpgradeExecutorAddress, + to: l1GatewayRouterAddress, + l2CallValue: BigNumber.from(0), + excessFeeRefundAddress: l1ChainOwnerAddress, + callValueRefundAddress: l1ChainOwnerAddress, + data: setGatewaysCalldata, + }, + await getBaseFee(l1Provider), + l1Provider, + gasOverridesForEstimation, + ); + + return { + gasLimit: setGatewaysGasParams.gasLimit.toBigInt(), + maxFeePerGas: setGatewaysGasParams.maxFeePerGas.toBigInt(), + maxSubmissionCost: setGatewaysGasParams.maxSubmissionCost.toBigInt(), + deposit: setGatewaysGasParams.deposit.toBigInt(), + }; +}; + export const getSigner = (provider: JsonRpcProvider, key?: string) => { if (!key && !provider) throw new Error('Provide at least one of key or provider.'); if (key) return new Wallet(key).connect(provider); diff --git a/src/createTokenBridge.integration.test.ts b/src/createTokenBridge.integration.test.ts index c5d16ff9..6522768c 100644 --- a/src/createTokenBridge.integration.test.ts +++ b/src/createTokenBridge.integration.test.ts @@ -4,6 +4,7 @@ import { encodeFunctionData, http, maxInt256, + parseAbi, parseEther, zeroAddress, } from 'viem'; @@ -17,6 +18,8 @@ import { deployTokenBridgeCreator } from './createTokenBridge-testHelpers'; import { createTokenBridgeEnoughCustomFeeTokenAllowance } from './createTokenBridgeEnoughCustomFeeTokenAllowance'; import { createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest } from './createTokenBridgePrepareCustomFeeTokenApprovalTransactionRequest'; import { erc20 } from './contracts'; +import { createTokenBridgePrepareSetWethGatewayTransactionRequest } from './createTokenBridgePrepareSetWethGatewayTransactionRequest'; +import { createTokenBridgePrepareSetWethGatewayTransactionReceipt } from './createTokenBridgePrepareSetWethGatewayTransactionReceipt'; type TestnodeInformation = { rollup: `0x${string}`; @@ -149,6 +152,46 @@ it(`successfully deploys token bridge contracts through token bridge creator`, a expect(tokenBridgeContracts.childChainContracts.beaconProxyFactory).not.toEqual(zeroAddress); expect(tokenBridgeContracts.childChainContracts.upgradeExecutor).not.toEqual(zeroAddress); expect(tokenBridgeContracts.childChainContracts.multicall).not.toEqual(zeroAddress); + + // set weth gateway + // ----------------------------- + const setWethGatewayTxRequest = await createTokenBridgePrepareSetWethGatewayTransactionRequest({ + rollup: testnodeInformation.rollup, + parentChainPublicClient: nitroTestnodeL1Client, + childChainPublicClient: nitroTestnodeL2Client, + account: l2RollupOwner.address, + gasOverrides: { + gasLimit: { + percentIncrease: 200n, + }, + }, + }); + + // 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 }), + ); + + function waitForRetryablesOfSetWethGateway() { + return setWethGatewayTxReceipt.waitForRetryables({ orbitPublicClient: nitroTestnodeL2Client }); + } + + expect(setWethGatewayTxReceipt.status).toEqual('success'); + await expect(waitForRetryablesOfSetWethGateway()).resolves.toHaveLength(1); + + // verify weth gateway + const registeredWethGateway = await nitroTestnodeL1Client.readContract({ + address: tokenBridgeContracts.parentChainContracts.router, + abi: parseAbi(['function l1TokenToGateway(address) view returns (address)']), + functionName: 'l1TokenToGateway', + args: [tokenBridgeContracts.parentChainContracts.weth], + }); + expect(registeredWethGateway).toEqual(tokenBridgeContracts.parentChainContracts.wethGateway); }); it(`successfully deploys token bridge contracts with a custom fee token through token bridge creator`, async () => { @@ -305,4 +348,9 @@ it(`successfully deploys token bridge contracts with a custom fee token through expect(tokenBridgeContracts.childChainContracts.beaconProxyFactory).not.toEqual(zeroAddress); expect(tokenBridgeContracts.childChainContracts.upgradeExecutor).not.toEqual(zeroAddress); expect(tokenBridgeContracts.childChainContracts.multicall).not.toEqual(zeroAddress); + + // verify weth gateway and token contracts + expect(tokenBridgeContracts.parentChainContracts.wethGateway).toEqual(zeroAddress); + expect(tokenBridgeContracts.childChainContracts.wethGateway).toEqual(zeroAddress); + expect(tokenBridgeContracts.childChainContracts.weth).toEqual(zeroAddress); }); diff --git a/src/createTokenBridgePrepareSetWethGatewayTransactionReceipt.ts b/src/createTokenBridgePrepareSetWethGatewayTransactionReceipt.ts new file mode 100644 index 00000000..f469eb08 --- /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, 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 l1TxReceipt = new L1TransactionReceipt(ethersTxReceipt); + const orbitProvider = publicClientToProvider(orbitPublicClient); + const messages = await l1TxReceipt.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, RedeemedRetryableTicket]) + // + .map((result) => + ethersTransactionReceiptToViemTransactionReceipt(result.l2TxReceipt), + ) as WaitForRetryablesResult + ); + }, + }; +} diff --git a/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts b/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts new file mode 100644 index 00000000..f2338c70 --- /dev/null +++ b/src/createTokenBridgePrepareSetWethGatewayTransactionRequest.ts @@ -0,0 +1,189 @@ +import { Address, PublicClient, encodeFunctionData, parseAbi } from 'viem'; + +import { validParentChainId } from './types/ParentChain'; +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 { TransactionRequestGasOverrides } from './types/TransactionRequestGasOverrides'; + +export type CreateTokenBridgePrepareRegisterWethGatewayTransactionRequestParams = { + rollup: Address; + parentChainPublicClient: PublicClient; + childChainPublicClient: PublicClient; + account: Address; + gasOverrides?: TransactionRequestGasOverrides; +}; + +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, + childChainPublicClient, + account, + gasOverrides, +}: 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, + }); + + // 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 childChainProvider = publicClientToProvider(childChainPublicClient); + + // 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, + childChainProvider, + gasOverrides, + ); + + // (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], + retryableTicketGasEstimates.gasLimit, // _maxGas + retryableTicketGasEstimates.maxFeePerGas, // _gasPriceBid + retryableTicketGasEstimates.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: retryableTicketGasEstimates.deposit, + account, + }); + + return { ...request, chainId }; +} diff --git a/src/createTokenBridgePrepareTransactionRequest.ts b/src/createTokenBridgePrepareTransactionRequest.ts index 94441d9d..a6d02797 100644 --- a/src/createTokenBridgePrepareTransactionRequest.ts +++ b/src/createTokenBridgePrepareTransactionRequest.ts @@ -6,16 +6,7 @@ import { validParentChainId } from './types/ParentChain'; import { createTokenBridgeGetInputs } from './createTokenBridge-ethers'; import { publicClientToProvider } from './ethers-compat/publicClientToProvider'; import { isCustomFeeTokenChain } from './utils/isCustomFeeTokenChain'; - -type GasOverrideOptions = { - minimum?: bigint; - percentIncrease?: bigint; -}; - -export type TransactionRequestGasOverrides = { - gasLimit?: GasOverrideOptions; - retryableTicketFees?: GasOverrideOptions; -}; +import { TransactionRequestGasOverrides } from './types/TransactionRequestGasOverrides'; export async function createTokenBridgePrepareTransactionRequest({ params, diff --git a/src/index.ts b/src/index.ts index 8eb74230..68b976d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ import { createRollupFetchTransactionHash, CreateRollupFetchTransactionHashParams, } from './createRollupFetchTransactionHash'; +import { createRollupFetchCoreContracts } from './createRollupFetchCoreContracts'; import { setValidKeyset, SetValidKeysetParams } from './setValidKeyset'; import { setValidKeysetPrepareTransactionRequest, @@ -54,6 +55,8 @@ import { import { createTokenBridgePrepareTransactionRequest } 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'; @@ -75,6 +78,7 @@ export { CreateRollupTransactionReceipt, createRollupFetchTransactionHash, CreateRollupFetchTransactionHashParams, + createRollupFetchCoreContracts, setValidKeyset, SetValidKeysetParams, setValidKeysetPrepareTransactionRequest, @@ -103,4 +107,6 @@ export { createTokenBridgePrepareTransactionRequest, createTokenBridgePrepareTransactionReceipt, createTokenBridgeFetchTokenBridgeContracts, + createTokenBridgePrepareSetWethGatewayTransactionRequest, + createTokenBridgePrepareSetWethGatewayTransactionReceipt, }; diff --git a/src/types/TransactionRequestGasOverrides.ts b/src/types/TransactionRequestGasOverrides.ts new file mode 100644 index 00000000..2f21b782 --- /dev/null +++ b/src/types/TransactionRequestGasOverrides.ts @@ -0,0 +1,9 @@ +type GasOverrideOptions = { + minimum?: bigint; + percentIncrease?: bigint; +}; + +export type TransactionRequestGasOverrides = { + gasLimit?: GasOverrideOptions; + retryableTicketFees?: GasOverrideOptions; +};