diff --git a/examples/setup-fast-withdrawal/.env.example b/examples/setup-fast-withdrawal-eoa/.env.example similarity index 100% rename from examples/setup-fast-withdrawal/.env.example rename to examples/setup-fast-withdrawal-eoa/.env.example diff --git a/examples/setup-fast-withdrawal/README.md b/examples/setup-fast-withdrawal-eoa/README.md similarity index 99% rename from examples/setup-fast-withdrawal/README.md rename to examples/setup-fast-withdrawal-eoa/README.md index d7e8663c..baba9ad5 100644 --- a/examples/setup-fast-withdrawal/README.md +++ b/examples/setup-fast-withdrawal-eoa/README.md @@ -52,3 +52,4 @@ You need to set the following environment variables in an .env file: ```bash yarn dev ``` + diff --git a/examples/setup-fast-withdrawal/index.ts b/examples/setup-fast-withdrawal-eoa/index.ts similarity index 100% rename from examples/setup-fast-withdrawal/index.ts rename to examples/setup-fast-withdrawal-eoa/index.ts diff --git a/examples/setup-fast-withdrawal/package.json b/examples/setup-fast-withdrawal-eoa/package.json similarity index 100% rename from examples/setup-fast-withdrawal/package.json rename to examples/setup-fast-withdrawal-eoa/package.json diff --git a/examples/setup-fast-withdrawal/tsconfig.json b/examples/setup-fast-withdrawal-eoa/tsconfig.json similarity index 100% rename from examples/setup-fast-withdrawal/tsconfig.json rename to examples/setup-fast-withdrawal-eoa/tsconfig.json diff --git a/examples/setup-fast-withdrawal-multisig/.env.example b/examples/setup-fast-withdrawal-multisig/.env.example new file mode 100644 index 00000000..aea0e8b2 --- /dev/null +++ b/examples/setup-fast-withdrawal-multisig/.env.example @@ -0,0 +1,57 @@ +#step 1 +#signer of the Safe multisignature contract that can propose a transaction +OWNER_1_ADDRESS_PRIVATE_KEY= +#parent chain id - 1 etheruem, 42161 arbitrum one +PARENT_CHAIN_ID= +#Safe multisignature contract address +SAFE_ADDRESS= +#stakers and validators that are whitelisted on the Rollup contract +FC_VALIDATORS='["0x1234567890123456789012345678901234567890"]' + +#step 2 +#Rollup contract address +ROLLUP_ADDRESS= +#Safe address that was created on step 1 +FC_VALIDATORS_SAFE_ADDRESS= +#RPC url +RPC= +#signer of the Safe multisignature contract that can propose a transaction +OWNER_1_ADDRESS_PRIVATE_KEY= +#parent chain id - 1 etheruem, 42161 arbitrum one +PARENT_CHAIN_ID= +#Safe multisignature contract address +SAFE_ADDRESS= +#stakers and validators that are whitelisted on the Rollup contract +FC_VALIDATORS='["0x1234567890123456789012345678901234567890"]' + +#step 3 +#Rollup contract address +ROLLUP_ADDRESS= +#add Safe address that was created on step 1 as fast-confirmer (setAnyTrustFastConfirmerPrepareTransactionRequest - fastConfirmer: safeAddress) +FC_VALIDATORS_SAFE_ADDRESS= +#RPC url +RPC= +#signer of the Safe multisignature contract that can propose a transaction +OWNER_1_ADDRESS_PRIVATE_KEY= +#parent chain id - 1 etheruem, 42161 arbitrum one +PARENT_CHAIN_ID= +#Safe multisignature contract address +SAFE_ADDRESS= +#stakers and validators that are whitelisted on the Rollup contract +FC_VALIDATORS='["0x1234567890123456789012345678901234567890"]' + +#step 4 +#Rollup contract address +ROLLUP_ADDRESS= +#Safe address that was created on step 1 +FC_VALIDATORS_SAFE_ADDRESS= +#RPC url +RPC= +#signer of the Safe multisignature contract that can propose a transaction +OWNER_1_ADDRESS_PRIVATE_KEY= +#parent chain id - 1 etheruem, 42161 arbitrum one +PARENT_CHAIN_ID= +#Safe multisignature contract address +SAFE_ADDRESS= +#default is 75 (15minutes) +MINIMUM_ASSERTION_PERIOD=1 diff --git a/examples/setup-fast-withdrawal-multisig/1-create_multisig.ts b/examples/setup-fast-withdrawal-multisig/1-create_multisig.ts new file mode 100644 index 00000000..a30d7334 --- /dev/null +++ b/examples/setup-fast-withdrawal-multisig/1-create_multisig.ts @@ -0,0 +1,79 @@ +import { createPublicClient, http, isAddress } from 'viem'; +import { + createSafePrepareTransactionRequest, +} from '@arbitrum/orbit-sdk'; +import { getParentChainFromId } from '@arbitrum/orbit-sdk/utils'; +import { config } from 'dotenv'; +import { propose } from './common.js'; + +config(); + +//check environment variables +if (typeof process.env.OWNER_1_ADDRESS_PRIVATE_KEY === 'undefined') { + throw new Error(`Please provide the "OWNER_1_ADDRESS_PRIVATE_KEY" environment variable`); +} + +if (typeof process.env.PARENT_CHAIN_ID === 'undefined') { + throw new Error(`Please provide the "PARENT_CHAIN_ID" environment variable`); +} + +if (typeof process.env.SAFE_ADDRESS === 'undefined') { + throw new Error(`Please provide the "SAFE_ADDRESS" environment variable`); +} + +if (typeof process.env.FC_VALIDATORS === 'undefined') { + throw new Error(`Please provide the "FC_VALIDATORS" environment variable`); +} + +const rollupOwnerSafeAddress = process.env.SAFE_ADDRESS as `0x${string}`; +// // set the parent chain and create a public client for it +const parentChainId = Number(process.env.PARENT_CHAIN_ID); +const parentChain = getParentChainFromId(parentChainId); +const parentChainPublicClient = createPublicClient({ + chain: parentChain, + transport: http(process.env.RPC), +}); +// sanitize validator addresses +const fcValidators = JSON.parse(process.env.FC_VALIDATORS); +const safeWalletThreshold = fcValidators.length; +if (!fcValidators) { + throw new Error(`The "FC_VALIDATORS" environment variable must be a valid array`); +} + +const sanitizedFcValidators = [ + ...new Set( + fcValidators.filter((validator: `0x${string}`) => + isAddress(validator) ? validator : undefined, + ), + ), +]; +if (sanitizedFcValidators.length !== safeWalletThreshold) { + throw new Error( + `Some of the addresses in the "FC_VALIDATORS" environment variable appear to not be valid or duplicated.`, + ); +} + +async function main() { + // + // Step 1. Create Safe multisig + // + console.log( + `Step 1: Create a new ${safeWalletThreshold}/${safeWalletThreshold} Safe wallet with the following addresses as signers:`, + fcValidators, + ); + console.log('---'); + const txRequest = await createSafePrepareTransactionRequest({ + publicClient: parentChainPublicClient, + account: rollupOwnerSafeAddress, + owners: fcValidators, + threshold: safeWalletThreshold, + saltNonce: BigInt(Date.now()) + }); + propose(txRequest.to as string, txRequest.data as string, rollupOwnerSafeAddress); + //execute the transaction + //https://help.safe.global/en/articles/40834-verify-safe-creation + //in the executed transaction find `ProxyCreation` event + //Data proxy :
is what you're looing for +} + +main(); diff --git a/examples/setup-fast-withdrawal-multisig/2-add_validators.ts b/examples/setup-fast-withdrawal-multisig/2-add_validators.ts new file mode 100644 index 00000000..30af8b25 --- /dev/null +++ b/examples/setup-fast-withdrawal-multisig/2-add_validators.ts @@ -0,0 +1,117 @@ +import { createPublicClient, http, isAddress, Address } from 'viem'; +import { + createRollupFetchTransactionHash, + createRollupPrepareTransactionReceipt, + rollupAdminLogicPublicActions, +} from '@arbitrum/orbit-sdk'; +import { getParentChainFromId } from '@arbitrum/orbit-sdk/utils'; +import { config } from 'dotenv'; +import { propose } from './common.js'; + + +config(); + +//check environment variables +if (typeof process.env.OWNER_1_ADDRESS_PRIVATE_KEY === 'undefined') { + throw new Error(`Please provide the "OWNER_1_ADDRESS_PRIVATE_KEY" environment variable`); +} + +if (typeof process.env.PARENT_CHAIN_ID === 'undefined') { + throw new Error(`Please provide the "PARENT_CHAIN_ID" environment variable`); +} + +if (typeof process.env.SAFE_ADDRESS === 'undefined') { + throw new Error(`Please provide the "SAFE_ADDRESS" environment variable`); +} + +if (typeof process.env.FC_VALIDATORS_SAFE_ADDRESS === 'undefined') { + throw new Error(`Please provide the "FC_VALIDATORS_SAFE_ADDRESS" environment variable (run step 1)`); +} + +if (typeof process.env.FC_VALIDATORS === 'undefined') { + throw new Error(`Please provide the "FC_VALIDATORS" environment variable`); +} + +if (typeof process.env.ROLLUP_ADDRESS === 'undefined') { + throw new Error(`Please provide the "ROLLUP_ADDRESS" environment variable`); +} + +if (typeof process.env.RPC === 'undefined') { + throw new Error(`Please provide an "RPC" endpoint with unlimited eth_getLogs range`); +} + +const rollupOwnerSafeAddress = process.env.SAFE_ADDRESS as `0x${string}`; +const safeAddress = process.env.FC_VALIDATORS_SAFE_ADDRESS as `0x${string}`; +const rollupAddress = process.env.ROLLUP_ADDRESS as Address; +// // set the parent chain and create a public client for it +const parentChainId = Number(process.env.PARENT_CHAIN_ID); +const parentChain = getParentChainFromId(parentChainId); +const parentChainPublicClient = createPublicClient({ + chain: parentChain, + transport: http(process.env.RPC), +}).extend( + rollupAdminLogicPublicActions({ + rollup: rollupAddress, + }), +); + +// sanitize validator addresses +const fcValidators = JSON.parse(process.env.FC_VALIDATORS); +const safeWalletThreshold = fcValidators.length; +if (!fcValidators) { + throw new Error(`The "FC_VALIDATORS" environment variable must be a valid array`); +} + +const sanitizedFcValidators = [ + ...new Set( + fcValidators.filter((validator: `0x${string}`) => + isAddress(validator) ? validator : undefined, + ), + ), +]; +if (sanitizedFcValidators.length !== safeWalletThreshold) { + throw new Error( + `Some of the addresses in the "FC_VALIDATORS" environment variable appear to not be valid or duplicated.`, + ); +} + +async function main() { + console.log('Add the new Safe address (created in step 1) as a validator'); + fcValidators.push(safeAddress); + + console.log('Gather necessary data (UpgradeExecutor address)'); + const transactionHash = await createRollupFetchTransactionHash({ + rollup: rollupAddress, + publicClient: parentChainPublicClient, + }); + const transactionReceipt = createRollupPrepareTransactionReceipt( + await parentChainPublicClient.getTransactionReceipt({ hash: transactionHash }), + ); + const coreContracts = transactionReceipt.getCoreContracts(); + const upgradeExecutorAddress = coreContracts.upgradeExecutor; + + // + // Step 2. Add validators to the Orbit chain rollup validator whitelist + // + console.log( + `Step 2: Adding the following validators to the Rollup validator whitelist:`, + fcValidators, + ); + console.log('---'); + + // prepare set validator transaction request + const fcValidatorsStatus = Array(fcValidators.length).fill(true); + const setValidatorTransactionRequest = + await parentChainPublicClient.rollupAdminLogicPrepareTransactionRequest({ + functionName: 'setValidator', + args: [ + fcValidators, // validator address list + fcValidatorsStatus, // validator status list + ], + upgradeExecutor: upgradeExecutorAddress, + account: rollupOwnerSafeAddress, + }); + propose(setValidatorTransactionRequest.to as string, setValidatorTransactionRequest.data as string, rollupOwnerSafeAddress); +} + +main(); diff --git a/examples/setup-fast-withdrawal-multisig/3-set-any-trust-fast-confirmer.ts b/examples/setup-fast-withdrawal-multisig/3-set-any-trust-fast-confirmer.ts new file mode 100644 index 00000000..63ebb297 --- /dev/null +++ b/examples/setup-fast-withdrawal-multisig/3-set-any-trust-fast-confirmer.ts @@ -0,0 +1,90 @@ +import { createPublicClient, http, Address, parseAbi } from 'viem'; +import { + createRollupFetchTransactionHash, + createRollupPrepareTransactionReceipt, + rollupAdminLogicPublicActions, + setAnyTrustFastConfirmerPrepareTransactionRequest, +} from '@arbitrum/orbit-sdk'; +import { getParentChainFromId } from '@arbitrum/orbit-sdk/utils'; +import { config } from 'dotenv'; +import { propose } from './common.js'; + + +config(); + +//check environment variables +if (typeof process.env.OWNER_1_ADDRESS_PRIVATE_KEY === 'undefined') { + throw new Error(`Please provide the "OWNER_1_ADDRESS_PRIVATE_KEY" environment variable`); +} + +if (typeof process.env.PARENT_CHAIN_ID === 'undefined') { + throw new Error(`Please provide the "PARENT_CHAIN_ID" environment variable`); +} + +if (typeof process.env.SAFE_ADDRESS === 'undefined') { + throw new Error(`Please provide the "SAFE_ADDRESS" environment variable`); +} + +if (typeof process.env.FC_VALIDATORS_SAFE_ADDRESS === 'undefined') { + throw new Error(`Please provide the "FC_VALIDATORS_SAFE_ADDRESS" environment variable (run step 1)`); +} + +if (typeof process.env.ROLLUP_ADDRESS === 'undefined') { + throw new Error(`Please provide the "ROLLUP_ADDRESS" environment variable`); +} + +if (typeof process.env.RPC === 'undefined') { + throw new Error(`Please provide an "RPC" endpoint with unlimited eth_getLogs range`); +} + +const rollupOwnerSafeAddress = process.env.SAFE_ADDRESS as `0x${string}`; +const safeAddress = process.env.FC_VALIDATORS_SAFE_ADDRESS as `0x${string}`; +const rollupAddress = process.env.ROLLUP_ADDRESS as Address; +// // set the parent chain and create a public client for it +const parentChainId = Number(process.env.PARENT_CHAIN_ID); +const parentChain = getParentChainFromId(parentChainId); +const parentChainPublicClient = createPublicClient({ + chain: parentChain, + transport: http(process.env.RPC), +}).extend( + rollupAdminLogicPublicActions({ + rollup: rollupAddress, + }), +); + +async function main() { + const currentAnyTrustFastConfirmer = await parentChainPublicClient.readContract({ + address: rollupAddress, + abi: parseAbi(['function anyTrustFastConfirmer() view returns (address)']), + functionName: 'anyTrustFastConfirmer', + }); + + if (currentAnyTrustFastConfirmer.toLowerCase() !== safeAddress.toLowerCase()) { + console.log('Gather necessary data (UpgradeExecutor address)'); + const transactionHash = await createRollupFetchTransactionHash({ + rollup: rollupAddress, + publicClient: parentChainPublicClient, + }); + const transactionReceipt = createRollupPrepareTransactionReceipt( + await parentChainPublicClient.getTransactionReceipt({ hash: transactionHash }), + ); + const coreContracts = transactionReceipt.getCoreContracts(); + const upgradeExecutorAddress = coreContracts.upgradeExecutor; + const setAnyTrustFastConfirmerTransactionRequest = + await setAnyTrustFastConfirmerPrepareTransactionRequest({ + publicClient: parentChainPublicClient, + account: rollupOwnerSafeAddress, + rollup: rollupAddress, + upgradeExecutor: upgradeExecutorAddress, + fastConfirmer: safeAddress, + }); + propose(setAnyTrustFastConfirmerTransactionRequest.to as string, setAnyTrustFastConfirmerTransactionRequest.data as string, rollupOwnerSafeAddress); + + } else { + console.log( + `AnyTrust fast confirmer is already configured to ${currentAnyTrustFastConfirmer}. Skipping.`, + ); + } +} + +main(); diff --git a/examples/setup-fast-withdrawal-multisig/4-configure-minimum-assertion-period.ts b/examples/setup-fast-withdrawal-multisig/4-configure-minimum-assertion-period.ts new file mode 100644 index 00000000..1514e17c --- /dev/null +++ b/examples/setup-fast-withdrawal-multisig/4-configure-minimum-assertion-period.ts @@ -0,0 +1,148 @@ +import { createPublicClient, http, Address } from 'viem'; +import { + createRollupFetchTransactionHash, + createRollupPrepareTransactionReceipt, + rollupAdminLogicPublicActions, +} from '@arbitrum/orbit-sdk'; +import { base, baseSepolia } from '@arbitrum/orbit-sdk/chains'; +import { getParentChainFromId } from '@arbitrum/orbit-sdk/utils'; +import { config } from 'dotenv'; +import { propose } from './common.js'; + + +config(); + +function getTimeDelayFromNumberOfBlocks(chainId: number, blocks: bigint): string { + // For Arbitrum L2s built on top of Ethereum, or Arbitrum L3s built on top of an Arbitrum L2, `block.number` always returns the L1 block number. + // L1 blocks are produced every 12 seconds. + // + // For Arbitrum L3s built on top of an OP Stack L2, `block.number` will return the L2 block number. + // L2 blocks in OP Stack chains are produced every 2 seconds. + const seconds = Number( + chainId === base.id || chainId === baseSepolia.id ? blocks * 2n : blocks * 12n, + ); + + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${h}h${m}m${s}s`; +} + + +//check environment variables +if (typeof process.env.OWNER_1_ADDRESS_PRIVATE_KEY === 'undefined') { + throw new Error(`Please provide the "OWNER_1_ADDRESS_PRIVATE_KEY" environment variable`); +} + +if (typeof process.env.PARENT_CHAIN_ID === 'undefined') { + throw new Error(`Please provide the "PARENT_CHAIN_ID" environment variable`); +} + +if (typeof process.env.SAFE_ADDRESS === 'undefined') { + throw new Error(`Please provide the "SAFE_ADDRESS" environment variable`); +} + +if (typeof process.env.ROLLUP_ADDRESS === 'undefined') { + throw new Error(`Please provide the "ROLLUP_ADDRESS" environment variable`); +} + +if (typeof process.env.RPC === 'undefined') { + throw new Error(`Please provide an "RPC" endpoint with unlimited eth_getLogs range`); +} + +if (typeof process.env.MINIMUM_ASSERTION_PERIOD === 'undefined') { + console.log('MinimumAssertionPeriod set to 75') +} + +if (typeof process.env.FC_VALIDATORS_SAFE_ADDRESS === 'undefined') { + throw new Error(`Please provide the "FC_VALIDATORS_SAFE_ADDRESS" environment variable (run step 1)`); +} + +const minimumAssertionPeriod = BigInt(process.env.MINIMUM_ASSERTION_PERIOD || 75); +const rollupOwnerSafeAddress = process.env.SAFE_ADDRESS as `0x${string}`; +const rollupAddress = process.env.ROLLUP_ADDRESS as Address; +// // set the parent chain and create a public client for it +const parentChainId = Number(process.env.PARENT_CHAIN_ID); +const parentChain = getParentChainFromId(parentChainId); +const parentChainPublicClient = createPublicClient({ + chain: parentChain, + transport: http(process.env.RPC), +}).extend( + rollupAdminLogicPublicActions({ + rollup: rollupAddress, + }), +); + +async function main() { + // + // Step 0. Gather necessary data (UpgradeExecutor address) + // + const transactionHash = await createRollupFetchTransactionHash({ + rollup: rollupAddress, + publicClient: parentChainPublicClient, + }); + const transactionReceipt = createRollupPrepareTransactionReceipt( + await parentChainPublicClient.getTransactionReceipt({ hash: transactionHash }), + ); + const coreContracts = transactionReceipt.getCoreContracts(); + const upgradeExecutorAddress = coreContracts.upgradeExecutor; + + console.log( + `Step 4: Configure the minimumAssertionPeriod in the Rollup contract to: ${Number( + minimumAssertionPeriod, + )}`, + ); + console.log('---'); + + // get current minimumAssertionPeriod + const currentMinimumAssertionPeriod = await parentChainPublicClient.rollupAdminLogicReadContract({ + functionName: 'minimumAssertionPeriod', + }); + + if (currentMinimumAssertionPeriod !== minimumAssertionPeriod) { + // prepare setMinimumAssertionPeriod transaction request + const setMinimumAssertionPeriodTransactionRequest = + await parentChainPublicClient.rollupAdminLogicPrepareTransactionRequest({ + functionName: 'setMinimumAssertionPeriod', + args: [minimumAssertionPeriod], + upgradeExecutor: upgradeExecutorAddress, + account: rollupOwnerSafeAddress, + }); + propose(setMinimumAssertionPeriodTransactionRequest.to as string, setMinimumAssertionPeriodTransactionRequest.data as string, rollupOwnerSafeAddress); + console.log('Transaction proposed'); + } else { + console.log( + `Minimum assertion period is already configured to ${currentMinimumAssertionPeriod}. Skipping.`, + ); + } + console.log(''); + + // + // Step 5. Show a confirmation message with the next steps + // + console.log(`Step 5: Configure your batch poster and your validator nodes`); + console.log(`---`); + console.log( + 'Your chain is now configured with a fast-withdrawal committee. The following instructions show how to configure your batch poster and your validators to start using this feature.', + ); + console.log(''); + + // Batch poster configuration + const timeDelay = getTimeDelayFromNumberOfBlocks(parentChain.id, minimumAssertionPeriod); + console.log('Your batch poster has to run at least nitro v3.1.2'); + console.log('Add the following parameter:'); + console.log(`--node.batch-poster.max-delay=${timeDelay}`); + console.log(''); + + // Validator configuration + console.log('Your validators have to run at least nitro v3.1.2'); + console.log('Add the following parameters:'); + console.log(`--node.staker.enable-fast-confirmation=true`); + console.log(`--node.staker.make-assertion-interval=${timeDelay}`); + console.log(''); + + // Final recommendation + console.log('Finally, restart your batch poster and validator nodes.'); +} + +main(); diff --git a/examples/setup-fast-withdrawal-multisig/README.md b/examples/setup-fast-withdrawal-multisig/README.md new file mode 100644 index 00000000..87a001f7 --- /dev/null +++ b/examples/setup-fast-withdrawal-multisig/README.md @@ -0,0 +1,26 @@ + + +## Multisig ownership +Beware: At least one of the signers needs to be an EOA account so that it can propose transactions through this script. +If you want to use this script then you need to make sure the owner of the Rollup has been transfered to a Multisig contract and it has execution rights on L1 Executor Contract. + +1. Build this example: yarn dev +2. This step will create a new Safe on the parent chain and add fast confirmation validators as owners. +``` +node ./dist/1-create_multisig.js +``` +3. The validators list is expanded with the Safe created with step 1 (1-create_multisig). That's why you need to provide FC_VALIDATORS_SAFE_ADDRESS. +``` +node ./dist/2-add_validators.js +``` + +4. We also add Safe as `fast confirmer`. +``` +node ./dist/3-set-any-trust-fast-confirmer.js +``` + + +5. Configure minimum assertion period. The default is 75 (~15 minutes) +``` +node ./dist/4-configure-minimum-assertion-period.js +``` diff --git a/examples/setup-fast-withdrawal-multisig/common.ts b/examples/setup-fast-withdrawal-multisig/common.ts new file mode 100644 index 00000000..d5ee67a7 --- /dev/null +++ b/examples/setup-fast-withdrawal-multisig/common.ts @@ -0,0 +1,53 @@ +import { createPublicClient, http } from 'viem'; +import { getParentChainFromId, sanitizePrivateKey } from '@arbitrum/orbit-sdk/utils'; +import SafeApiKit from '@safe-global/api-kit' +import Safe from '@safe-global/protocol-kit' +import { privateKeyToAccount } from 'viem/accounts'; +import { + MetaTransactionData, + OperationType +} from '@safe-global/safe-core-sdk-types' + +export async function propose(to: string, data: string, rollupOwnerSafeAddress: string): Promise