Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add getUpgradeExecutor function #140

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions src/__snapshots__/getUpgradeExecutor.integration.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`successfully get upgrade executor > from child chain 1`] = `"0x24198F8A339cd3C47AEa3A764A20d2dDaB4D1b5b"`;
32 changes: 32 additions & 0 deletions src/getUpgradeExecutor.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
102 changes: 102 additions & 0 deletions src/getUpgradeExecutor.ts
Original file line number Diff line number Diff line change
@@ -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<TChain extends Chain | undefined>(
fionnachan marked this conversation as resolved.
Show resolved Hide resolved
publicClient: PublicClient<Transport, TChain>,
params?: GetUpgradeExecutorParams,
): Promise<GetUpgradeExecutorReturnType> {
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];
}
105 changes: 105 additions & 0 deletions src/getUpgradeExecutor.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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',
fionnachan marked this conversation as resolved.
Show resolved Hide resolved
};
}

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);
});
});
20 changes: 19 additions & 1 deletion src/testHelpers.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -81,6 +81,7 @@ type TestnodeInformation = {
rollup: Address;
sequencerInbox: Address;
l3SequencerInbox: Address;
upgradeExecutor: Address;
l3Bridge: Address;
batchPoster: Address;
l3BatchPoster: Address;
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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,
});
Loading