From ba4d97060fca871810bca5ec4013ca0b15086e1a Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 6 Aug 2024 11:13:24 -0600 Subject: [PATCH 1/6] make script to check for finalizations Signed-off-by: bennett --- deploy/consts.ts | 1 + scripts/sendTokensToHubPool.ts | 177 +++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 scripts/sendTokensToHubPool.ts diff --git a/deploy/consts.ts b/deploy/consts.ts index 5e2937dd1..8d98a4485 100644 --- a/deploy/consts.ts +++ b/deploy/consts.ts @@ -1,6 +1,7 @@ import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../utils"; export const WETH = TOKEN_SYMBOLS_MAP.WETH.addresses; +export const MOCK_ADMIN = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string } } = { 1: { diff --git a/scripts/sendTokensToHubPool.ts b/scripts/sendTokensToHubPool.ts new file mode 100644 index 000000000..228613125 --- /dev/null +++ b/scripts/sendTokensToHubPool.ts @@ -0,0 +1,177 @@ +import { getContractFactory, ethers, SignerWithAddress } from "../utils/utils"; +import { hre } from "../utils/utils.hre"; +import { L1_ADDRESS_MAP, L2_ADDRESS_MAP } from "../deploy/consts"; +import { getNodeUrl, EMPTY_MERKLE_ROOT } from "@uma/common"; +import { Event, Wallet, providers, Contract } from "ethers"; + +/** + * Script to claim L1->L2 or L2->L1 messages. Run via + * ``` + * yarn hardhat run ./scripts/sendTokensToHubPool.ts \ + * --network base \ + * ``` + * This REQUIRES a spoke pool to be deployed to the specified network AND for the + * spoke pool to have the signer as the `crossDomainAdmin`. + * Flags: + * - `--network`: The L2 network, which is defined in hardhat.config.ts. + */ + +async function main() { + const rootBundleId = process.env.TEST_ROOT_BUNDLE_ID ?? 0; + const spokeAddress = process.env.TEST_SPOKE_POOL_ADDRESS; + if (!spokeAddress) { + throw new Error("No spoke pool address specified. Please set TEST_SPOKE_POOL_ADDRESS to the target L2 spoke pool"); + } + const tokenAddress = process.env.TEST_TOKEN_ADDRESS; + if (!tokenAddress) { + throw new Error( + "No token address specified. Please set TEST_TOKEN_ADDRESS to the l2 token address to send back to the hub pool" + ); + } + const amountToReturn = process.env.AMOUNT_TO_RETURN; + if (!amountToReturn) { + throw new Error( + "No AMOUNT_TO_RETURN in env. Please set AMOUNT_TO_RETURN to the amount of the l2 token to send back to the hub pool" + ); + } + + const l2ChainId = parseInt(await hre.getChainId()); + const providerUrl = + process.env[`NODE_URL_${l2ChainId}`] ?? `https://base-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`; + const l2Provider = new ethers.providers.JsonRpcProvider(providerUrl); + console.log(l2Provider); + const l2Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l2Provider); + const spokePool = new Contract(spokeAddress, ABI, l2Signer); + + const rootBundleType = "tuple(uint256,uint256,uint256[],uint32,address,address[])"; + // Construct the root bundle + const encodedRootBundle = ethers.utils.defaultAbiCoder.encode( + [rootBundleType], + [[amountToReturn, l2ChainId, [], 0, tokenAddress, []]] + ); + const rootBundleHash = ethers.utils.keccak256(encodedRootBundle); + // Submit the root bundle to chain. + const relayRootBundle = await spokePool.relayRootBundle(rootBundleHash, EMPTY_MERKLE_ROOT); + console.log(`Sent relayer root ${rootBundleHash} to spoke pool with transaction hash ${relayRootBundle}`); + // Execute the refund leaf. + const executeRelayerRefundLeaf = await spokePool.executeRelayerRefundLeaf( + rootBundleId, + [amountToReturn, l2ChainId, [], 0, tokenAddress, []], + [] + ); + console.log(`Executed root bundle with transaction hash ${executeRelayerRefundLeaf}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); + +// We only need to call two functions in this script: one to set a root bundle, and one to execute that set root bundle. +// This ABI should be consistent for all spoke pool implementations. +const ABI: unknown[] = `[ + { + "inputs": [ + { + "internalType": "uint32", + "name": "rootBundleId", + "type": "uint32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "amountToReturn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "refundAmounts", + "type": "uint256[]" + }, + { + "internalType": "uint32", + "name": "leafId", + "type": "uint32" + }, + { + "internalType": "address", + "name": "l2TokenAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "refundAddresses", + "type": "address[]" + } + ], + "internalType": "struct SpokePoolInterface.RelayerRefundLeaf", + "name": "relayerRefundLeaf", + "type": "tuple" + }, + { + "internalType": "bytes32[]", + "name": "proof", + "type": "bytes32[]" + } + ], + "name": "executeRelayerRefundLeaf", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "relayerRefundRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "slowRelayRoot", + "type": "bytes32" + } + ], + "name": "relayRootBundle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "rootBundles", + "outputs": [ + { + "internalType": "bytes32", + "name": "slowRelayRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "relayerRefundRoot", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + } +]`; From 56b8a3229d1726188e9bd2163c6bd0c9562171b9 Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 6 Aug 2024 11:27:57 -0600 Subject: [PATCH 2/6] remove incorrect type assignment Signed-off-by: bennett --- scripts/sendTokensToHubPool.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/sendTokensToHubPool.ts b/scripts/sendTokensToHubPool.ts index 228613125..170c85eaf 100644 --- a/scripts/sendTokensToHubPool.ts +++ b/scripts/sendTokensToHubPool.ts @@ -39,7 +39,6 @@ async function main() { const providerUrl = process.env[`NODE_URL_${l2ChainId}`] ?? `https://base-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`; const l2Provider = new ethers.providers.JsonRpcProvider(providerUrl); - console.log(l2Provider); const l2Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l2Provider); const spokePool = new Contract(spokeAddress, ABI, l2Signer); @@ -69,7 +68,7 @@ main().catch((error) => { // We only need to call two functions in this script: one to set a root bundle, and one to execute that set root bundle. // This ABI should be consistent for all spoke pool implementations. -const ABI: unknown[] = `[ +const ABI = `[ { "inputs": [ { From be16dedfadb108b90b46af865bc5d9836904ea60 Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 6 Aug 2024 15:45:53 -0600 Subject: [PATCH 3/6] add scaffolding for script. Still many TODOs Signed-off-by: bennett --- scripts/sendTokensToHubPool.ts | 133 +++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 21 deletions(-) diff --git a/scripts/sendTokensToHubPool.ts b/scripts/sendTokensToHubPool.ts index 170c85eaf..4c096ea04 100644 --- a/scripts/sendTokensToHubPool.ts +++ b/scripts/sendTokensToHubPool.ts @@ -1,5 +1,6 @@ -import { getContractFactory, ethers, SignerWithAddress } from "../utils/utils"; +import { getContractFactory, ethers, SignerWithAddress, TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "../utils/utils"; import { hre } from "../utils/utils.hre"; +import { getDeployedAddress } from "../src"; import { L1_ADDRESS_MAP, L2_ADDRESS_MAP } from "../deploy/consts"; import { getNodeUrl, EMPTY_MERKLE_ROOT } from "@uma/common"; import { Event, Wallet, providers, Contract } from "ethers"; @@ -17,31 +18,56 @@ import { Event, Wallet, providers, Contract } from "ethers"; */ async function main() { + /* + * Setup: Need to obtain all contract addresses involved and instantiate L1/L2 providers and contracts. + */ + + // Instantiate providers/signers/chainIds + const l2ChainId = parseInt(await hre.getChainId()); + const l1ChainId = parseInt(await hre.companionNetworks.l1.getChainId()); + + // TODO: Figure out how to get this from hardhat.config.ts + const l2ProviderUrl = + process.env[`NODE_URL_${l2ChainId}`] ?? `https://base-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`; + const l1ProviderUrl = + process.env[`NODE_URL_${l1ChainId}`] ?? `https://base-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`; + + const l2Provider = new ethers.providers.JsonRpcProvider(l2ProviderUrl); + const l2Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l2Provider); + const l1Provider = new ethers.providers.JsonRpcProvider(l1ProviderUrl); + const l1Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l1Provider); + + // Process environment first so we can throw errors immediately if we are missing any configuration. + // TODO: I need to figure out a way to get this dynamically. const rootBundleId = process.env.TEST_ROOT_BUNDLE_ID ?? 0; - const spokeAddress = process.env.TEST_SPOKE_POOL_ADDRESS; + const spokeAddress = process.env.TEST_SPOKE_POOL_ADDRESS; // We need to specify this since this is an unsaved deployment. + // TODO: It would be nice to fetch these dynamically based off of the chain ids. if (!spokeAddress) { - throw new Error("No spoke pool address specified. Please set TEST_SPOKE_POOL_ADDRESS to the target L2 spoke pool"); - } - const tokenAddress = process.env.TEST_TOKEN_ADDRESS; - if (!tokenAddress) { throw new Error( - "No token address specified. Please set TEST_TOKEN_ADDRESS to the l2 token address to send back to the hub pool" + "[-] No spoke pool address specified. Please set TEST_SPOKE_POOL_ADDRESS to the target L2 spoke pool address" ); } - const amountToReturn = process.env.AMOUNT_TO_RETURN; - if (!amountToReturn) { + const adapterAddress = process.env.ADAPTER_ADDRESS; + if (!adapterAddress) { + throw new Error("[-] No adapter address specified. Please set ADAPTER_ADDRESS to the Across L1 adapter address"); + } + const tokenAddress = process.env.TEST_TOKEN_ADDRESS ?? TOKEN_SYMBOLS_MAP.WETH.addresses[l2ChainId]; + if (!tokenAddress) { throw new Error( - "No AMOUNT_TO_RETURN in env. Please set AMOUNT_TO_RETURN to the amount of the l2 token to send back to the hub pool" + "[-] No token address specified and cannot default to WETH. Please set TEST_TOKEN_ADDRESS to the l2 token address to send back to the hub pool" ); } + const amountToReturn = process.env.AMOUNT_TO_RETURN ?? 1000; // Default to dust. - const l2ChainId = parseInt(await hre.getChainId()); - const providerUrl = - process.env[`NODE_URL_${l2ChainId}`] ?? `https://base-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`; - const l2Provider = new ethers.providers.JsonRpcProvider(providerUrl); - const l2Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l2Provider); - const spokePool = new Contract(spokeAddress, ABI, l2Signer); + // Construct the contracts + const spokePool = new Contract(spokeAddress, spokePoolAbi, l2Signer); + const adapter = new Contract(adapterAddress, adapterAbi, l1Signer); + console.log("[+] Successfully constructed all contracts. Beginning L1 message relay."); + /* + * Step 1: Craft and send a message to be sent to the provided L1 chain adapter contract. This message should be used to call `relayRootBundle` on the + * associated L2 contract + */ const rootBundleType = "tuple(uint256,uint256,uint256[],uint32,address,address[])"; // Construct the root bundle const encodedRootBundle = ethers.utils.defaultAbiCoder.encode( @@ -50,15 +76,50 @@ async function main() { ); const rootBundleHash = ethers.utils.keccak256(encodedRootBundle); // Submit the root bundle to chain. - const relayRootBundle = await spokePool.relayRootBundle(rootBundleHash, EMPTY_MERKLE_ROOT); - console.log(`Sent relayer root ${rootBundleHash} to spoke pool with transaction hash ${relayRootBundle}`); - // Execute the refund leaf. + const relayRootBundleTxnData = spokePool.interface.encodeFunctionData("relayRootBundle", [ + rootBundleHash, + EMPTY_MERKLE_ROOT, + ]); + const adapterTxn = await adapter.relayMessage(spokePool.address, relayRootBundleTxnData); + const hash = await adapterTxn; + // TODO: Specify adapter name + console.log( + `[+] Called L1 adapter to relay refund leaf message to mock spoke pool at ${ + spokePool.address + }. Txn: ${JSON.stringify(hash)}` + ); + /* + * Step 2: Spin until we observe the message to be executed on the L2. This should take ~3 minutes. + */ + const threeMins = 1000 * 60 * 3; + await delay(threeMins); + + // We should be able to query the canonical messenger to see if our L1 message was propagated, but this requires us to instantiate a unique L2 messenger contract + // for each new chain we make, which is not scalable. Instead, we query whether our root bundle is in the spoke pool contract, as this is generalizable and does not + // require us to instantiate any new contract. + while (1) { + try { + // Check the root bundle + await spokePool.rootBundles(rootBundleId); + break; + } catch (e) { + // No root bundle made it yet. Continue to spin + console.log("[-] Root bundle not found on L2. Waiting another 30 seconds."); + } + } + console.log("[+] Root bundle observed on L2 spoke pool. Attempting to execute."); + /* + * Step 3: Call `executeRelayerRefund` on the target spoke pool to send funds back to the hub pool (or, whatever was initialized as the `hubPool` in the deploy + * script, which is likely the dev EOA). + */ const executeRelayerRefundLeaf = await spokePool.executeRelayerRefundLeaf( rootBundleId, [amountToReturn, l2ChainId, [], 0, tokenAddress, []], [] ); - console.log(`Executed root bundle with transaction hash ${executeRelayerRefundLeaf}`); + console.log( + `[+] Executed root bundle with transaction hash ${executeRelayerRefundLeaf}. You can now test the finalizer in the relayer repository.` + ); } main().catch((error) => { @@ -66,9 +127,17 @@ main().catch((error) => { process.exitCode = 1; }); +// Sleep +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +///////////////////// +/// Contract ABIs /// +///////////////////// // We only need to call two functions in this script: one to set a root bundle, and one to execute that set root bundle. // This ABI should be consistent for all spoke pool implementations. -const ABI = `[ +const spokePoolAbi = `[ { "inputs": [ { @@ -174,3 +243,25 @@ const ABI = `[ "type": "function" } ]`; + +// Only one function needs to be called in the adapter. +const adapterAbi = `[ + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "relayMessage", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +]`; From 89c554627fb90d406aa24cc4abe382e75d40a59c Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 6 Aug 2024 15:53:15 -0600 Subject: [PATCH 4/6] lint Signed-off-by: bennett --- scripts/sendTokensToHubPool.ts | 247 ++++++++++++++++----------------- 1 file changed, 120 insertions(+), 127 deletions(-) diff --git a/scripts/sendTokensToHubPool.ts b/scripts/sendTokensToHubPool.ts index 4c096ea04..631de61a8 100644 --- a/scripts/sendTokensToHubPool.ts +++ b/scripts/sendTokensToHubPool.ts @@ -1,6 +1,6 @@ -import { getContractFactory, ethers, SignerWithAddress, TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "../utils/utils"; +import { getContractFactory, ethers, SignerWithAddress } from "../utils/utils"; +import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants"; import { hre } from "../utils/utils.hre"; -import { getDeployedAddress } from "../src"; import { L1_ADDRESS_MAP, L2_ADDRESS_MAP } from "../deploy/consts"; import { getNodeUrl, EMPTY_MERKLE_ROOT } from "@uma/common"; import { Event, Wallet, providers, Contract } from "ethers"; @@ -137,131 +137,124 @@ function delay(ms: number) { ///////////////////// // We only need to call two functions in this script: one to set a root bundle, and one to execute that set root bundle. // This ABI should be consistent for all spoke pool implementations. -const spokePoolAbi = `[ - { - "inputs": [ - { - "internalType": "uint32", - "name": "rootBundleId", - "type": "uint32" - }, - { - "components": [ - { - "internalType": "uint256", - "name": "amountToReturn", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "chainId", - "type": "uint256" - }, - { - "internalType": "uint256[]", - "name": "refundAmounts", - "type": "uint256[]" - }, - { - "internalType": "uint32", - "name": "leafId", - "type": "uint32" - }, - { - "internalType": "address", - "name": "l2TokenAddress", - "type": "address" - }, - { - "internalType": "address[]", - "name": "refundAddresses", - "type": "address[]" - } - ], - "internalType": "struct SpokePoolInterface.RelayerRefundLeaf", - "name": "relayerRefundLeaf", - "type": "tuple" - }, - { - "internalType": "bytes32[]", - "name": "proof", - "type": "bytes32[]" - } +const spokePoolAbi = [ + { + inputs: [ + { + internalType: "uint32", + name: "rootBundleId", + type: "uint32", + }, + { + components: [ + { + internalType: "uint256", + name: "amountToReturn", + type: "uint256", + }, + { + internalType: "uint256", + name: "chainId", + type: "uint256", + }, + { + internalType: "uint256[]", + name: "refundAmounts", + type: "uint256[]", + }, + { + internalType: "uint32", + name: "leafId", + type: "uint32", + }, + { + internalType: "address", + name: "l2TokenAddress", + type: "address", + }, + { + internalType: "address[]", + name: "refundAddresses", + type: "address[]", + }, ], - "name": "executeRelayerRefundLeaf", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "relayerRefundRoot", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "slowRelayRoot", - "type": "bytes32" - } - ], - "name": "relayRootBundle", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "rootBundles", - "outputs": [ - { - "internalType": "bytes32", - "name": "slowRelayRoot", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "relayerRefundRoot", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - } -]`; + internalType: "struct SpokePoolInterface.RelayerRefundLeaf", + name: "relayerRefundLeaf", + type: "tuple", + }, + { + internalType: "bytes32[]", + name: "proof", + type: "bytes32[]", + }, + ], + name: "executeRelayerRefundLeaf", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "relayerRefundRoot", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "slowRelayRoot", + type: "bytes32", + }, + ], + name: "relayRootBundle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "rootBundles", + outputs: [ + { + internalType: "bytes32", + name: "slowRelayRoot", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "relayerRefundRoot", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, +]; // Only one function needs to be called in the adapter. -const adapterAbi = `[ - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "bytes", - "name": "message", - "type": "bytes" - } - ], - "name": "relayMessage", - "outputs": [], - "stateMutability": "payable", - "type": "function" - } -]`; +const adapterAbi = [ + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + name: "relayMessage", + outputs: [], + stateMutability: "payable", + type: "function", + }, +]; From 4bb1153dfd476153bc3f3f83c11bd8bb4d339ead Mon Sep 17 00:00:00 2001 From: bennett Date: Wed, 7 Aug 2024 12:17:55 -0600 Subject: [PATCH 5/6] update script Signed-off-by: bennett --- scripts/sendTokensToHubPool.ts | 41 +++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/scripts/sendTokensToHubPool.ts b/scripts/sendTokensToHubPool.ts index 631de61a8..930583228 100644 --- a/scripts/sendTokensToHubPool.ts +++ b/scripts/sendTokensToHubPool.ts @@ -27,10 +27,8 @@ async function main() { const l1ChainId = parseInt(await hre.companionNetworks.l1.getChainId()); // TODO: Figure out how to get this from hardhat.config.ts - const l2ProviderUrl = - process.env[`NODE_URL_${l2ChainId}`] ?? `https://base-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`; - const l1ProviderUrl = - process.env[`NODE_URL_${l1ChainId}`] ?? `https://base-sepolia.infura.io/v3/${process.env.INFURA_API_KEY}`; + const l2ProviderUrl = process.env[`NODE_URL_${l2ChainId}`]; + const l1ProviderUrl = process.env[`NODE_URL_${l1ChainId}`]; const l2Provider = new ethers.providers.JsonRpcProvider(l2ProviderUrl); const l2Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l2Provider); @@ -38,15 +36,13 @@ async function main() { const l1Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l1Provider); // Process environment first so we can throw errors immediately if we are missing any configuration. - // TODO: I need to figure out a way to get this dynamically. - const rootBundleId = process.env.TEST_ROOT_BUNDLE_ID ?? 0; const spokeAddress = process.env.TEST_SPOKE_POOL_ADDRESS; // We need to specify this since this is an unsaved deployment. - // TODO: It would be nice to fetch these dynamically based off of the chain ids. if (!spokeAddress) { throw new Error( "[-] No spoke pool address specified. Please set TEST_SPOKE_POOL_ADDRESS to the target L2 spoke pool address" ); } + // TODO: It would be nice to fetch these dynamically based off of the chain ids. const adapterAddress = process.env.ADAPTER_ADDRESS; if (!adapterAddress) { throw new Error("[-] No adapter address specified. Please set ADAPTER_ADDRESS to the Across L1 adapter address"); @@ -63,7 +59,16 @@ async function main() { const spokePool = new Contract(spokeAddress, spokePoolAbi, l2Signer); const adapter = new Contract(adapterAddress, adapterAbi, l1Signer); - console.log("[+] Successfully constructed all contracts. Beginning L1 message relay."); + console.log("[+] Successfully constructed all contracts. Determining root bundle Id to use."); + let rootBundleId = 0; + try { + while (1) { + await spokePool.rootBundles(rootBundleId); + rootBundleId++; + } + } catch (e) { + console.log(`[+] Obtained latest root bundle Id ${rootBundleId}`); + } /* * Step 1: Craft and send a message to be sent to the provided L1 chain adapter contract. This message should be used to call `relayRootBundle` on the * associated L2 contract @@ -81,18 +86,20 @@ async function main() { EMPTY_MERKLE_ROOT, ]); const adapterTxn = await adapter.relayMessage(spokePool.address, relayRootBundleTxnData); - const hash = await adapterTxn; + const txn = await adapterTxn.wait(); // TODO: Specify adapter name console.log( - `[+] Called L1 adapter to relay refund leaf message to mock spoke pool at ${ - spokePool.address - }. Txn: ${JSON.stringify(hash)}` + `[+] Called L1 adapter (${adapter.address}) to relay refund leaf message to mock spoke pool at ${spokePool.address}. Txn: ${txn.transactionHash}` ); /* * Step 2: Spin until we observe the message to be executed on the L2. This should take ~3 minutes. */ - const threeMins = 1000 * 60 * 3; - await delay(threeMins); + // It would be nice to just have a websocket listening for the `RelayedRootBundle` event, but I don't want to assume a websocket provider. + console.log( + "[i] Optimistically waiting 5 minutes for L1 message to propagate. If root bundle is not observed, will check spoke every minute thereafter." + ); + const fiveMins = 1000 * 60 * 5; + await delay(fiveMins); // We should be able to query the canonical messenger to see if our L1 message was propagated, but this requires us to instantiate a unique L2 messenger contract // for each new chain we make, which is not scalable. Instead, we query whether our root bundle is in the spoke pool contract, as this is generalizable and does not @@ -104,7 +111,8 @@ async function main() { break; } catch (e) { // No root bundle made it yet. Continue to spin - console.log("[-] Root bundle not found on L2. Waiting another 30 seconds."); + console.log("[-] Root bundle not found on L2. Waiting another 60 seconds."); + await delay(1000 * 60); } } console.log("[+] Root bundle observed on L2 spoke pool. Attempting to execute."); @@ -117,8 +125,9 @@ async function main() { [amountToReturn, l2ChainId, [], 0, tokenAddress, []], [] ); + const l2Txn = await executeRelayerRefundLeaf.wait(); console.log( - `[+] Executed root bundle with transaction hash ${executeRelayerRefundLeaf}. You can now test the finalizer in the relayer repository.` + `[+] Executed root bundle with transaction hash ${l2Txn.transactionHash}. You can now test the finalizer in the relayer repository.` ); } From 0add851a893e2176555b5baae75a8a1bb115691c Mon Sep 17 00:00:00 2001 From: bennett Date: Tue, 20 Aug 2024 15:37:12 -0500 Subject: [PATCH 6/6] switch to a hardhat task Signed-off-by: bennett --- hardhat.config.ts | 2 + scripts/sendTokensToHubPool.ts | 269 --------------------------------- tasks/tokenTraversal.ts | 132 ++++++++++++++++ tasks/utils.ts | 118 +++++++++++++++ 4 files changed, 252 insertions(+), 269 deletions(-) delete mode 100644 scripts/sendTokensToHubPool.ts create mode 100644 tasks/tokenTraversal.ts diff --git a/hardhat.config.ts b/hardhat.config.ts index ba64baab8..a76416fb2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -22,6 +22,8 @@ require("./tasks/enableL1TokenAcrossEcosystem"); require("./tasks/finalizeScrollClaims"); // eslint-disable-next-line node/no-missing-require require("./tasks/rescueStuckScrollTxn"); +// eslint-disable-next-line node/no-missing-require +require("./tasks/tokenTraversal"); dotenv.config(); diff --git a/scripts/sendTokensToHubPool.ts b/scripts/sendTokensToHubPool.ts deleted file mode 100644 index 930583228..000000000 --- a/scripts/sendTokensToHubPool.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { getContractFactory, ethers, SignerWithAddress } from "../utils/utils"; -import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants"; -import { hre } from "../utils/utils.hre"; -import { L1_ADDRESS_MAP, L2_ADDRESS_MAP } from "../deploy/consts"; -import { getNodeUrl, EMPTY_MERKLE_ROOT } from "@uma/common"; -import { Event, Wallet, providers, Contract } from "ethers"; - -/** - * Script to claim L1->L2 or L2->L1 messages. Run via - * ``` - * yarn hardhat run ./scripts/sendTokensToHubPool.ts \ - * --network base \ - * ``` - * This REQUIRES a spoke pool to be deployed to the specified network AND for the - * spoke pool to have the signer as the `crossDomainAdmin`. - * Flags: - * - `--network`: The L2 network, which is defined in hardhat.config.ts. - */ - -async function main() { - /* - * Setup: Need to obtain all contract addresses involved and instantiate L1/L2 providers and contracts. - */ - - // Instantiate providers/signers/chainIds - const l2ChainId = parseInt(await hre.getChainId()); - const l1ChainId = parseInt(await hre.companionNetworks.l1.getChainId()); - - // TODO: Figure out how to get this from hardhat.config.ts - const l2ProviderUrl = process.env[`NODE_URL_${l2ChainId}`]; - const l1ProviderUrl = process.env[`NODE_URL_${l1ChainId}`]; - - const l2Provider = new ethers.providers.JsonRpcProvider(l2ProviderUrl); - const l2Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l2Provider); - const l1Provider = new ethers.providers.JsonRpcProvider(l1ProviderUrl); - const l1Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l1Provider); - - // Process environment first so we can throw errors immediately if we are missing any configuration. - const spokeAddress = process.env.TEST_SPOKE_POOL_ADDRESS; // We need to specify this since this is an unsaved deployment. - if (!spokeAddress) { - throw new Error( - "[-] No spoke pool address specified. Please set TEST_SPOKE_POOL_ADDRESS to the target L2 spoke pool address" - ); - } - // TODO: It would be nice to fetch these dynamically based off of the chain ids. - const adapterAddress = process.env.ADAPTER_ADDRESS; - if (!adapterAddress) { - throw new Error("[-] No adapter address specified. Please set ADAPTER_ADDRESS to the Across L1 adapter address"); - } - const tokenAddress = process.env.TEST_TOKEN_ADDRESS ?? TOKEN_SYMBOLS_MAP.WETH.addresses[l2ChainId]; - if (!tokenAddress) { - throw new Error( - "[-] No token address specified and cannot default to WETH. Please set TEST_TOKEN_ADDRESS to the l2 token address to send back to the hub pool" - ); - } - const amountToReturn = process.env.AMOUNT_TO_RETURN ?? 1000; // Default to dust. - - // Construct the contracts - const spokePool = new Contract(spokeAddress, spokePoolAbi, l2Signer); - const adapter = new Contract(adapterAddress, adapterAbi, l1Signer); - - console.log("[+] Successfully constructed all contracts. Determining root bundle Id to use."); - let rootBundleId = 0; - try { - while (1) { - await spokePool.rootBundles(rootBundleId); - rootBundleId++; - } - } catch (e) { - console.log(`[+] Obtained latest root bundle Id ${rootBundleId}`); - } - /* - * Step 1: Craft and send a message to be sent to the provided L1 chain adapter contract. This message should be used to call `relayRootBundle` on the - * associated L2 contract - */ - const rootBundleType = "tuple(uint256,uint256,uint256[],uint32,address,address[])"; - // Construct the root bundle - const encodedRootBundle = ethers.utils.defaultAbiCoder.encode( - [rootBundleType], - [[amountToReturn, l2ChainId, [], 0, tokenAddress, []]] - ); - const rootBundleHash = ethers.utils.keccak256(encodedRootBundle); - // Submit the root bundle to chain. - const relayRootBundleTxnData = spokePool.interface.encodeFunctionData("relayRootBundle", [ - rootBundleHash, - EMPTY_MERKLE_ROOT, - ]); - const adapterTxn = await adapter.relayMessage(spokePool.address, relayRootBundleTxnData); - const txn = await adapterTxn.wait(); - // TODO: Specify adapter name - console.log( - `[+] Called L1 adapter (${adapter.address}) to relay refund leaf message to mock spoke pool at ${spokePool.address}. Txn: ${txn.transactionHash}` - ); - /* - * Step 2: Spin until we observe the message to be executed on the L2. This should take ~3 minutes. - */ - // It would be nice to just have a websocket listening for the `RelayedRootBundle` event, but I don't want to assume a websocket provider. - console.log( - "[i] Optimistically waiting 5 minutes for L1 message to propagate. If root bundle is not observed, will check spoke every minute thereafter." - ); - const fiveMins = 1000 * 60 * 5; - await delay(fiveMins); - - // We should be able to query the canonical messenger to see if our L1 message was propagated, but this requires us to instantiate a unique L2 messenger contract - // for each new chain we make, which is not scalable. Instead, we query whether our root bundle is in the spoke pool contract, as this is generalizable and does not - // require us to instantiate any new contract. - while (1) { - try { - // Check the root bundle - await spokePool.rootBundles(rootBundleId); - break; - } catch (e) { - // No root bundle made it yet. Continue to spin - console.log("[-] Root bundle not found on L2. Waiting another 60 seconds."); - await delay(1000 * 60); - } - } - console.log("[+] Root bundle observed on L2 spoke pool. Attempting to execute."); - /* - * Step 3: Call `executeRelayerRefund` on the target spoke pool to send funds back to the hub pool (or, whatever was initialized as the `hubPool` in the deploy - * script, which is likely the dev EOA). - */ - const executeRelayerRefundLeaf = await spokePool.executeRelayerRefundLeaf( - rootBundleId, - [amountToReturn, l2ChainId, [], 0, tokenAddress, []], - [] - ); - const l2Txn = await executeRelayerRefundLeaf.wait(); - console.log( - `[+] Executed root bundle with transaction hash ${l2Txn.transactionHash}. You can now test the finalizer in the relayer repository.` - ); -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); - -// Sleep -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -///////////////////// -/// Contract ABIs /// -///////////////////// -// We only need to call two functions in this script: one to set a root bundle, and one to execute that set root bundle. -// This ABI should be consistent for all spoke pool implementations. -const spokePoolAbi = [ - { - inputs: [ - { - internalType: "uint32", - name: "rootBundleId", - type: "uint32", - }, - { - components: [ - { - internalType: "uint256", - name: "amountToReturn", - type: "uint256", - }, - { - internalType: "uint256", - name: "chainId", - type: "uint256", - }, - { - internalType: "uint256[]", - name: "refundAmounts", - type: "uint256[]", - }, - { - internalType: "uint32", - name: "leafId", - type: "uint32", - }, - { - internalType: "address", - name: "l2TokenAddress", - type: "address", - }, - { - internalType: "address[]", - name: "refundAddresses", - type: "address[]", - }, - ], - internalType: "struct SpokePoolInterface.RelayerRefundLeaf", - name: "relayerRefundLeaf", - type: "tuple", - }, - { - internalType: "bytes32[]", - name: "proof", - type: "bytes32[]", - }, - ], - name: "executeRelayerRefundLeaf", - outputs: [], - stateMutability: "payable", - type: "function", - }, - { - inputs: [ - { - internalType: "bytes32", - name: "relayerRefundRoot", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "slowRelayRoot", - type: "bytes32", - }, - ], - name: "relayRootBundle", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { - internalType: "uint256", - name: "", - type: "uint256", - }, - ], - name: "rootBundles", - outputs: [ - { - internalType: "bytes32", - name: "slowRelayRoot", - type: "bytes32", - }, - { - internalType: "bytes32", - name: "relayerRefundRoot", - type: "bytes32", - }, - ], - stateMutability: "view", - type: "function", - }, -]; - -// Only one function needs to be called in the adapter. -const adapterAbi = [ - { - inputs: [ - { - internalType: "address", - name: "target", - type: "address", - }, - { - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - name: "relayMessage", - outputs: [], - stateMutability: "payable", - type: "function", - }, -]; diff --git a/tasks/tokenTraversal.ts b/tasks/tokenTraversal.ts new file mode 100644 index 000000000..91a2dff34 --- /dev/null +++ b/tasks/tokenTraversal.ts @@ -0,0 +1,132 @@ +import { getNodeUrl, EMPTY_MERKLE_ROOT } from "@uma/common"; +import { TOKEN_SYMBOLS_MAP, CHAIN_IDs } from "@across-protocol/constants"; +import { task } from "hardhat/config"; +import { minimalSpokePoolInterface, minimalAdapterInterface } from "./utils"; +import { Event, Wallet, providers, Contract, ethers, bnZero } from "ethers"; + +/** + * ``` + * yarn hardhat token-traversal \ + * --network base --adapter [adapter_address] --spokePool [spoke_pool_address] --value 0.02 + * ``` + * This REQUIRES a spoke pool to be deployed to the specified network AND for the + * spoke pool to have the signer as the `crossDomainAdmin`. + */ + +task("token-traversal", "Test L1 <-> L2 communication between a deployed L1 adapter and a L2 spoke pool") + .addParam("adapter", "address of the L1 adapter to use") + .addParam("spoke", "address of the L1 spoke pool to use") + .addOptionalParam( + "value", + "amount of ETH to send with transaction. This should only be used in special cases since improper use could nuke funds" + ) + .addOptionalParam("l2token", "The l2 token address to send over via the adapter. Defaults to weth.") + .addOptionalParam("amountToSend", "amount of token to send over via the adapter. Defaults to 1000") + .setAction(async function (taskArguments, hre_) { + const hre = hre_ as any; + const msgValue = ethers.utils.parseEther(taskArguments.value === undefined ? "0" : value); + + /* + * Setup: Need to obtain all contract addresses involved and instantiate L1/L2 providers and contracts. + */ + // Instantiate providers/signers/chainIds + const l2ChainId = parseInt(await hre.getChainId()); + const l1ChainId = parseInt(await hre.companionNetworks.l1.getChainId()); + + const l2ProviderUrl = hre.network.config.url; + + const baseNetwork = l1ChainId === CHAIN_IDs.MAINNET ? "mainnet" : "sepolia"; + const l1ProviderUrl = hre.config.networks[`${baseNetwork}`].url; + + const l2Provider = new ethers.providers.JsonRpcProvider(l2ProviderUrl); + const l2Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l2Provider); + const l1Provider = new ethers.providers.JsonRpcProvider(l1ProviderUrl); + const l1Signer = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic).connect(l1Provider); + + const tokenAddress = taskArguments.token ?? TOKEN_SYMBOLS_MAP.WETH.addresses[l2ChainId]; + if (!tokenAddress) { + throw new Error( + "[-] No token address specified and cannot default to WETH. Please set the `l2token` parameter to the l2 token address to send back to the hub pool" + ); + } + const amountToReturn = taskArguments.amountToSend ?? 1000; + + // Construct the contracts + const spokePool = new Contract(taskArguments.spokePool, minimalSpokePoolInterface, l2Signer); + const adapter = new Contract(taskArguments.adapter, minimalAdapterInterface, l1Signer); + + console.log("[+] Successfully constructed all contracts. Determining root bundle Id to use."); + let rootBundleId = 0; + try { + while (1) { + await spokePool.rootBundles(rootBundleId); + rootBundleId++; + } + } catch (e) { + console.log(`[+] Obtained latest root bundle Id ${rootBundleId}`); + } + /* + * Step 1: Craft and send a message to be sent to the provided L1 chain adapter contract. This message should be used to call `relayRootBundle` on the + * associated L2 contract + */ + const rootBundleType = "tuple(uint256,uint256,uint256[],uint32,address,address[])"; + // Construct the root bundle + const encodedRootBundle = ethers.utils.defaultAbiCoder.encode( + [rootBundleType], + [[amountToReturn, l2ChainId, [], 0, tokenAddress, []]] + ); + const rootBundleHash = ethers.utils.keccak256(encodedRootBundle); + // Submit the root bundle to chain. + const relayRootBundleTxnData = spokePool.interface.encodeFunctionData("relayRootBundle", [ + rootBundleHash, + EMPTY_MERKLE_ROOT, + ]); + const adapterTxn = await adapter.relayMessage(spokePool.address, relayRootBundleTxnData, { value }); + const txn = await adapterTxn.wait(); + console.log( + `[+] Called L1 adapter (${adapter.address}) to relay refund leaf message to mock spoke pool at ${spokePool.address}. Txn: ${txn.transactionHash}` + ); + /* + * Step 2: Spin until we observe the message to be executed on the L2. This should take ~3 minutes. + */ + // It would be nice to just have a websocket listening for the `RelayedRootBundle` event, but I don't want to assume a websocket provider. + console.log( + "[i] Optimistically waiting 5 minutes for L1 message to propagate. If root bundle is not observed, will check spoke every minute thereafter." + ); + const fiveMins = 1000 * 60 * 5; + await delay(fiveMins); + + // We should be able to query the canonical messenger to see if our L1 message was propagated, but this requires us to instantiate a unique L2 messenger contract + // for each new chain we make, which is not scalable. Instead, we query whether our root bundle is in the spoke pool contract, as this is generalizable and does not + // require us to instantiate any new contract. + while (1) { + try { + // Check the root bundle + await spokePool.rootBundles(rootBundleId); + break; + } catch (e) { + // No root bundle made it yet. Continue to spin + console.log("[-] Root bundle not found on L2. Waiting another 60 seconds."); + await delay(1000 * 60); + } + } + console.log("[+] Root bundle observed on L2 spoke pool. Attempting to execute."); + /* + * Step 3: Call `executeRelayerRefund` on the target spoke pool to send funds back to the hub pool (or, whatever was initialized as the `hubPool` in the deploy + * script, which is likely the dev EOA). + */ + const executeRelayerRefundLeaf = await spokePool.executeRelayerRefundLeaf( + rootBundleId, + [amountToReturn, l2ChainId, [], 0, tokenAddress, []], + [] + ); + const l2Txn = await executeRelayerRefundLeaf.wait(); + console.log( + `[+] Executed root bundle with transaction hash ${l2Txn.transactionHash}. You can now test the finalizer in the relayer repository.` + ); + }); + +// Sleep +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/tasks/utils.ts b/tasks/utils.ts index 81c279f84..ec61e4264 100644 --- a/tasks/utils.ts +++ b/tasks/utils.ts @@ -23,6 +23,124 @@ export const minimalSpokePoolInterface = [ stateMutability: "nonpayable", type: "function", }, + { + inputs: [ + { + internalType: "uint32", + name: "rootBundleId", + type: "uint32", + }, + { + components: [ + { + internalType: "uint256", + name: "amountToReturn", + type: "uint256", + }, + { + internalType: "uint256", + name: "chainId", + type: "uint256", + }, + { + internalType: "uint256[]", + name: "refundAmounts", + type: "uint256[]", + }, + { + internalType: "uint32", + name: "leafId", + type: "uint32", + }, + { + internalType: "address", + name: "l2TokenAddress", + type: "address", + }, + { + internalType: "address[]", + name: "refundAddresses", + type: "address[]", + }, + ], + internalType: "struct SpokePoolInterface.RelayerRefundLeaf", + name: "relayerRefundLeaf", + type: "tuple", + }, + { + internalType: "bytes32[]", + name: "proof", + type: "bytes32[]", + }, + ], + name: "executeRelayerRefundLeaf", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "relayerRefundRoot", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "slowRelayRoot", + type: "bytes32", + }, + ], + name: "relayRootBundle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "rootBundles", + outputs: [ + { + internalType: "bytes32", + name: "slowRelayRoot", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "relayerRefundRoot", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, +]; + +export const minimalAdapterInterface = [ + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "bytes", + name: "message", + type: "bytes", + }, + ], + name: "relayMessage", + outputs: [], + stateMutability: "payable", + type: "function", + }, ]; async function askQuestion(query: string) {