From dab9e649ccaaa6e8b3e98feec0dd8ef81450593f Mon Sep 17 00:00:00 2001 From: Taylor Webb Date: Wed, 17 Dec 2025 09:03:27 -0700 Subject: [PATCH 1/6] Create UpgradeSpokePool Foundry script Signed-off-by: Taylor Webb --- script/tasks/UpgradeSpokePool.s.sol | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 script/tasks/UpgradeSpokePool.s.sol diff --git a/script/tasks/UpgradeSpokePool.s.sol b/script/tasks/UpgradeSpokePool.s.sol new file mode 100644 index 000000000..397abde1e --- /dev/null +++ b/script/tasks/UpgradeSpokePool.s.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Script } from "forge-std/Script.sol"; +import { console } from "forge-std/console.sol"; + +// Minimal interface for SpokePool upgrade functions +interface ISpokePoolUpgradeable { + function pauseDeposits(bool pause) external; + function multicall(bytes[] calldata data) external returns (bytes[] memory results); + function upgradeToAndCall(address newImplementation, bytes memory data) external payable; +} + +/** + * @title UpgradeSpokePool + * @notice Generate calldata to upgrade a SpokePool deployment + * @dev This script generates the calldata needed to call relaySpokePoolAdminFunction() + * on the HubPool from the owner's account. + * + * Usage: + * forge script script/tasks/UpgradeSpokePool.s.sol:UpgradeSpokePool \ + * --sig "run(address)" \ + * -vvvv + * + * Example: + * forge script script/tasks/UpgradeSpokePool.s.sol:UpgradeSpokePool \ + * --sig "run(address)" 0x1234567890123456789012345678901234567890 \ + * -vvvv + */ +contract UpgradeSpokePool is Script { + function run(address implementation) external view { + require(implementation != address(0), "Implementation address cannot be zero"); + + /** + * We perform this seemingly unnecessary pause/unpause sequence because we want to ensure that the + * upgrade is successful and the new implementation gets forwarded calls by the proxy contract as expected. + * + * Since the upgrade and call happens atomically, the upgrade will revert if the new implementation + * is not functioning correctly. + */ + bytes[] memory multicallData = new bytes[](2); + multicallData[0] = abi.encodeWithSelector(ISpokePoolUpgradeable.pauseDeposits.selector, true); + multicallData[1] = abi.encodeWithSelector(ISpokePoolUpgradeable.pauseDeposits.selector, false); + + bytes memory data = abi.encodeWithSelector(ISpokePoolUpgradeable.multicall.selector, multicallData); + bytes memory calldata_ = abi.encodeWithSelector( + ISpokePoolUpgradeable.upgradeToAndCall.selector, + implementation, + data + ); + + console.log("======================================================="); + console.log("SpokePool Upgrade Calldata Generator"); + console.log("======================================================="); + console.log(""); + console.log("New Implementation Address:", implementation); + console.log(""); + console.log("To upgrade a SpokePool on chain :"); + console.log("Call relaySpokePoolAdminFunction() on the HubPool with:"); + console.log(" - chainId: "); + console.log(" - calldata:"); + console.logBytes(calldata_); + console.log(""); + console.log("======================================================="); + } +} From 7168ddb10602aff9eaf927cbf98937f0d3a703d0 Mon Sep 17 00:00:00 2001 From: Taylor Webb Date: Wed, 17 Dec 2025 12:16:54 -0700 Subject: [PATCH 2/6] update verifyBytecode script to check full init bytecode (including construstor args) Signed-off-by: Taylor Webb --- scripts/verifyBytecode.sh | 113 ++++++++++++++++++++++++++++++++++++++ scripts/veryfyBytecode.sh | 37 ------------- 2 files changed, 113 insertions(+), 37 deletions(-) create mode 100755 scripts/verifyBytecode.sh delete mode 100755 scripts/veryfyBytecode.sh diff --git a/scripts/verifyBytecode.sh b/scripts/verifyBytecode.sh new file mode 100755 index 000000000..c5e42690e --- /dev/null +++ b/scripts/verifyBytecode.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +# This script verifies that deployed contract bytecode matches what was in the broadcast file. +# It takes the following arguments: +# 1. Contract name to verify (e.g., "DstOFTHandler") +# 2. Path to the broadcast JSON (e.g., "broadcast/DeployDstHandler.s.sol/999/run-latest.json") +# 3. Chain name as configured in foundry.toml [rpc_endpoints] (e.g., "hyperevm") +# +# The script compares the full init code (creation bytecode + constructor args) that was +# sent in the deployment transaction against what's actually on chain. +# +# RPC URLs are resolved from foundry.toml [rpc_endpoints] which reference env vars (e.g., NODE_URL_999). +# The script automatically loads .env if present. +# +# Example: +# ./scripts/verifyBytecode.sh SponsoredCCTPDstPeriphery broadcast/114DeploySponsoredCCTPDstPeriphery.sol/999/run-latest.json hyperevm + +set -e + +# Load .env file if it exists +if [ -f .env ]; then + set -a + source .env + set +a +fi + +args=("$@") + +if [ ${#args[@]} -ne 3 ]; then + echo "Usage: $0 " + echo "" + echo "Example: $0 DstOFTHandler broadcast/DeployDstHandler.s.sol/999/run-latest.json hyperevm" + echo "" + echo "Available chains (from foundry.toml):" + grep -E "^[a-z_-]+ = " foundry.toml | grep NODE_URL | sed 's/ =.*//' | sed 's/^/ /' + exit 1 +fi + +CONTRACT_NAME=${args[0]} +BROADCAST_JSON=${args[1]} +CHAIN_NAME=${args[2]} + +# Check if broadcast file exists +if [ ! -f "$BROADCAST_JSON" ]; then + echo "Broadcast file not found: $BROADCAST_JSON" + exit 1 +fi + +# Get RPC URL from foundry.toml via environment variable +# The foundry.toml uses format: chain_name = "${NODE_URL_CHAINID}" +# We need to extract the env var name and resolve it +# Filter for NODE_URL to avoid matching [etherscan] section +RPC_ENV_VAR=$(grep "^${CHAIN_NAME} = " foundry.toml | grep NODE_URL | sed 's/.*"\${//' | sed 's/}"//') + +if [ -z "$RPC_ENV_VAR" ]; then + echo "Chain '$CHAIN_NAME' not found in foundry.toml [rpc_endpoints]" + echo "" + echo "Available chains:" + grep -E "^[a-z_-]+ = " foundry.toml | grep NODE_URL | sed 's/ =.*//' | sed 's/^/ /' + exit 1 +fi + +RPC_URL="${!RPC_ENV_VAR}" + +if [ -z "$RPC_URL" ]; then + echo "Environment variable $RPC_ENV_VAR is not set" + echo "Please set it to the RPC URL for $CHAIN_NAME" + exit 1 +fi + +echo "Verifying deployment from: $BROADCAST_JSON" +echo "Chain: $CHAIN_NAME (RPC: ${RPC_URL:0:50}...)" +echo "Contract: $CONTRACT_NAME" + +# Get the CREATE transaction for the specified contract +TX=$(jq -c --arg name "$CONTRACT_NAME" '.transactions[] | select(.transactionType == "CREATE" and .contractName == $name)' "$BROADCAST_JSON" | head -1) + +if [ -z "$TX" ]; then + echo "No CREATE transaction found for contract '$CONTRACT_NAME'" + echo "Available contracts in this broadcast:" + jq -r '.transactions[] | select(.transactionType == "CREATE") | .contractName' "$BROADCAST_JSON" | sed 's/^/ /' + exit 1 +fi + +TX_HASH=$(echo "$TX" | jq -r '.hash') +EXPECTED_INPUT=$(echo "$TX" | jq -r '.transaction.input' | sed 's/^0x//') +CONTRACT_ADDRESS=$(echo "$TX" | jq -r '.contractAddress') + +echo "Address: $CONTRACT_ADDRESS" +echo "TX: $TX_HASH" + +# Get on-chain transaction input +ONCHAIN_INPUT=$(cast tx "$TX_HASH" --rpc-url "$RPC_URL" --json 2>/dev/null | jq -r '.input' | sed 's/^0x//') + +if [ -z "$ONCHAIN_INPUT" ] || [ "$ONCHAIN_INPUT" = "null" ]; then + echo "Failed to fetch transaction from chain" + exit 1 +fi + +# Compare full init code (creation bytecode + constructor args) +if [ "$EXPECTED_INPUT" = "$ONCHAIN_INPUT" ]; then + echo "Full init code matches (including constructor args)" + + # Also show the keccak hash for reference + EXPECTED_HASH=$(cast keccak "0x$EXPECTED_INPUT" 2>/dev/null || echo "error") + echo "Init code hash: $EXPECTED_HASH" + echo "✅ Deployment verified successfully!" +else + echo "❌ Init code MISMATCH!" + echo "Expected hash: $(cast keccak "0x$EXPECTED_INPUT" 2>/dev/null || echo "error")" + echo "On-chain hash: $(cast keccak "0x$ONCHAIN_INPUT" 2>/dev/null || echo "error")" + exit 1 +fi diff --git a/scripts/veryfyBytecode.sh b/scripts/veryfyBytecode.sh deleted file mode 100755 index b77aaa4f3..000000000 --- a/scripts/veryfyBytecode.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# This script verifies the bytecode of a contract onchain matches the bytecode in the artifact -# It takes the following arguments: -# 1. The transaction hash of the contract deployment -# 2. The RPC URL to use -# 3. The name of the contract - -# Note that this script doesn't take into account any link libraries that are used in the contract - -args=("$@") - -if [ ${#args[@]} -ne 3 ]; then - echo "Usage: $0 " - exit 1 -fi - -TX=${args[0]} -RPC=${args[1]} -CONTRACT_NAME=${args[2]} - -ONCHAIN=$(cast tx $TX --rpc-url $RPC --json | jq -r '.input' | sed 's/^0x//') - -ART=out/$CONTRACT_NAME.sol/$CONTRACT_NAME.json - -CREATION=$(jq -r '.bytecode.object' "$ART" | sed 's/^0x//') - -CODE_ONCHAIN=${ONCHAIN:0:${#CREATION}} - -cast keccak $CODE_ONCHAIN -cast keccak $CREATION - -if [[ $CODE_ONCHAIN == $CREATION ]]; then - echo "✅ Code match" -else - echo "❌ Code mismatch" -fi From bba08a37f1c97d8033aecf013ac3203595a5a252 Mon Sep 17 00:00:00 2001 From: Taylor Webb Date: Thu, 18 Dec 2025 09:20:25 -0700 Subject: [PATCH 3/6] add TestChainAdapter foundry script Signed-off-by: Taylor Webb --- script/tasks/TestChainAdapter.s.sol | 106 ++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 script/tasks/TestChainAdapter.s.sol diff --git a/script/tasks/TestChainAdapter.s.sol b/script/tasks/TestChainAdapter.s.sol new file mode 100644 index 000000000..8bf48792c --- /dev/null +++ b/script/tasks/TestChainAdapter.s.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { Script } from "forge-std/Script.sol"; +import { console } from "forge-std/console.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Constants } from "../utils/Constants.sol"; +import { DeployedAddresses } from "../utils/DeployedAddresses.sol"; + +/** + * @title TestChainAdapter + * @notice Foundry script to test a chain adapter by bridging tokens from L1 to L2 + * @dev Equivalent to the Hardhat task `testChainAdapter` + * + * Usage: + * forge script script/tasks/TestChainAdapter.s.sol:TestChainAdapter \ + * --sig "run(uint256,address,address,uint256,address)" \ + * \ + * --rpc-url mainnet --broadcast + * + * Example (bridge 1 USDC to Optimism): + * forge script script/tasks/TestChainAdapter.s.sol:TestChainAdapter \ + * --sig "run(uint256,address,address,uint256,address)" \ + * 10 0x... 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 1000000 0x... \ + * --rpc-url mainnet --broadcast + */ +contract TestChainAdapter is Script, Constants, DeployedAddresses { + function run( + uint256 spokeChainId, + address adapterAddress, + address l1Token, + uint256 amount, + address l2Token + ) external { + uint256 hubChainId = block.chainid; + require(hubChainId == 1 || hubChainId == 11155111, "Must run on mainnet (1) or sepolia (11155111)"); + + address sender = vm.envOr("SENDER", msg.sender); + + console.log(""); + console.log("=============== Test Chain Adapter ==============="); + console.log("Hub Chain ID:", hubChainId); + console.log("Spoke Chain ID:", spokeChainId); + console.log("Adapter:", adapterAddress); + console.log("L1 Token:", l1Token); + console.log("L2 Token:", l2Token); + console.log("Amount:", amount); + console.log("Recipient:", sender); + console.log("--------------------------------------------------"); + + IERC20 token = IERC20(l1Token); + uint256 adapterBalance = token.balanceOf(adapterAddress); + + console.log("Adapter token balance:", adapterBalance); + + vm.startBroadcast(); + + // If adapter doesn't have enough tokens, transfer them + if (adapterBalance < amount) { + uint256 needed = amount - adapterBalance; + console.log("Transferring tokens to adapter:", needed); + + // Note: This transfer comes from the broadcasting wallet (the signer) + // The signer must have approved or have sufficient balance + bool success = token.transfer(adapterAddress, needed); + require(success, "Token transfer failed"); + console.log("Transfer complete"); + + // Re-check balance after transfer is confirmed + adapterBalance = token.balanceOf(adapterAddress); + console.log("Adapter balance after transfer:", adapterBalance); + } + + // Call relayTokens on the adapter + console.log("Calling relayTokens..."); + IAdapter(adapterAddress).relayTokens(l1Token, l2Token, adapterBalance, sender); + + console.log("--------------------------------------------------"); + console.log("[SUCCESS] Tokens relayed to chain", spokeChainId); + console.log("================================================="); + + vm.stopBroadcast(); + } + + /// @notice Simplified version that looks up adapter from deployed addresses + function runWithLookup( + uint256 spokeChainId, + string calldata adapterName, + address l1Token, + uint256 amount, + address l2Token + ) external { + uint256 hubChainId = block.chainid; + + // Try to get adapter from deployed addresses + address adapterAddress = getAddress(hubChainId, adapterName); + require(adapterAddress != address(0), string.concat("Adapter not found: ", adapterName)); + + this.run(spokeChainId, adapterAddress, l1Token, amount, l2Token); + } +} + +/// @notice Minimal interface for chain adapter +interface IAdapter { + function relayTokens(address l1Token, address l2Token, uint256 amount, address to) external payable; +} From eb0b696346d75543a90233dcda70e828782471d2 Mon Sep 17 00:00:00 2001 From: Taylor Webb Date: Thu, 18 Dec 2025 10:09:47 -0700 Subject: [PATCH 4/6] use .env mnemonic for pk Signed-off-by: Taylor Webb --- script/tasks/TestChainAdapter.s.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/script/tasks/TestChainAdapter.s.sol b/script/tasks/TestChainAdapter.s.sol index 8bf48792c..8c2c303da 100644 --- a/script/tasks/TestChainAdapter.s.sol +++ b/script/tasks/TestChainAdapter.s.sol @@ -12,6 +12,8 @@ import { DeployedAddresses } from "../utils/DeployedAddresses.sol"; * @notice Foundry script to test a chain adapter by bridging tokens from L1 to L2 * @dev Equivalent to the Hardhat task `testChainAdapter` * + * Requires MNEMONIC to be set in .env file. + * * Usage: * forge script script/tasks/TestChainAdapter.s.sol:TestChainAdapter \ * --sig "run(uint256,address,address,uint256,address)" \ @@ -35,7 +37,10 @@ contract TestChainAdapter is Script, Constants, DeployedAddresses { uint256 hubChainId = block.chainid; require(hubChainId == 1 || hubChainId == 11155111, "Must run on mainnet (1) or sepolia (11155111)"); - address sender = vm.envOr("SENDER", msg.sender); + // Derive signer from mnemonic in .env + string memory mnemonic = vm.envString("MNEMONIC"); + uint256 privateKey = vm.deriveKey(mnemonic, 0); + address sender = vm.addr(privateKey); console.log(""); console.log("=============== Test Chain Adapter ==============="); @@ -45,7 +50,7 @@ contract TestChainAdapter is Script, Constants, DeployedAddresses { console.log("L1 Token:", l1Token); console.log("L2 Token:", l2Token); console.log("Amount:", amount); - console.log("Recipient:", sender); + console.log("Sender/Recipient:", sender); console.log("--------------------------------------------------"); IERC20 token = IERC20(l1Token); @@ -53,7 +58,7 @@ contract TestChainAdapter is Script, Constants, DeployedAddresses { console.log("Adapter token balance:", adapterBalance); - vm.startBroadcast(); + vm.startBroadcast(privateKey); // If adapter doesn't have enough tokens, transfer them if (adapterBalance < amount) { From 375c726c36bcea834aa0a49503edc5ab41ea7c26 Mon Sep 17 00:00:00 2001 From: Taylor Webb Date: Thu, 18 Dec 2025 15:58:59 -0700 Subject: [PATCH 5/6] revert to original verifyBytecode script Signed-off-by: Taylor Webb --- scripts/verifyBytecode.sh | 114 +++++++------------------------------- 1 file changed, 19 insertions(+), 95 deletions(-) diff --git a/scripts/verifyBytecode.sh b/scripts/verifyBytecode.sh index c5e42690e..6844eae63 100755 --- a/scripts/verifyBytecode.sh +++ b/scripts/verifyBytecode.sh @@ -1,113 +1,37 @@ #!/bin/bash -# This script verifies that deployed contract bytecode matches what was in the broadcast file. +# This script verifies the bytecode of a contract onchain matches the bytecode in the artifact # It takes the following arguments: -# 1. Contract name to verify (e.g., "DstOFTHandler") -# 2. Path to the broadcast JSON (e.g., "broadcast/DeployDstHandler.s.sol/999/run-latest.json") -# 3. Chain name as configured in foundry.toml [rpc_endpoints] (e.g., "hyperevm") -# -# The script compares the full init code (creation bytecode + constructor args) that was -# sent in the deployment transaction against what's actually on chain. -# -# RPC URLs are resolved from foundry.toml [rpc_endpoints] which reference env vars (e.g., NODE_URL_999). -# The script automatically loads .env if present. -# -# Example: -# ./scripts/verifyBytecode.sh SponsoredCCTPDstPeriphery broadcast/114DeploySponsoredCCTPDstPeriphery.sol/999/run-latest.json hyperevm +# 1. The transaction hash of the contract deployment +# 2. The RPC URL to use +# 3. The name of the contract -set -e - -# Load .env file if it exists -if [ -f .env ]; then - set -a - source .env - set +a -fi +# Note that this script doesn't take into account any link libraries that are used in the contract args=("$@") if [ ${#args[@]} -ne 3 ]; then - echo "Usage: $0 " - echo "" - echo "Example: $0 DstOFTHandler broadcast/DeployDstHandler.s.sol/999/run-latest.json hyperevm" - echo "" - echo "Available chains (from foundry.toml):" - grep -E "^[a-z_-]+ = " foundry.toml | grep NODE_URL | sed 's/ =.*//' | sed 's/^/ /' + echo "Usage: $0 " exit 1 fi -CONTRACT_NAME=${args[0]} -BROADCAST_JSON=${args[1]} -CHAIN_NAME=${args[2]} +TX=${args[0]} +RPC=${args[1]} +CONTRACT_NAME=${args[2]} -# Check if broadcast file exists -if [ ! -f "$BROADCAST_JSON" ]; then - echo "Broadcast file not found: $BROADCAST_JSON" - exit 1 -fi +ONCHAIN=$(cast tx $TX --rpc-url $RPC --json | jq -r '.input' | sed 's/^0x//') -# Get RPC URL from foundry.toml via environment variable -# The foundry.toml uses format: chain_name = "${NODE_URL_CHAINID}" -# We need to extract the env var name and resolve it -# Filter for NODE_URL to avoid matching [etherscan] section -RPC_ENV_VAR=$(grep "^${CHAIN_NAME} = " foundry.toml | grep NODE_URL | sed 's/.*"\${//' | sed 's/}"//') +ART=out/$CONTRACT_NAME.sol/$CONTRACT_NAME.json -if [ -z "$RPC_ENV_VAR" ]; then - echo "Chain '$CHAIN_NAME' not found in foundry.toml [rpc_endpoints]" - echo "" - echo "Available chains:" - grep -E "^[a-z_-]+ = " foundry.toml | grep NODE_URL | sed 's/ =.*//' | sed 's/^/ /' - exit 1 -fi +CREATION=$(jq -r '.bytecode.object' "$ART" | sed 's/^0x//') -RPC_URL="${!RPC_ENV_VAR}" +CODE_ONCHAIN=${ONCHAIN:0:${#CREATION}} -if [ -z "$RPC_URL" ]; then - echo "Environment variable $RPC_ENV_VAR is not set" - echo "Please set it to the RPC URL for $CHAIN_NAME" - exit 1 -fi - -echo "Verifying deployment from: $BROADCAST_JSON" -echo "Chain: $CHAIN_NAME (RPC: ${RPC_URL:0:50}...)" -echo "Contract: $CONTRACT_NAME" +cast keccak $CODE_ONCHAIN +cast keccak $CREATION -# Get the CREATE transaction for the specified contract -TX=$(jq -c --arg name "$CONTRACT_NAME" '.transactions[] | select(.transactionType == "CREATE" and .contractName == $name)' "$BROADCAST_JSON" | head -1) - -if [ -z "$TX" ]; then - echo "No CREATE transaction found for contract '$CONTRACT_NAME'" - echo "Available contracts in this broadcast:" - jq -r '.transactions[] | select(.transactionType == "CREATE") | .contractName' "$BROADCAST_JSON" | sed 's/^/ /' - exit 1 -fi - -TX_HASH=$(echo "$TX" | jq -r '.hash') -EXPECTED_INPUT=$(echo "$TX" | jq -r '.transaction.input' | sed 's/^0x//') -CONTRACT_ADDRESS=$(echo "$TX" | jq -r '.contractAddress') - -echo "Address: $CONTRACT_ADDRESS" -echo "TX: $TX_HASH" - -# Get on-chain transaction input -ONCHAIN_INPUT=$(cast tx "$TX_HASH" --rpc-url "$RPC_URL" --json 2>/dev/null | jq -r '.input' | sed 's/^0x//') - -if [ -z "$ONCHAIN_INPUT" ] || [ "$ONCHAIN_INPUT" = "null" ]; then - echo "Failed to fetch transaction from chain" - exit 1 -fi - -# Compare full init code (creation bytecode + constructor args) -if [ "$EXPECTED_INPUT" = "$ONCHAIN_INPUT" ]; then - echo "Full init code matches (including constructor args)" - - # Also show the keccak hash for reference - EXPECTED_HASH=$(cast keccak "0x$EXPECTED_INPUT" 2>/dev/null || echo "error") - echo "Init code hash: $EXPECTED_HASH" - echo "✅ Deployment verified successfully!" +if [[ $CODE_ONCHAIN == $CREATION ]]; then + echo "✅ Code match" else - echo "❌ Init code MISMATCH!" - echo "Expected hash: $(cast keccak "0x$EXPECTED_INPUT" 2>/dev/null || echo "error")" - echo "On-chain hash: $(cast keccak "0x$ONCHAIN_INPUT" 2>/dev/null || echo "error")" - exit 1 -fi + echo "❌ Code mismatch" +fi \ No newline at end of file From 9fc07e4bb10a972c4fc3f046674cbc30b61c7c1c Mon Sep 17 00:00:00 2001 From: Taylor Webb Date: Thu, 18 Dec 2025 16:09:32 -0700 Subject: [PATCH 6/6] remove old hardhat tasks and associated documentaiton Signed-off-by: Taylor Webb --- README.md | 6 - hardhat.config.ts | 15 -- tasks/enableL1TokenAcrossEcosystem.ts | 310 ------------------------ tasks/evmRelayMessageWithdrawal.ts | 138 ----------- tasks/finalizeScrollClaims.ts | 115 --------- tasks/rescueStuckScrollTxn.ts | 146 ------------ tasks/testChainAdapter.ts | 92 -------- tasks/types.ts | 3 - tasks/upgradeSpokePool.ts | 41 ---- tasks/utils.ts | 203 ---------------- tasks/verifyBytecode.ts | 325 -------------------------- tasks/verifySpokePool.ts | 95 -------- 12 files changed, 1489 deletions(-) delete mode 100644 tasks/enableL1TokenAcrossEcosystem.ts delete mode 100644 tasks/evmRelayMessageWithdrawal.ts delete mode 100644 tasks/finalizeScrollClaims.ts delete mode 100644 tasks/rescueStuckScrollTxn.ts delete mode 100644 tasks/testChainAdapter.ts delete mode 100644 tasks/types.ts delete mode 100644 tasks/upgradeSpokePool.ts delete mode 100644 tasks/utils.ts delete mode 100644 tasks/verifyBytecode.ts delete mode 100644 tasks/verifySpokePool.ts diff --git a/README.md b/README.md index d8b32236b..6e3558b57 100644 --- a/README.md +++ b/README.md @@ -292,12 +292,6 @@ solana-verify remote submit-job \ ## Miscellaneous topics -### Manually Finalizing Scroll Claims from L2 -> L1 (Mainnet | Sepolia) - -```shell -yarn hardhat finalize-scroll-claims --l2-address {operatorAddress} -``` - ### Slither [Slither](https://github.com/crytic/slither) is a Solidity static analysis framework written in Python 3. It runs a diff --git a/hardhat.config.ts b/hardhat.config.ts index 12e7d866d..49173c671 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -55,21 +55,6 @@ const getDefaultHardhatConfig = (chainId: number, isTestnet: boolean = false): a }; }; -// Custom tasks to add to HRE. -const tasks = [ - "enableL1TokenAcrossEcosystem", - "finalizeScrollClaims", - "rescueStuckScrollTxn", - "verifySpokePool", - "verifyBytecode", - "evmRelayMessageWithdrawal", - "testChainAdapter", - "upgradeSpokePool", -]; - -// eslint-disable-next-line node/no-missing-require -tasks.forEach((task) => require(`./tasks/${task}`)); - const isTest = process.env.IS_TEST === "true"; // To compile with zksolc, `hardhat` must be the default network and its `zksync` property must be true. diff --git a/tasks/enableL1TokenAcrossEcosystem.ts b/tasks/enableL1TokenAcrossEcosystem.ts deleted file mode 100644 index db77b7f27..000000000 --- a/tasks/enableL1TokenAcrossEcosystem.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { task, types } from "hardhat/config"; -import assert from "assert"; -import { CHAIN_IDs, MAINNET_CHAIN_IDs, TESTNET_CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../utils/constants"; -import { askYesNoQuestion, resolveTokenOnChain, isTokenSymbol, minimalSpokePoolInterface } from "./utils"; -import { TokenSymbol } from "./types"; - -const { ARBITRUM, OPTIMISM } = CHAIN_IDs; -const NO_SYMBOL = "----"; -const NO_ADDRESS = "------------------------------------------"; - -const IGNORED_CHAINS = [CHAIN_IDs.BOBA, CHAIN_IDs.SOLANA]; -const V4_CHAINS = [CHAIN_IDs.BSC, CHAIN_IDs.LISK, CHAIN_IDs.LINEA, CHAIN_IDs.WORLD_CHAIN]; - -// Supported mainnet chain IDs. -const enabledChainIds = (hubChainId: number) => { - const chainIds = hubChainId === CHAIN_IDs.MAINNET ? MAINNET_CHAIN_IDs : TESTNET_CHAIN_IDs; - return Object.values(chainIds) - .map(Number) - .filter((chainId) => !IGNORED_CHAINS.includes(chainId)) - .sort((x, y) => x - y); -}; - -const getChainsFromList = (taskArgInput: string): number[] => - taskArgInput - ?.replace(/\s/g, "") - ?.split(",") - ?.map((chainId: string) => Number(chainId)) || []; - -const notSolana = (chainId: number | string) => ![CHAIN_IDs.SOLANA, CHAIN_IDs.SOLANA_DEVNET].includes(Number(chainId)); - -task("enableToken", "Enable a provided token across the entire ecosystem of supported chains") - .addFlag("execute", "Provide this flag if you would like to actually execute the transaction from the EOA") - .addFlag("disable", "Set to disable deposit routes for the specified chains") - .addParam("token", "Symbol of token to enable") - .addOptionalParam("chains", "Comma-delimited list of chains to enable the token on. Defaults to all supported chains") - .addOptionalParam("burn", "Amount of LP token to burn when enabling a new token", 1, types.int) - .addOptionalParam( - "customoptimismbridge", - "Custom token bridge to set for optimism, for example used with SNX and DAI" - ) - .addOptionalParam("depositroutechains", "ChainIds to enable deposit routes for exclusively. Separated by comma.") - .setAction(async function (taskArguments, hre_) { - const hre = hre_ as any; - const { burn, chains, execute, token: symbol } = taskArguments; - const enableRoute = !taskArguments.disable; - - const hubChainId = parseInt(await hre.getChainId()); - if (hubChainId === 31337) { - throw new Error(`Defaulted to network \`hardhat\`; specify \`--network mainnet\` or \`--network sepolia\``); - } - - const _matchedSymbol = Object.keys(TOKEN_SYMBOLS_MAP).find((_symbol) => _symbol === symbol); - assert(isTokenSymbol(_matchedSymbol)); - const matchedSymbol = _matchedSymbol as TokenSymbol; - - const l1TokenAddr = TOKEN_SYMBOLS_MAP[matchedSymbol].addresses[hubChainId]; - assert(l1TokenAddr !== undefined, `Could not find ${symbol} in TOKEN_SYMBOLS_MAP`); - - // If deposit routes chains are provided then we'll only add routes involving these chains. This is used to add new - // deposit routes to a new chain for an existing L1 token, so we also won't add a new LP token if this is defined. - const depositRouteChains = getChainsFromList(taskArguments.depositroutechains); - if (depositRouteChains.length > 0) { - console.log(`\nOnly adding deposit routes involving chains on list ${depositRouteChains.join(", ")}`); - } - - const hasSetConfigStore = await askYesNoQuestion( - `\nHave you setup the ConfigStore for this token? If not then this script will exit because a rate model must be set before the first deposit is sent otherwise the bots will error out` - ); - if (!hasSetConfigStore) process.exit(0); - - console.log(`\nRunning task to enable L1 token over entire Across ecosystem 🌉. L1 token: ${l1TokenAddr}`); - const { deployments, ethers } = hre; - const { AddressZero: ZERO_ADDRESS } = ethers.constants; - const { hexlify, zeroPad } = ethers.utils; - const [signer] = await hre.ethers.getSigners(); - const { BigNumber } = ethers; - - // Remove chainIds that are in the ignore list. - const _enabledChainIds = enabledChainIds(hubChainId); - let inputChains: number[] = []; - try { - inputChains = (chains?.split(",") ?? _enabledChainIds).map(Number); - console.log(`\nParsed 'chains' argument:`, inputChains); - } catch (error) { - throw new Error(`Failed to parse 'chains' argument ${chains} as a comma-separated list of numbers.`); - } - if (inputChains.length === 0) inputChains = _enabledChainIds; - else if (inputChains.some((chain) => isNaN(chain) || !Number.isInteger(chain) || chain < 0)) { - throw new Error(`Invalid chains list: ${inputChains}`); - } - const chainIds = _enabledChainIds.filter((chainId) => inputChains.includes(chainId) && notSolana(chainId)); - - console.log("\nLoading L2 companion token address for provided L1 token."); - const tokens = Object.fromEntries( - chainIds.map((chainId) => { - const token = resolveTokenOnChain(matchedSymbol, chainId); - if (token === undefined) { - return [chainId, { symbol: NO_SYMBOL, address: NO_ADDRESS }]; - } - - const { symbol, address } = token; - return [chainId, { symbol: symbol as string, address }]; - }) - ); - - console.table( - Object.entries(tokens).map(([_chainId, { symbol, address }]) => ({ chainId: Number(_chainId), symbol, address })), - ["chainId", "symbol", "address"] - ); - - // Check the user is ok with the token addresses provided. If not, abort. - if (!(await askYesNoQuestion("\nDo these token addresses match your expectations?"))) process.exit(0); - - // Construct an ethers contract to access the `interface` prop to create encoded function calls. - const hubPoolDeployment = await deployments.get("HubPool"); - const hubPool = new ethers.Contract(hubPoolDeployment.address, hubPoolDeployment.abi, signer); - console.log(`\nConstructing calldata to enable these tokens. Using HubPool at address: ${hubPool.address}`); - - // Construct calldata to enable these tokens. - // nb. This implementation relies on initial callData ordering when unpacking via Object.entries(). - const callData: { [target: string]: string[] } = {}; - - // If the l1 token is not yet enabled for LP, enable it. - let { lpToken: lpTokenAddr } = await hubPool.pooledTokens(l1TokenAddr); - if (lpTokenAddr === ZERO_ADDRESS) { - const [lpFactoryAddr, { abi: lpFactoryABI }] = await Promise.all([ - hubPool.lpTokenFactory(), - deployments.get("LpTokenFactory"), - ]); - const lpTokenFactory = new ethers.Contract(lpFactoryAddr, lpFactoryABI, signer); - lpTokenAddr = await lpTokenFactory.callStatic.createLpToken(l1TokenAddr); - console.log(`\nAdding calldata to enable liquidity provision on ${l1TokenAddr} (LP token ${lpTokenAddr})`); - - const erc20 = await ethers.getContractFactory("ExpandedERC20"); - const l1Token = erc20.attach(l1TokenAddr); - const decimals = await l1Token.decimals(); - const depositAmount = BigNumber.from(burn ?? "1").mul(BigNumber.from(10).pow(decimals)); - const doBurn = await askYesNoQuestion(`\nBurn ${burn} ${symbol} (${depositAmount}) LP tokens? (RECOMMENDED!)`); - - if (doBurn) { - callData[l1Token.address] ??= []; - callData[l1Token.address].push( - l1Token.interface.encodeFunctionData("approve", [hubPool.address, depositAmount]) - ); - } - - // Create LP token and seed the LP with `depositAmount` amount. - callData[hubPool.address] ??= []; - callData[hubPool.address].push( - hubPool.interface.encodeFunctionData("enableL1TokenForLiquidityProvision", [l1TokenAddr]) - ); - callData[hubPool.address].push( - hubPool.interface.encodeFunctionData("addLiquidity", [l1Token.address, depositAmount]) - ); - - // For a new token, the balance of lpToken will be 1:1 with depositAmount. Burn it. - if (doBurn) { - callData[lpTokenAddr] ??= []; - const burnRecipient = hexlify(zeroPad(ethers.utils.arrayify("0x01"), 20)); - callData[lpTokenAddr].push( - erc20.attach(lpTokenAddr).interface.encodeFunctionData("transfer", [burnRecipient, depositAmount]) - ); - } - } - - console.log("\nAdding calldata to enable routes between all chains and tokens:"); - let i = 0; // counter for logging. - const skipped: { [originChainId: number]: number[] } = {}; - const routeChainIds = Object.keys(tokens).map(Number); - const chainPadding = _enabledChainIds[enabledChainIds.length - 1].toString().length; - const formatChainId = (chainId: number): string => chainId.toString().padStart(chainPadding, " "); - routeChainIds.forEach((fromId) => { - const formattedFromId = formatChainId(fromId); - const { address: inputToken } = tokens[fromId]; - skipped[fromId] = []; - routeChainIds.forEach((toId) => { - if ( - fromId === toId || - V4_CHAINS.includes(fromId) || - [fromId, toId].some((chainId) => tokens[chainId].symbol === NO_SYMBOL) - ) { - return; - } - - // If deposit route chains are defined, only add route if it involves a chain on that list - if ( - depositRouteChains.length === 0 || - depositRouteChains.includes(toId) || - depositRouteChains.includes(fromId) - ) { - const n = (++i).toString().padStart(2, " "); - console.log(`\t${n} Added route for ${inputToken} from ${formattedFromId} -> ${formatChainId(toId)}.`); - callData[hubPool.address] ??= []; - callData[hubPool.address].push( - hubPool.interface.encodeFunctionData("setDepositRoute", [fromId, toId, inputToken, enableRoute]) - ); - } else { - skipped[fromId].push(toId); - } - }); - }); - console.log(""); - - Object.entries(skipped).forEach(([srcChainId, dstChainIds]) => { - if (dstChainIds.length > 0) { - const { address: inputToken } = tokens[srcChainId]; - console.log(`\tSkipped route for ${inputToken} on chains ${srcChainId} -> ${dstChainIds.join(", ")}.`); - } - }); - - // If deposit route chains are defined then we don't want to add a new PoolRebalanceRoute - console.log("\nAdding calldata to set the pool rebalance route for the respective destination tokens:"); - i = 0; // counter for logging. - const rebalanceRoutesSkipped: number[] = []; - chainIds.forEach((toId) => { - const destinationToken = tokens[toId].address; - if (destinationToken === NO_ADDRESS) { - return; - } - - // If deposit route chains are defined, only add route if it involves a chain on that list - if (depositRouteChains.length === 0 || depositRouteChains.includes(toId)) { - const n = (++i).toString().padStart(2, " "); - console.log( - `\t${n} Setting rebalance route for chain ${symbol} ${hubChainId} -> ${destinationToken} on ${toId}.` - ); - callData[hubPool.address] ??= []; - callData[hubPool.address].push( - hubPool.interface.encodeFunctionData("setPoolRebalanceRoute", [toId, l1TokenAddr, destinationToken]) - ); - } else { - rebalanceRoutesSkipped.push(toId); - } - }); - - if (rebalanceRoutesSkipped.length > 0) { - console.log(`\n\tSkipped pool rebalance routes ${hubChainId} -> ${rebalanceRoutesSkipped.join(", ")}.`); - } - - // We only need to whitelist an Arbitrum token on the SpokePool if we're setting up a pool rebalance route between - // mainnet and Arbitrum, so if deposit route chains are set then no need to do this. - if (depositRouteChains.includes(ARBITRUM)) { - const arbitrumToken = tokens[ARBITRUM].address; - console.log( - `\nAdding call data to whitelist L2 ${arbitrumToken} -> L1 token ${l1TokenAddr} on Arbitrum.` + - " This is only needed on this chain." - ); - - // Address doesn't matter, we only want the interface. - const spokePool = new ethers.Contract(hubPoolDeployment.address, minimalSpokePoolInterface, signer); - // Find the address of the Arbitrum representation of this token. Construct whitelistToken call to send to the - // Arbitrum spoke pool via the relaySpokeAdminFunction call. - const whitelistTokenCallData = spokePool.interface.encodeFunctionData("whitelistToken", [ - arbitrumToken, - l1TokenAddr, - ]); - callData[hubPool.address] ??= []; - callData[hubPool.address].push( - hubPool.interface.encodeFunctionData("relaySpokePoolAdminFunction", [ARBITRUM, whitelistTokenCallData]) - ); - } - - // Add optimism setTokenBridge call if the token has a custom bridge needed to get to mainnet. - if (depositRouteChains.includes(OPTIMISM) && taskArguments.customoptimismbridge) { - console.log("\nAdding call data to set custom Optimism bridge."); - - // Address doesn't matter, we only want the interface: - const spokePool = new ethers.Contract(hubPoolDeployment.address, minimalSpokePoolInterface, signer); - const optimismToken = tokens[OPTIMISM].address; - const setTokenBridgeCallData = spokePool.interface.encodeFunctionData("setTokenBridge", [ - optimismToken, - taskArguments.customoptimismbridge, - ]); - callData[hubPool.address].push( - hubPool.interface.encodeFunctionData("relaySpokePoolAdminFunction", [OPTIMISM, setTokenBridgeCallData]) - ); - } - - console.log(`\n***DONE***\nProduced calldata for ${Object.values(callData).flat().length} calls.`); - if (execute) { - console.log(`\n--execute provided. Submitting transactions.`); - for (let [target, calls] of Object.entries(callData)) { - if (target === hubPool.address) { - const { hash, wait } = await hubPool.multicall(calls); - console.log(`\nTarget ${target}: ${hash}`); - await wait(); - continue; - } - for (const data of calls) { - const txnRequest = await signer.populateTransaction({ to: target, data }); - const txn = await signer.signTransaction(txnRequest); - const { hash, wait } = await signer.sendTransaction(txn); - console.log(`\nTarget ${target}: ${hash}`); - await wait(); - } - } - } else { - let i = 1; - for (const [target, calldata] of Object.entries(callData)) { - console.log(`\nTransaction ${i++}:`); - if (target === hubPool.address) { - console.log("\tmethod: multicall"); - console.log(`\ttarget: ${target}\n\tdata:\t${[calldata]}`); - continue; - } - - calldata.forEach((data) => console.log(`\ttarget:\t${target}\n\tdata:\t${data}`)); - } - } - }); diff --git a/tasks/evmRelayMessageWithdrawal.ts b/tasks/evmRelayMessageWithdrawal.ts deleted file mode 100644 index 095e2321e..000000000 --- a/tasks/evmRelayMessageWithdrawal.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { EMPTY_MERKLE_ROOT } from "@uma/common"; -import { CHAIN_IDs } from "@across-protocol/constants"; -import { task } from "hardhat/config"; -import { minimalSpokePoolInterface, minimalAdapterInterface } from "./utils"; -import { Contract, ethers } from "ethers"; - -/** - * ``` - * yarn hardhat evm-relay-message-withdrawal \ - * --network [l2_network] --adapter [adapter_address] --spoke-pool [spoke_pool_address] --l2-token [l2_token_address] \ - * --amount-to-return [amount_of_l2_token_to_bridge_to_withdrawal_recipient] --value [eth_to_send] - * ``` - * This REQUIRES a spoke pool to be deployed to the specified network AND for the - * spoke pool to have the adapter as the `crossDomainAdmin`. - */ - -task("evm-relay-message-withdrawal", "Test L1 <-> L2 communication between a deployed L1 adapter and a L2 spoke pool.") - .addParam("spokePool", "address of the L2 spoke pool to use.") - .addParam( - "adapter", - "address of the adapter to use. This must correspond to the network on which the L2 spoke pool is deployed" - ) - .addParam("l2Token", "The l2 token address to withdraw from the spoke pool.") - .addParam("amountToReturn", "amount of token to withdraw from the spoke pool.") - .addOptionalParam( - "value", - "amount of ETH to send with transaction (which may be needed to call `relayMessage`, such as with zksync). This should only be used in special cases since improper use could nuke funds." - ) - .setAction(async function (taskArguments, hre_) { - const hre = hre_ as any; - const msgValue = ethers.utils.parseEther(taskArguments.value === undefined ? "0" : taskArguments.value); - if (!ethers.utils.isAddress(taskArguments.l2Token)) - throw new Error(`${taskArguments.l2token} is not a valid evm token address`); - if (isNaN(taskArguments.amountToReturn) || taskArguments.amountToReturn < 0) - throw new Error(`${taskArguments.amountToReturn} is not a valid amount to send`); - - /** - * 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 l1Network = l1ChainId === CHAIN_IDs.MAINNET ? "mainnet" : "sepolia"; - const l1ProviderUrl = hre.config.networks[`${l1Network}`].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); - - // 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], - [[taskArguments.amountToReturn, l2ChainId, [], 0, taskArguments.l2Token, []]] - ); - 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: msgValue }); - 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. Time varies per chain. - */ - - 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, - [taskArguments.amountToReturn, l2ChainId, [], 0, taskArguments.l2Token, []], - [] - ); - 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/finalizeScrollClaims.ts b/tasks/finalizeScrollClaims.ts deleted file mode 100644 index 79e248cfa..000000000 --- a/tasks/finalizeScrollClaims.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable camelcase */ -import axios from "axios"; -import { task } from "hardhat/config"; -import { Contract, Signer } from "ethers"; -import { L1_ADDRESS_MAP } from "../deploy/consts"; - -require("dotenv").config(); - -const relayMessengerAbi = [ - { - inputs: [ - { - internalType: "address", - name: "_from", - type: "address", - }, - { - internalType: "address", - name: "_to", - type: "address", - }, - { - internalType: "uint256", - name: "_value", - type: "uint256", - }, - { - internalType: "uint256", - name: "_nonce", - type: "uint256", - }, - { - internalType: "bytes", - name: "_message", - type: "bytes", - }, - { - components: [ - { - internalType: "uint256", - name: "batchIndex", - type: "uint256", - }, - { - internalType: "bytes", - name: "merkleProof", - type: "bytes", - }, - ], - internalType: "struct IL1ScrollMessenger.L2MessageProof", - name: "_proof", - type: "tuple", - }, - ], - name: "relayMessageWithProof", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, -]; - -task("finalize-scroll-claims", "Finalize scroll claims") - .addParam("l2Address", "Address that we'll attempt to claim") - .setAction(async function (taskArguments, hre_: any) { - const chainId = await hre_.getChainId(); - if (!["11155111", "1"].includes(String(chainId))) { - throw new Error("This script can only be run on Sepolia or Ethereum mainnet"); - } - const l2Operator = String(taskArguments.l2Address); - if (!hre_.ethers.utils.isAddress(l2Operator)) { - throw new Error("Invalid L2 operator address. Must pass as last argument to script"); - } - - const signer = (await (hre_ as any).ethers.getSigners())[0] as unknown as Signer; - const apiUrl = `https://${String(chainId) === "1" ? "mainnet" : "sepolia"}-api-bridge.scroll.io/api/claimable`; - const messengerContract = new Contract(L1_ADDRESS_MAP[chainId].scrollMessengerRelay, relayMessengerAbi, signer); - const claimList = ( - await axios.get<{ - data: { - result: { - claimInfo: { - from: string; - to: string; - value: string; - nonce: string; - message: string; - proof: string; - batch_index: string; - }; - }[]; - }; - }>(apiUrl, { - params: { - page_size: 100, - page: 1, - address: l2Operator, - }, - }) - ).data.data.result.map(({ claimInfo }) => claimInfo); - console.log(`Attempting to finalize ${claimList.length} claims for ${l2Operator}`); - const result = await Promise.allSettled( - claimList.map(async (c) => { - console.log(`Finalizing claim: (c.from -> c.to) = (${c.from}, ${c.to})`); - await messengerContract.relayMessageWithProof(c.from, c.to, c.value, c.nonce, c.message, { - batchIndex: c.batch_index, - merkleProof: c.proof, - }); - }) - ); - console.log(`Successfully finalized ${result.filter((r) => r.status === "fulfilled").length} claims`); - if (result.filter((r) => r.status === "rejected").length > 0) { - console.log(result.filter((r) => r.status === "rejected")); - console.log(`Failed to finalize ${result.filter((r) => r.status === "rejected").length} claims`); - } - }); diff --git a/tasks/rescueStuckScrollTxn.ts b/tasks/rescueStuckScrollTxn.ts deleted file mode 100644 index ed9a67f8c..000000000 --- a/tasks/rescueStuckScrollTxn.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* eslint-disable camelcase */ -import { task } from "hardhat/config"; -import { Contract, Signer, ethers } from "ethers"; -import { L1_ADDRESS_MAP } from "../deploy/consts"; - -require("dotenv").config(); - -const relayMessengerAbi = [ - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "sender", - type: "address", - }, - { - indexed: true, - internalType: "address", - name: "target", - type: "address", - }, - { - indexed: false, - internalType: "uint256", - name: "value", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "messageNonce", - type: "uint256", - }, - { - indexed: false, - internalType: "uint256", - name: "gasLimit", - type: "uint256", - }, - { - indexed: false, - internalType: "bytes", - name: "message", - type: "bytes", - }, - ], - name: "SentMessage", - type: "event", - }, - { - inputs: [ - { - internalType: "address", - name: "_from", - type: "address", - }, - { - internalType: "address", - name: "_to", - type: "address", - }, - { - internalType: "uint256", - name: "_value", - type: "uint256", - }, - { - internalType: "uint256", - name: "_messageNonce", - type: "uint256", - }, - { - internalType: "bytes", - name: "_message", - type: "bytes", - }, - { - internalType: "uint32", - name: "_newGasLimit", - type: "uint32", - }, - { - internalType: "address", - name: "_refundAddress", - type: "address", - }, - ], - name: "replayMessage", - outputs: [], - stateMutability: "payable", - type: "function", - }, -]; - -task("rescue-stuck-scroll-txn", "Rescue a failed Scroll transaction") - .addParam("l1Hash", "Txn of the L1 message to rescue") - .addParam("gasLimit", "Gas limit to use for the rescue transaction") - .setAction(async function (taskArguments, hre_: any) { - const chainId = await hre_.getChainId(); - if (!["1", "11155111"].includes(String(chainId))) { - throw new Error("This script can only be run on Sepolia or Ethereum mainnet"); - } - const signer = (await hre_.ethers.getSigners())[0] as unknown as Signer; - const messengerContract = new Contract(L1_ADDRESS_MAP[chainId].scrollMessengerRelay, relayMessengerAbi, signer); - - const txn = await signer.provider?.getTransactionReceipt(taskArguments.l1Hash); - const relevantEvent = txn?.logs?.find( - (log) => log.topics[0] === messengerContract.interface.getEventTopic("SentMessage") - ); - if (!relevantEvent) { - throw new Error("No relevant event found. Is this a Scroll bridge transaction?"); - } - const decodedEvent = messengerContract.interface.parseLog(relevantEvent); - const { sender, target, value, messageNonce, message } = decodedEvent.args; - const refundAddress = await signer.getAddress(); - - console.debug("Log found. Event Decoded."); - console.debug("Will replay with these parameters:", { - _from: sender, - _to: target, - _value: value.toString(), - _messageNonce: messageNonce.toString(), - _message: message.toString(), - _newGasLimit: taskArguments.gasLimit, - _refundAddress: refundAddress, - }); - console.debug("Replaying message (sending with 0.001ETH )..."); - const resultingTxn = await messengerContract.replayMessage( - sender, // _from - target, // _to - value, // _value - messageNonce, // _messageNonce - message, // _message - ethers.BigNumber.from(taskArguments.gasLimit), // _newGasLimit - refundAddress, // _refundAddress - { - // 0.001 ETH to be sent to the Scroll relayer (to cover L1 gas costs) - // Using recommended value default as described here: https://docs.scroll.io/en/developers/l1-and-l2-bridging/eth-and-erc20-token-bridge/ - // *Any* leftover ETH will be immediately refunded to the signer - this is just the L1 gas cost for submitting the transaction - value: ethers.utils.parseEther("0.001"), - } - ); - console.log("Replay transaction hash:", resultingTxn.hash); - }); diff --git a/tasks/testChainAdapter.ts b/tasks/testChainAdapter.ts deleted file mode 100644 index 79d5591f4..000000000 --- a/tasks/testChainAdapter.ts +++ /dev/null @@ -1,92 +0,0 @@ -import assert from "assert"; -import { getMnemonic } from "@uma/common"; -import { task } from "hardhat/config"; -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../utils/constants"; -import { askYesNoQuestion, resolveTokenOnChain } from "./utils"; - -// Chain adapter names are not 1:1 consistent with chain names, so some overrides are needed. -const chains = { - [CHAIN_IDs.ARBITRUM]: "Arbitrum_Adapter", - [CHAIN_IDs.ALEPH_ZERO]: "Arbitrum_CustomGasToken_Adapter", - [CHAIN_IDs.WORLD_CHAIN]: "WorldChain_Adapter", -}; - -task("testChainAdapter", "Verify a chain adapter") - .addParam("chain", "chain ID of the adapter being tested") - .addParam("token", "Token to bridge to the destination chain") - .addParam("amount", "Amount to bridge to the destination chain") - .addOptionalParam("adapter", "Optional address of the bridge adapter to use") - .setAction(async function (args, hre: HardhatRuntimeEnvironment) { - const { deployments, ethers, getChainId, network } = hre; - const provider = new ethers.providers.StaticJsonRpcProvider(network.config.url); - const signer = new ethers.Wallet.fromMnemonic(getMnemonic()).connect(provider); - - const hubChainId = await getChainId(); - assert( - [CHAIN_IDs.MAINNET, CHAIN_IDs.SEPOLIA].includes(Number(hubChainId)), - "required: --network [mainnet|sepolia]" - ); - const spokeChainId = parseInt(args.chain); - - const [spokeName] = Object.entries(CHAIN_IDs).find(([, chainId]) => chainId === spokeChainId) ?? []; - assert(spokeName, `Could not find any chain entry for chainId ${spokeChainId}.`); - - let adapterAddress = args.adapter; - if (!adapterAddress) { - // Last resort - try to guess the adapter address based on the chainId. - const adapterName = - chains[spokeChainId] ?? `${spokeName[0].toUpperCase()}${spokeName.slice(1).toLowerCase()}_Adapter`; - ({ address: adapterAddress } = await deployments.get(adapterName)); - } - - const abi = new ethers.utils.Interface(["function relayTokens(address,address,uint256,address) payable"]); - const adapter = new ethers.Contract(adapterAddress, abi, provider); - const tokenSymbol = args.token.toUpperCase(); - const tokenAddress = TOKEN_SYMBOLS_MAP[tokenSymbol].addresses[hubChainId]; - - // For USDC this will resolve to native USDC on CCTP-enabled chains. - const _l2Token = resolveTokenOnChain(tokenSymbol, spokeChainId); - assert(_l2Token !== undefined, `Token ${tokenSymbol} is not known on chain ${spokeChainId}`); - const l2Token = _l2Token.address; - console.log(`Resolved ${tokenSymbol} l2 token address on chain ${spokeChainId}: ${l2Token}.`); - - const erc20 = (await ethers.getContractFactory("ExpandedERC20")).attach(tokenAddress); - let balance = await erc20.balanceOf(adapterAddress); - const decimals = await erc20.decimals(); - const { amount } = args; - const scaledAmount = ethers.utils.parseUnits(amount, decimals); - - if (balance.lt(scaledAmount)) { - const proceed = await askYesNoQuestion( - `\t\nWARNING: ${amount} ${tokenSymbol} may be lost.\n` + - `\t\nProceed to send ${amount} ${tokenSymbol} to chain adapter ${adapterAddress} ?` - ); - if (!proceed) process.exit(0); - - const txn = await erc20.connect(signer).transfer(adapterAddress, scaledAmount); - console.log(`Transferring ${amount} ${tokenSymbol} -> ${adapterAddress}: ${txn.hash}`); - await txn.wait(); - } - - balance = await erc20.balanceOf(adapterAddress); - const recipient = await signer.getAddress(); - - let populatedTxn = await adapter.populateTransaction.relayTokens(tokenAddress, l2Token, balance, recipient); - const gasLimit = await provider.estimateGas(populatedTxn); - - // Any adapter requiring msg.value > 0 (i.e. Scroll) will fail here. - const txn = await adapter.connect(signer).relayTokens( - tokenAddress, - l2Token, - balance, - recipient, - { gasLimit: gasLimit.mul(2) } // 2x the gas limit; this helps on OP stack bridges. - ); - - console.log( - `Relaying ${balance} ${tokenSymbol} from ${adapterAddress}` + - ` to chain ${spokeChainId} recipient ${recipient}: ${txn.hash}.` - ); - await txn.wait(); - }); diff --git a/tasks/types.ts b/tasks/types.ts deleted file mode 100644 index 7bfb843f4..000000000 --- a/tasks/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { TOKEN_SYMBOLS_MAP } from "../utils/constants"; - -export type TokenSymbol = keyof typeof TOKEN_SYMBOLS_MAP; diff --git a/tasks/upgradeSpokePool.ts b/tasks/upgradeSpokePool.ts deleted file mode 100644 index a9b4ebf9c..000000000 --- a/tasks/upgradeSpokePool.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { task } from "hardhat/config"; -import { HardhatRuntimeEnvironment } from "hardhat/types"; - -task("upgrade-spokepool", "Generate calldata to upgrade a SpokePool deployment") - .addParam("implementation", "New SpokePool implementation address") - .setAction(async function (args, hre: HardhatRuntimeEnvironment) { - const { implementation } = args; - if (!implementation) { - console.log("Usage: yarn hardhat upgrade-spokepool --implementation "); - return; - } - - const { ethers } = hre; - - const artifact = await hre.artifacts.readArtifact("SpokePool"); - - // @dev Any spoke pool's interface can be used here since they all should have the same upgradeTo function signature. - const abi = artifact.abi; - const spokePool = new ethers.Contract(implementation, abi); - - let calldata = ""; - - /** - * We perform this seemingly unnecessary pause/unpause sequence because we want to ensure that the - * upgrade is successful and the new implementation gets forwarded calls by the proxy contract as expected - * - * Since the upgrade and call happens atomically, the upgrade will revert if the new implementation - * is not functioning correctly. - */ - const data = spokePool.interface.encodeFunctionData("multicall", [ - [ - spokePool.interface.encodeFunctionData("pauseDeposits", [true]), - spokePool.interface.encodeFunctionData("pauseDeposits", [false]), - ], - ]); - - calldata = spokePool.interface.encodeFunctionData("upgradeToAndCall", [implementation, data]); - console.log( - `Call relaySpokePoolAdminFunction() with the params [, ${calldata}] on the hub pool from the owner's account.` - ); - }); diff --git a/tasks/utils.ts b/tasks/utils.ts deleted file mode 100644 index 785f4e69b..000000000 --- a/tasks/utils.ts +++ /dev/null @@ -1,203 +0,0 @@ -import assert from "assert"; -import { ethers } from "ethers"; -import readline from "readline"; -import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../utils/constants"; -import { TokenSymbol } from "./types"; - -export const zeroAddress = ethers.constants.AddressZero; - -export const minimalSpokePoolInterface = [ - { - inputs: [ - { internalType: "address", name: "l2Token", type: "address" }, - { internalType: "address", name: "l1Token", type: "address" }, - ], - name: "whitelistToken", - outputs: [], - stateMutability: "nonpayable", - type: "function", - }, - { - inputs: [ - { internalType: "address", name: "l2Token", type: "address" }, - { internalType: "address", name: "tokenBridge", type: "address" }, - ], - name: "setTokenBridge", - outputs: [], - 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) { - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - - return new Promise((resolve) => - rl.question(query, (ans) => { - rl.close(); - resolve(ans); - }) - ); -} - -export async function askYesNoQuestion(query: string): Promise { - const ans = (await askQuestion(`${query} (y/n) `)) as string; - if (ans.toLowerCase() === "y") return true; - if (ans.toLowerCase() === "n") return false; - return askYesNoQuestion(query); -} - -/** - * Given a token symbol from the HubPool chain and a remote chain ID, resolve the relevant token symbol and address. - */ -export function resolveTokenOnChain( - mainnetSymbol: string, - chainId: number -): { symbol: TokenSymbol; address: string } | undefined { - assert(isTokenSymbol(mainnetSymbol), `Unrecognised token symbol (${mainnetSymbol})`); - let symbol = mainnetSymbol as TokenSymbol; - - // Handle USDC special case where L1 USDC is mapped to different token symbols on L2s. - if (mainnetSymbol === "USDC") { - const symbols = ["USDC", "USDC.e", "USDbC", "USDzC"] as TokenSymbol[]; - const tokenSymbol = symbols.find((symbol) => TOKEN_SYMBOLS_MAP[symbol]?.addresses[chainId]); - if (!isTokenSymbol(tokenSymbol)) { - return; - } - symbol = tokenSymbol; - } else if (symbol === "DAI" && chainId === CHAIN_IDs.BLAST) { - symbol = "USDB"; - } - - const address = TOKEN_SYMBOLS_MAP[symbol].addresses[chainId]; - if (!address) { - return; - } - - return { symbol, address }; -} - -/** - * Given a token symbol, determine whether it is a valid key for the TOKEN_SYMBOLS_MAP object. - */ -export function isTokenSymbol(symbol: unknown): symbol is TokenSymbol { - return TOKEN_SYMBOLS_MAP[symbol as TokenSymbol] !== undefined; -} diff --git a/tasks/verifyBytecode.ts b/tasks/verifyBytecode.ts deleted file mode 100644 index 71bea5ead..000000000 --- a/tasks/verifyBytecode.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { task } from "hardhat/config"; -import type { HardhatRuntimeEnvironment } from "hardhat/types"; -import "hardhat-deploy"; -import "@nomiclabs/hardhat-ethers"; -import fs from "fs"; -import path from "path"; -import { execSync } from "child_process"; - -type VerifyBytecodeArgs = { - contract?: string; - txHash?: string; - libraries?: string; - broadcast?: string; -}; - -/** - * Best-effort parser for `foundry.toml` that extracts the default profile's `out` directory. - * Falls back to `/out` if anything goes wrong. - */ -function getFoundryOutDir(): string { - const root = process.cwd(); - const configPath = path.join(root, "foundry.toml"); - - if (!fs.existsSync(configPath)) { - return path.join(root, "out"); - } - - const contents = fs.readFileSync(configPath, "utf8"); - const lines = contents.split(/\r?\n/); - - let inDefaultProfile = false; - for (const rawLine of lines) { - const line = rawLine.trim(); - - if (line.startsWith("[") && line.endsWith("]")) { - // Enter or exit `[profile.default]` section. - inDefaultProfile = line === "[profile.default]"; - continue; - } - - if (!inDefaultProfile) continue; - - const match = line.match(/^out\s*=\s*"(.*)"\s*$/); - if (match) { - const configuredOut = match[1].trim(); - if (configuredOut.length > 0) { - return path.isAbsolute(configuredOut) ? configuredOut : path.join(root, configuredOut); - } - } - } - - // Default Foundry output directory. - return path.join(root, "out"); -} - -function normalizeFoundryBytecode(raw: any, key: "bytecode" | "deployedBytecode"): [string, any] { - const value = raw[key]; - if (!value) { - return ["0x", {}]; - } - - if (typeof value === "string") { - const linksKey = key === "bytecode" ? "linkReferences" : "deployedLinkReferences"; - const links = raw[linksKey] ?? {}; - return [value, links]; - } - - if (typeof value === "object") { - return [value.object ?? "0x", value.linkReferences ?? {}]; - } - - return ["0x", {}]; -} - -/** - * Load a Foundry artifact (`out/...json`) and adapt it into a Hardhat-style artifact - * that can be consumed by `ethers.getContractFactoryFromArtifact`. - */ -function loadFoundryArtifact(contractName: string): any { - const outDir = getFoundryOutDir(); - const candidates = [ - path.join(outDir, `${contractName}.sol`, `${contractName}.json`), - path.join(outDir, `${contractName}.json`), - ]; - - const artifactPath = candidates.find((p) => fs.existsSync(p)); - if (!artifactPath) { - throw new Error( - `Could not find Foundry artifact for contract "${contractName}". Tried:\n` + - candidates.map((p) => ` - ${p}`).join("\n") - ); - } - - const rawJson = fs.readFileSync(artifactPath, "utf8"); - const raw: any = JSON.parse(rawJson); - - const abi = raw.abi ?? []; - const [bytecode, linkReferences] = normalizeFoundryBytecode(raw, "bytecode"); - const [deployedBytecode, deployedLinkReferences] = normalizeFoundryBytecode(raw, "deployedBytecode"); - - return { - _format: "hh-foundry-compat-0", - contractName, - sourceName: raw.sourceName ?? raw.source_name ?? path.basename(artifactPath), - abi, - bytecode, - deployedBytecode, - linkReferences, - deployedLinkReferences, - }; -} - -function ensureForgeBuildArtifacts() { - try { - // This keeps Foundry's `out/` artifacts up to date when verifying Foundry deployments. - console.log("Running `forge build` to refresh Foundry artifacts..."); - execSync("forge build", { stdio: "inherit" }); - } catch (error: any) { - throw new Error(`forge build failed: ${error?.message ?? String(error)}`); - } -} - -/** -Verify that the deployment init code (creation bytecode + encoded constructor args) -matches the locally reconstructed init code from artifacts and recorded args. - -Compares keccak256(initCodeOnChain) vs keccak256(initCodeLocal). - -Sample usage: -yarn hardhat verify-bytecode --contract Arbitrum_Adapter --network mainnet -yarn hardhat verify-bytecode --contract Arbitrum_Adapter --tx-hash 0x... --network mainnet -yarn hardhat verify-bytecode --contract X --tx-hash 0x... --libraries "MyLib=0x...,OtherLib=0x..." --network mainnet - -For Foundry deployments that used `forge script --broadcast`, you can instead -point this task at the Foundry broadcast JSON: - -yarn hardhat verify-bytecode \ - --contract DstOFTHandler \ - --broadcast broadcast/DeployDstHandler.s.sol/999/run-latest.json \ - --network hyperevm - */ -task("verify-bytecode", "Verify deploy transaction input against local artifacts") - .addOptionalParam("contract", "Contract name; falls back to env CONTRACT") - // @dev For proxies, we don't save transactionHash in deployments/. You have to provide it manually via --tx-hash 0x... by checking e.g. block explorer first - .addOptionalParam("txHash", "Deployment transaction hash (defaults to deployments JSON)") - .addOptionalParam("libraries", "Libraries to link. JSON string or 'Name=0x..,Other=0x..'") - .addOptionalParam( - "broadcast", - "Path to Foundry broadcast JSON (e.g. broadcast/DeployFoo.s.sol/1/run-latest.json). " + - "If set, constructor args and default txHash are taken from this file instead of hardhat-deploy deployments." - ) - .setAction(async (args: VerifyBytecodeArgs, hre: HardhatRuntimeEnvironment) => { - const { deployments, ethers, artifacts, network } = hre; - - const useFoundryArtifacts = Boolean(args.broadcast); - - // For Hardhat deployments, make sure we're using latest local Hardhat artifacts. - if (!useFoundryArtifacts) { - await hre.run("compile"); - } else { - // For Foundry deployments, refresh Foundry's `out/` artifacts instead. - ensureForgeBuildArtifacts(); - } - - const contractName = args.contract || process.env.CONTRACT; - if (!contractName) throw new Error("Please provide --contract or set CONTRACT env var"); - - /** - * Resolve constructor args, deployed address and default tx hash either from: - * - hardhat-deploy deployments (default), or - * - Foundry broadcast JSON (when --broadcast is provided). - */ - let deployedAddress: string | undefined; - let constructorArgs: any[] = []; - let defaultTxHash: string | undefined; - - if (args.broadcast) { - const resolvedPath = path.isAbsolute(args.broadcast) ? args.broadcast : path.join(process.cwd(), args.broadcast); - - if (!fs.existsSync(resolvedPath)) { - throw new Error(`Broadcast file not found at path ${resolvedPath}`); - } - - // Narrow JSON structure to only what we need. - type BroadcastTx = { - hash?: string; - transactionType?: string; - contractName?: string; - contractAddress?: string; - arguments?: any[]; - transaction?: { - input?: string; - }; - }; - type BroadcastJson = { - transactions?: BroadcastTx[]; - }; - - const raw = fs.readFileSync(resolvedPath, "utf8"); - const parsed: BroadcastJson = JSON.parse(raw); - const txs = parsed.transactions || []; - - const createTxsForContract = txs.filter( - (tx) => tx.transactionType === "CREATE" && tx.contractName === contractName - ); - - if (!createTxsForContract.length) { - throw new Error(`No CREATE transaction for contract "${contractName}" found in broadcast file ${resolvedPath}`); - } - - let selected: BroadcastTx; - if (args.txHash) { - const match = createTxsForContract.find( - (tx) => tx.hash && tx.hash.toLowerCase() === args.txHash!.toLowerCase() - ); - if (!match) { - throw new Error( - `No CREATE transaction with hash ${args.txHash} for contract "${contractName}" in ${resolvedPath}` - ); - } - selected = match; - } else if (createTxsForContract.length === 1) { - selected = createTxsForContract[0]; - } else { - const hashes = createTxsForContract - .map((tx) => tx.hash) - .filter(Boolean) - .join(", "); - throw new Error( - `Multiple CREATE transactions for contract "${contractName}" found in ${resolvedPath}. ` + - `Please re-run with --tx-hash set to one of: ${hashes}` - ); - } - - if (!selected.hash) { - throw new Error(`Selected broadcast transaction for "${contractName}" is missing a tx hash`); - } - - deployedAddress = selected.contractAddress; - constructorArgs = selected.arguments || []; - defaultTxHash = selected.hash; - } else { - const deployment = await deployments.get(contractName); - deployedAddress = deployment.address; - constructorArgs = deployment.args || []; - defaultTxHash = deployment.transactionHash; - } - - const parseLibraries = (s?: string): Record => { - if (!s) return {}; - const out: Record = {}; - const trimmed = s.trim(); - if (trimmed.startsWith("{") && trimmed.endsWith("}")) { - const parsed = JSON.parse(trimmed); - for (const [k, v] of Object.entries(parsed)) out[k] = String(v); - return out; - } - for (const part of trimmed.split(/[\,\n]/)) { - const [k, v] = part.split("=").map((x) => x.trim()); - if (k && v) out[k] = v; - } - return out; - }; - - // Read local compilation artifact (Hardhat or Foundry) for reconstructing init code. - const artifact = useFoundryArtifacts - ? loadFoundryArtifact(contractName) - : await artifacts.readArtifact(contractName); - console.log( - "Reading compilation artifact for", - (artifact as any).sourceName ?? (useFoundryArtifacts ? "" : "") - ); - - /** - * TODO - * the `libraries` bit is untested. Could be wrong. Could remove this part if we don't have contracts with dynamic libraries - * artifact.linkReferences might help solve this better. Also, deployments.libraries. Implement only if required later. - */ - const libraries: Record = parseLibraries(args.libraries); - const factory = await ethers.getContractFactoryFromArtifact( - artifact, - Object.keys(libraries).length ? { libraries } : {} - ); - - // Note: `factory.getDeployTransaction` populates the transaction with whatever data we WOULD put in it if we were deploying it right now - const populatedDeployTransaction = factory.getDeployTransaction(...constructorArgs); - const expectedInit: string = ethers.utils.hexlify(populatedDeployTransaction.data!).toLowerCase(); - if (!expectedInit || expectedInit === "0x") { - throw new Error("Failed to reconstruct deployment init code from local artifacts"); - } - - // Get on-chain creation input - const txHash = args.txHash ?? defaultTxHash; - if (!txHash) { - throw new Error( - "Could not find deployment tx hash. Pass --tx-hash when running script, " + - "or ensure deployments / broadcast metadata includes it." - ); - } - const tx = await ethers.provider.getTransaction(txHash); - if (!tx) throw new Error(`Transaction not found for hash ${txHash}`); - if (tx.to && tx.to != "") { - throw new Error(`Transaction ${txHash} is not a direct contract creation (tx.to=${tx.to})`); - } - - const expectedHash = ethers.utils.keccak256(expectedInit); - const onchainHash = ethers.utils.keccak256(tx.data.toLowerCase()); - - console.log("\n=============== Deploy Tx Verification ==============="); - console.log(`Contract : ${contractName}`); - console.log(`Network : ${network.name}`); - if (deployedAddress) { - console.log(`Deployed address : ${deployedAddress}`); - } - if (args.broadcast) { - console.log(`Broadcast file : ${args.broadcast}`); - } - if (txHash) console.log(`Tx hash : ${txHash}`); - console.log("-------------------------------------------------------"); - console.log(`On-chain init hash : ${onchainHash}`); - console.log(`Local init hash : ${expectedHash}`); - console.log("-------------------------------------------------------"); - console.log(onchainHash === expectedHash ? "✅ MATCH" : "❌ MISMATCH – init code differs"); - console.log("=======================================================\n"); - }); diff --git a/tasks/verifySpokePool.ts b/tasks/verifySpokePool.ts deleted file mode 100644 index 101d92eca..000000000 --- a/tasks/verifySpokePool.ts +++ /dev/null @@ -1,95 +0,0 @@ -import assert from "assert"; -import { task } from "hardhat/config"; -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { getDeployedAddress } from "../src/DeploymentUtils"; -import { CHAIN_IDs, MAINNET_CHAIN_IDs, PRODUCTION_NETWORKS, TOKEN_SYMBOLS_MAP } from "../utils/constants"; - -const { MAINNET, SEPOLIA } = CHAIN_IDs; -const TEXT_PADDING = 36; -const CHAIN_PADDING = Object.values(MAINNET_CHAIN_IDs) - .sort((x, y) => x - y) - .pop() - .toString().length; -const formatChainId = (chainId: number): string => chainId.toString().padStart(CHAIN_PADDING, " "); - -task("verify-spokepool", "Verify the configuration of a deployed SpokePool") - .addFlag("routes", "Dump deposit route history (if any)") - .setAction(async function (args, hre: HardhatRuntimeEnvironment) { - const { deployments, ethers, companionNetworks, getChainId, network } = hre; - - const bytes32ToAddress = (bytestring: string) => { - assert(bytestring.slice(2, 26) === "000000000000000000000000"); - return ethers.utils.getAddress(bytestring.slice(0, 2).concat(bytestring.slice(-40))); - }; - - const spokeChainId = parseInt(await getChainId()); - const hubChainId = Object.values(MAINNET_CHAIN_IDs).includes(spokeChainId) ? MAINNET : SEPOLIA; - - const spokeAddress = getDeployedAddress("SpokePool", spokeChainId, true); - const hubAddress = getDeployedAddress("HubPool", hubChainId, true); - - const provider = new ethers.providers.StaticJsonRpcProvider(network.config.url); - - // Initialize contracts. Only generic SpokePool functions are used, so Ethereum_SpokePool is OK. - const { abi } = await deployments.get("Ethereum_SpokePool"); - const spokePool = new ethers.Contract(spokeAddress, abi, provider); - - const spokeFunctions = [ - "chainId", - "getCurrentTime", - "depositQuoteTimeBuffer", - "fillDeadlineBuffer", - "wrappedNativeToken", - "withdrawalRecipient", - "crossDomainAdmin", - ] as const; - const multicall = await spokePool.callStatic.multicall( - spokeFunctions.map((fn) => spokePool.interface.encodeFunctionData(fn)) - ); - const results = Object.fromEntries(spokeFunctions.map((fn, idx) => [fn, multicall[idx]])); - - // Log state from SpokePool - const originChainId = Number(results.chainId); - console.log("SpokePool.chainId()".padEnd(TEXT_PADDING) + ": " + originChainId); - assert(Number(originChainId) === spokeChainId, `${originChainId} != ${spokeChainId}`); - - const currentTime = Number(results.getCurrentTime); - const formattedTime = `${currentTime} (${new Date(Number(currentTime) * 1000).toUTCString()})`; - console.log("SpokePool.getCurrentTime()".padEnd(TEXT_PADDING) + ": " + formattedTime); - - const quoteTimeBuffer = Number(results.depositQuoteTimeBuffer); - console.log("SpokePool.depositQuoteTimeBuffer()".padEnd(TEXT_PADDING) + ": " + quoteTimeBuffer); - - const fillDeadlineBuffer = Number(results.fillDeadlineBuffer); - console.log("SpokePool.fillDeadlineBuffer()".padEnd(TEXT_PADDING) + ": " + fillDeadlineBuffer); - - const wrappedNative = bytes32ToAddress(results.wrappedNativeToken); - const nativeTokenSymbol = PRODUCTION_NETWORKS[spokeChainId].nativeToken; - const wrappedPrefix = "W"; - const wrappedNativeSymbol = `${wrappedPrefix}${nativeTokenSymbol}`; - const expectedWrappedNative = TOKEN_SYMBOLS_MAP[wrappedNativeSymbol].addresses[spokeChainId]; - assert(wrappedNative === expectedWrappedNative, `wrappedNativeToken: ${wrappedNative} != ${expectedWrappedNative}`); - console.log("SpokePool.wrappedNativeToken()".padEnd(TEXT_PADDING) + ": " + wrappedNative); - - const withdrawalRecipient = bytes32ToAddress(results.withdrawalRecipient); - console.log("SpokePool.withdrawalRecipient()".padEnd(TEXT_PADDING) + ": " + withdrawalRecipient); - assert(withdrawalRecipient === hubAddress, `withdrawalRecipient: ${withdrawalRecipient} != ${hubAddress}`); - - const crossDomainAdmin = bytes32ToAddress(results.crossDomainAdmin); - console.log("SpokePool.crossDomainAdmin()".padEnd(TEXT_PADDING) + ": " + crossDomainAdmin); - assert(crossDomainAdmin === hubAddress, `${crossDomainAdmin} != ${hubAddress}`); - - // Log EnabledDepositRoute on SpokePool to test that L1 message arrived to L2: - const filter = spokePool.filters.EnabledDepositRoute(); - const events = await spokePool.queryFilter(filter); - - console.log("Deposit routes".padEnd(TEXT_PADDING) + ": " + events.length); - if (events.length > 0 && args.routes) { - console.log(`\nDeposit routes:`); - events.reverse().forEach(({ args, blockNumber, transactionHash }) => { - const dstChainId = formatChainId(args.destinationChainId); - const enabled = args.enabled.toString().padStart(5); - console.log(`${blockNumber} ${args.originToken} -> ${dstChainId} ${enabled} (${transactionHash})`); - }); - } - });