diff --git a/src/__snapshots__/getUpgradeExecutor.integration.test.ts.snap b/src/__snapshots__/getUpgradeExecutor.integration.test.ts.snap new file mode 100644 index 00000000..5802ac4b --- /dev/null +++ b/src/__snapshots__/getUpgradeExecutor.integration.test.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`successfully get upgrade executor > from child chain 1`] = `"0x24198F8A339cd3C47AEa3A764A20d2dDaB4D1b5b"`; diff --git a/src/getUpgradeExecutor.integration.test.ts b/src/getUpgradeExecutor.integration.test.ts new file mode 100644 index 00000000..399c18ca --- /dev/null +++ b/src/getUpgradeExecutor.integration.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { createPublicClient, http } from 'viem'; + +import { nitroTestnodeL2, nitroTestnodeL3 } from './chains'; +import { getInformationFromTestnode } from './testHelpers'; +import { getUpgradeExecutor } from './getUpgradeExecutor'; + +const { l3UpgradeExecutor, l3Rollup } = getInformationFromTestnode(); + +// Tests can be enabled once we run one node per integration test +describe('successfully get upgrade executor', () => { + it('from parent chain', async () => { + const parentChainClient = createPublicClient({ + chain: nitroTestnodeL2, + transport: http(), + }); + + const upgradeExecutor = await getUpgradeExecutor(parentChainClient, { + rollup: l3Rollup, + }); + expect(upgradeExecutor?.toLowerCase()).toEqual(l3UpgradeExecutor); + }); + + it('from child chain', async () => { + const childChainClient = createPublicClient({ + chain: nitroTestnodeL3, + transport: http(), + }); + const upgradeExecutor = await getUpgradeExecutor(childChainClient); + expect(upgradeExecutor).toMatchSnapshot(); + }); +}); diff --git a/src/getUpgradeExecutor.ts b/src/getUpgradeExecutor.ts new file mode 100644 index 00000000..d6cd9059 --- /dev/null +++ b/src/getUpgradeExecutor.ts @@ -0,0 +1,102 @@ +import { Address, Chain, PublicClient, Transport, getAbiItem } from 'viem'; +import { rollupAdminLogicABI } from './abi'; +import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash'; +import { isValidParentChainId } from './types/ParentChain'; +import { arbOwnerPublic, upgradeExecutor } from './contracts'; +import { UPGRADE_EXECUTOR_ROLE_ADMIN } from './upgradeExecutorEncodeFunctionData'; + +const AdminChangedAbi = getAbiItem({ abi: rollupAdminLogicABI, name: 'AdminChanged' }); + +export type GetUpgradeExecutorParams = { + /** Address of the rollup we're getting logs from */ + rollup: Address; +}; +/** + * Address of the current upgrade executor + */ +export type GetUpgradeExecutorReturnType = Address | undefined; + +/** + * Return upgrade executor address for a parent or child chain + * + * Docs: https://docs.arbitrum.io/launch-orbit-chain/concepts/chain-ownership + * + * @param {PublicClient} publicClient - The chain Viem Public Client + * @param {GetUpgradeExecutorParams} GetUpgradeExecutorParams {@link GetUpgradeExecutorParams} + * + * @returns Promise<{@link GetUpgradeExecutorReturnType}> + * + * @example + * const upgradeExecutor = getUpgradeExecutor(client, { + * rollup: '0xc47dacfbaa80bd9d8112f4e8069482c2a3221336' + * }); + * + */ +export async function getUpgradeExecutor( + publicClient: PublicClient, + params?: GetUpgradeExecutorParams, +): Promise { + const isParentChain = isValidParentChainId(publicClient.chain?.id); + if (isParentChain && !params?.rollup) { + throw new Error('[getUpgradeExecutor] requires a rollup address'); + } + + // Parent chain, get the newOwner args from the last event + if (isParentChain && params) { + let blockNumber: bigint | 'earliest'; + + try { + const createRollupTransactionHash = await createRollupFetchTransactionHash({ + rollup: params.rollup, + publicClient, + }); + const receipt = await publicClient.waitForTransactionReceipt({ + hash: createRollupTransactionHash, + }); + blockNumber = receipt.blockNumber; + } catch (e) { + console.warn(`[getUpgradeExecutor] ${(e as any).message}`); + blockNumber = 'earliest'; + } + + const events = await publicClient.getLogs({ + address: params.rollup, + /** + * The event comes from: + * - event AdminChanged(address previousAdmin, address newAdmin) + * - ERC1967Upgrade + * - DoubleLogicUUPSUpgradeable + * - RollupAdminLogic + * + * see https://github.com/OffchainLabs/nitro-contracts/blob/90037b996509312ef1addb3f9352457b8a99d6a6/src/rollup/RollupAdminLogic.sol#L182 + */ + events: [AdminChangedAbi], + fromBlock: blockNumber, + toBlock: 'latest', + }); + + return events[events.length - 1].args.newAdmin; + } + + // Child chain, check for all chainOwners + const chainOwners = await publicClient.readContract({ + abi: arbOwnerPublic.abi, + functionName: 'getAllChainOwners', + address: arbOwnerPublic.address, + }); + + const results = await Promise.allSettled( + chainOwners.map((chainOwner) => + publicClient.readContract({ + address: chainOwner, + abi: upgradeExecutor.abi, + functionName: 'hasRole', + args: [UPGRADE_EXECUTOR_ROLE_ADMIN, chainOwner], + }), + ), + ); + const upgradeExecutorIndex = results.findIndex( + (p) => p.status === 'fulfilled' && p.value === true, + ); + return chainOwners[upgradeExecutorIndex]; +} diff --git a/src/getUpgradeExecutor.unit.test.ts b/src/getUpgradeExecutor.unit.test.ts new file mode 100644 index 00000000..139941f6 --- /dev/null +++ b/src/getUpgradeExecutor.unit.test.ts @@ -0,0 +1,105 @@ +import { Address, EIP1193RequestFn, createPublicClient, createTransport, http, padHex } from 'viem'; +import { arbitrum, arbitrumSepolia } from 'viem/chains'; +import { it, vi, describe } from 'vitest'; +import { getUpgradeExecutor } from './getUpgradeExecutor'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; +import { xai } from './testHelpers'; + +const rollupAddress = '0xe0875cbd144fe66c015a95e5b2d2c15c3b612179'; + +function mockAdminChangedEvent(previousAdmin: Address, newAdmin: Address) { + return { + address: '0xa58f38102579dae7c584850780dda55744f67df1', + blockNumber: 183097536n, + transactionHash: '0x13baa9be2bf267fde01e730855d34526f339a21f1877af175f0958e5dc546e6d', + transactionIndex: 1, + blockHash: '0x31d403a11112e6a8be0e24423df83341790a8c1cc1728a2c2deff1b683961635', + logIndex: 0, + data: '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000013300000000000000010000000000000001012160184f37a4eaea75e8252d38d5b3f0298703794d58f38b3551104ce0c2472aea78f53ccd07fdb7b1c5b08444d2be9025d519ccc21d12fd9f8b67c50615694b626aaec898b9c1e613b0c17aac28539ee667a98e08d734193de9b2e612b4b082439506aa6ff965bfff2e8d3e6ade9e038412d767778850c717b388fb17e40c359c8ef3b99b4e7aee94b88f7d96c09e8d522a0f24d90efa7db34f42cefa18ae1ab1e08f780e613e0baf8e28c322a0d52b915fcff3e143a9daa7c2ba525029066f8230120e9803fd21d332015b3ec22ae180cbd1f3cf89561a0c5bd914dc5f746d692cefcb4762a012af0fe55c1148f138221a196fbec9942400b7772ce371c8ccc8ed0cd3926398cd62f1b900758b82591174295eb7ac00555d40051ad280ceb2cfa700000000000000000000000000', + args: { + previousAdmin, + newAdmin, + }, + eventName: 'AdminChanged', + }; +} + +describe.concurrent('getUpgradeExecutor', () => { + it('should return upgrade executor on arbitrum one for xai', async ({ expect }) => { + const arbitrumOneClient = createPublicClient({ + chain: arbitrum, + transport: http(), + }); + const upgradeExecutor = await getUpgradeExecutor(arbitrumOneClient, { + rollup: '0xc47dacfbaa80bd9d8112f4e8069482c2a3221336', + }); + expect(upgradeExecutor).toEqual('0x0EE7AD3Cc291343C9952fFd8844e86d294fa513F'); + }); + + it('should return upgrade executor on xai for xai', async ({ expect }) => { + const xaiClient = createPublicClient({ + chain: xai, + transport: http(), + }); + const upgradeExecutor = await getUpgradeExecutor(xaiClient); + expect(upgradeExecutor).toEqual('0xB30f0939c072255C9a8019B5a52Df9a364861f84'); + }); + + it('should return upgrade executor on parent chain with mocked data', async ({ expect }) => { + const randomAddress = privateKeyToAccount(generatePrivateKey()).address; + const randomAddress2 = privateKeyToAccount(generatePrivateKey()).address; + const randomAddress3 = privateKeyToAccount(generatePrivateKey()).address; + + const mockTransport = () => + createTransport({ + key: 'mock', + name: 'Mock Transport', + request: vi.fn(({ method, params }) => { + return [ + mockAdminChangedEvent(randomAddress3, randomAddress), + mockAdminChangedEvent(randomAddress, randomAddress3), + mockAdminChangedEvent(randomAddress3, randomAddress), + mockAdminChangedEvent(randomAddress, randomAddress2), + ]; + }) as unknown as EIP1193RequestFn, + type: 'mock', + }); + + const mockClient = createPublicClient({ + transport: mockTransport, + chain: arbitrumSepolia, + }); + + const upgradeExecutor = await getUpgradeExecutor(mockClient, { + rollup: rollupAddress, + }); + + expect(upgradeExecutor).toEqual(randomAddress2); + }); + + it('should return upgrade executor on child chain with mocked data', async ({ expect }) => { + const randomAddress = privateKeyToAccount(generatePrivateKey()).address; + const randomAddress2 = privateKeyToAccount(generatePrivateKey()).address; + + const mockClient = createPublicClient({ + transport: http(), + chain: xai, + }); + + // Mock initial getChainOwners + const readContractSpy = vi.spyOn(mockClient, 'readContract'); + readContractSpy + .mockImplementationOnce(async () => [randomAddress]) // getChainOwners + .mockImplementationOnce(async () => true); // hasRole + + const upgradeExecutor = await getUpgradeExecutor(mockClient); + expect(upgradeExecutor).toEqual(randomAddress); + + readContractSpy + .mockImplementationOnce(async () => [randomAddress, randomAddress2]) // second getChainOwners + .mockImplementationOnce(async () => false) + .mockImplementationOnce(async () => true); + const upgradeExecutor2 = await getUpgradeExecutor(mockClient); + expect(upgradeExecutor2).toEqual(randomAddress2); + }); +}); diff --git a/src/testHelpers.ts b/src/testHelpers.ts index eb8c3893..746afb3f 100644 --- a/src/testHelpers.ts +++ b/src/testHelpers.ts @@ -1,4 +1,4 @@ -import { Address, Client, PublicClient, zeroAddress } from 'viem'; +import { Address, PublicClient, defineChain, zeroAddress } from 'viem'; import { privateKeyToAccount, PrivateKeyAccount } from 'viem/accounts'; import { config } from 'dotenv'; import { execSync } from 'node:child_process'; @@ -81,6 +81,7 @@ type TestnodeInformation = { rollup: Address; sequencerInbox: Address; l3SequencerInbox: Address; + upgradeExecutor: Address; l3Bridge: Address; batchPoster: Address; l3BatchPoster: Address; @@ -120,6 +121,7 @@ export function getInformationFromTestnode(): TestnodeInformation { rollup: deploymentJson['rollup'], sequencerInbox: deploymentJson['sequencer-inbox'], batchPoster: sequencerConfig.node['batch-poster']['parent-chain-wallet'].account, + upgradeExecutor: deploymentJson['upgrade-executor'], l3Bridge: l3DeploymentJson['bridge'], l3Rollup: l3DeploymentJson['rollup'], l3SequencerInbox: l3DeploymentJson['sequencer-inbox'], @@ -179,3 +181,19 @@ export async function createRollupHelper({ createRollupInformation, }; } + +export const xai = defineChain({ + id: 660279, + network: 'Xai Mainnet', + name: 'Xai Mainnet', + nativeCurrency: { name: 'Xai', symbol: 'XAI', decimals: 18 }, + rpcUrls: { + default: { + http: ['https://xai-chain.net/rpc'], + }, + public: { + http: ['https://xai-chain.net/rpc'], + }, + }, + testnet: false, +});