From 6b079c7e24acf076097265f609e6c5c866c0e471 Mon Sep 17 00:00:00 2001 From: rplusq Date: Fri, 10 Jul 2026 19:13:29 +0100 Subject: [PATCH 1/2] fix(deploy): resolve WCT proxy from artifact, not recomputed CREATE2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WCTDeploy.upgradeToL2WCT/upgradeToWCT recomputed the proxy CREATE2 address using ADMIN_ADDRESS as the initialOwner, but the proxy address depends on initialOwner + init calldata — which differ from what other deploy paths deployed (OptimismDeploy uses the admin timelock as owner). The computed address could point at a codeless phantom. Read the deployed proxy from the persisted artifact instead (OP vs Ethereum, mirroring _updateDeploymentJson), and assert it has code. Removes the unused _computeLegacyAddress + TransparentUpgradeableProxy import. Co-Authored-By: Claude Opus 4.8 (1M context) --- evm/script/deploy/WCTDeploy.s.sol | 52 +++++++++++++------------------ 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/evm/script/deploy/WCTDeploy.s.sol b/evm/script/deploy/WCTDeploy.s.sol index c7d6a76..7a08486 100644 --- a/evm/script/deploy/WCTDeploy.s.sol +++ b/evm/script/deploy/WCTDeploy.s.sol @@ -8,7 +8,6 @@ import { WCT } from "src/WCT.sol"; import { BaseScript, EthereumDeployments, OptimismDeployments } from "script/Base.s.sol"; import { Eip1967Logger } from "script/utils/Eip1967Logger.sol"; import { DeploymentJsonWriter } from "script/utils/DeploymentJsonWriter.sol"; -import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { Options } from "openzeppelin-foundry-upgrades/Options.sol"; import { newL2WCT } from "script/helpers/Proxy.sol"; @@ -78,7 +77,10 @@ contract WCTDeploy is BaseScript { function upgradeToL2WCT() public broadcast { LegacyDeploymentParams memory params = _readDeploymentParamsFromEnv(); - address legacyAddress = _computeLegacyAddress(params.salt); + // Read the deployed proxy from the persisted artifact rather than recomputing its CREATE2 address + // (which depends on the proxy's initialOwner/init-calldata and diverges from what other deploy + // paths actually deployed). + address legacyAddress = _readDeployedProxy(); // Set up upgrade options Options memory opts; @@ -104,8 +106,8 @@ contract WCTDeploy is BaseScript { } function upgradeToWCT() public broadcast { - LegacyDeploymentParams memory params = _readDeploymentParamsFromEnv(); - address legacyAddress = _computeLegacyAddress(params.salt); + // Read the deployed proxy from the persisted artifact rather than recomputing its CREATE2 address. + address legacyAddress = _readDeployedProxy(); // Set up upgrade options Options memory opts; @@ -128,33 +130,21 @@ contract WCTDeploy is BaseScript { } } - function _computeLegacyAddress(bytes32 salt) internal view returns (address) { - // First compute implementation address - bytes memory bytecode = abi.encodePacked(type(LegacyL2WCT).creationCode); - bytes32 initCodeHash = keccak256(bytecode); - address implementation = vm.computeCreate2Address(salt, initCodeHash); - console2.log("Implementation address:", implementation); - - // Then compute proxy address - bytecode = abi.encodePacked( - type(TransparentUpgradeableProxy).creationCode, - abi.encode( - implementation, - vm.envAddress("ADMIN_ADDRESS"), - abi.encodeCall( - LegacyL2WCT.initialize, - LegacyL2WCT.Init({ - initialAdmin: vm.envAddress("ADMIN_ADDRESS"), - initialManager: vm.envAddress("MANAGER_ADDRESS"), - bridge: vm.envAddress("OP_BRIDGE_ADDRESS"), - remoteToken: vm.envAddress("REMOTE_TOKEN_ADDRESS") - }) - ) - ) - ); - initCodeHash = keccak256(bytecode); - - return vm.computeCreate2Address(salt, initCodeHash); + /// @dev Resolve the deployed WCT/L2WCT proxy from the persisted deployment JSON, which is the source of + /// truth. Recomputing the CREATE2 address is unsafe: the proxy address depends on its initialOwner and + /// init calldata, and those differ across deploy paths (e.g. OptimismDeploy uses the admin timelock as + /// owner, WCTDeploy used ADMIN_ADDRESS), so recomputation can silently point at a codeless phantom + /// address. Reads the JSON (`deployments/.json`) rather than the binary artifact, because some + /// chains (e.g. Arbitrum, Base) only ship the JSON. Mirrors the OP-vs-Ethereum branching of _updateDeploymentJson. + function _readDeployedProxy() internal returns (address proxy) { + string memory path = + string.concat(vm.projectRoot(), "/deployments/", vm.toString(block.chainid), ".json"); + require(vm.exists(path), "WCTDeploy: no deployment JSON for chain"); + string memory json = vm.readFile(path); + string memory key = _isOpSuperchain(block.chainid) ? ".L2WCT.address" : ".WCT.address"; + proxy = vm.parseJsonAddress(json, key); + require(proxy != address(0), "WCTDeploy: no deployed proxy recorded in artifact"); + require(proxy.code.length > 0, "WCTDeploy: artifact proxy address has no code"); } function _readDeploymentParamsFromEnv() private view returns (LegacyDeploymentParams memory) { From 00a79f3b13a0da6b84d5a4257db700e56339a5d7 Mon Sep 17 00:00:00 2001 From: rplusq Date: Tue, 14 Jul 2026 21:32:59 +0100 Subject: [PATCH 2/2] fix(deploy): read only SUPERCHAIN_BRIDGE_ADDRESS in upgradeToL2WCT upgradeToL2WCT loaded the full LegacyDeploymentParams, so a pure upgrade would revert on unrelated unset env vars (ADMIN_ADDRESS, OP_BRIDGE_ADDRESS, ...). Read the one var it uses directly. Also drop the now-dead salt and superchainBridge fields from the struct so run() only requires the vars it actually consumes. Co-Authored-By: Claude Opus 4.8 (1M context) --- evm/script/deploy/WCTDeploy.s.sol | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/evm/script/deploy/WCTDeploy.s.sol b/evm/script/deploy/WCTDeploy.s.sol index 7a08486..2912ead 100644 --- a/evm/script/deploy/WCTDeploy.s.sol +++ b/evm/script/deploy/WCTDeploy.s.sol @@ -16,9 +16,7 @@ struct LegacyDeploymentParams { address admin; address manager; address bridge; - address superchainBridge; address remoteToken; - bytes32 salt; } contract WCTDeploy is BaseScript { @@ -76,7 +74,10 @@ contract WCTDeploy is BaseScript { } function upgradeToL2WCT() public broadcast { - LegacyDeploymentParams memory params = _readDeploymentParamsFromEnv(); + // Only the superchain bridge is needed for this upgrade; read it directly rather than loading the + // full deployment params, which would revert on unrelated unset env vars (ADMIN_ADDRESS, + // OP_BRIDGE_ADDRESS, …) that a pure upgrade has no reason to set. + address superchainBridge = vm.envAddress("SUPERCHAIN_BRIDGE_ADDRESS"); // Read the deployed proxy from the persisted artifact rather than recomputing its CREATE2 address // (which depends on the proxy's initialOwner/init-calldata and diverges from what other deploy // paths actually deployed). @@ -95,7 +96,7 @@ contract WCTDeploy is BaseScript { L2WCT upgraded = L2WCT(legacyAddress); - upgraded.setBridge(params.superchainBridge); + upgraded.setBridge(superchainBridge); console2.log("Upgraded LegacyL2WCT to L2WCT at:", address(upgraded)); @@ -152,9 +153,7 @@ contract WCTDeploy is BaseScript { admin: vm.envAddress("ADMIN_ADDRESS"), manager: vm.envAddress("MANAGER_ADDRESS"), bridge: vm.envAddress("OP_BRIDGE_ADDRESS"), - superchainBridge: vm.envAddress("SUPERCHAIN_BRIDGE_ADDRESS"), - remoteToken: vm.envAddress("REMOTE_TOKEN_ADDRESS"), - salt: keccak256(abi.encodePacked("walletconnect.l2wct")) + remoteToken: vm.envAddress("REMOTE_TOKEN_ADDRESS") }); } }