From 1b597fd29c9a79db42179680f9c5a1c19cbcaf1d Mon Sep 17 00:00:00 2001 From: Jack Xu Date: Tue, 3 Mar 2026 16:15:18 +0800 Subject: [PATCH 1/2] fix: recover missing verification JSONs after interrupted CI When a contract already exists on-chain but the verification standard-json-input file is missing (e.g. CI interrupted after deploy but before file write), the early return in __deploy() now detects the missing file and regenerates it. Changes: - Extract _getStandardJsonInputPath() helper to DRY path construction - Add verification JSON recovery logic to __deploy() early return block - Add regression test with VerificationRecoveryHelper mock --- src/DeployHelper.sol | 37 ++++++++++++++++--- test/DeployHelper.t.sol | 80 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/src/DeployHelper.sol b/src/DeployHelper.sol index a60a60e..653ea7c 100644 --- a/src/DeployHelper.sol +++ b/src/DeployHelper.sol @@ -466,6 +466,21 @@ abstract contract DeployHelper is CreateXHelper { console.log( unicode"⚠️[WARN] Skipping deployment, %s already deployed at %s", versionAndVariant, computed ); + + // Persist verification JSON if missing (e.g. after interrupted CI runs) + if (!_shouldSkipStandardJsonInput()) { + string memory outputPath = _getStandardJsonInputPath(subfolder, versionAndVariant); + if (!vm.isFile(outputPath)) { + string memory sjInput = _generateStandardJsonInput(name); + if (bytes(sjInput).length > 0) { + console.log( + unicode"🔄[INFO] Recovering missing verification input for %s", versionAndVariant + ); + _saveContractToStandardJsonInput(versionAndVariant, subfolder, sjInput); + } + } + } + return (false, computed); } @@ -780,6 +795,22 @@ abstract contract DeployHelper is CreateXHelper { return ""; } + /** + * @notice Construct the standard JSON input file path for a given version and subfolder + * @param subfolder Deployment category + * @param versionAndVariant Version string for file naming + * @return path Full file path to the standard JSON input file + */ + function _getStandardJsonInputPath(string memory subfolder, string memory versionAndVariant) + internal + view + returns (string memory) + { + return string.concat( + vm.projectRoot(), "/deployments/", subfolder, "/standard-json-inputs/", versionAndVariant, ".json" + ); + } + /** * @notice Save contract verification JSON to file * @param versionAndVariant Version string for file naming @@ -796,7 +827,7 @@ abstract contract DeployHelper is CreateXHelper { string memory outputDir = string.concat(vm.projectRoot(), "/deployments/", subfolder, "/standard-json-inputs"); vm.createDir(outputDir, true); - string memory outputPath = string.concat(outputDir, "/", versionAndVariant, ".json"); + string memory outputPath = _getStandardJsonInputPath(subfolder, versionAndVariant); if (vm.isFile(outputPath) && _FORCE_DEPLOY) { outputPath = string.concat( @@ -832,9 +863,7 @@ abstract contract DeployHelper is CreateXHelper { string memory subfolder, string memory standardJsonInput ) internal view virtual { - string memory outputPath = string.concat( - vm.projectRoot(), "/deployments/", subfolder, "/standard-json-inputs/", versionAndVariant, ".json" - ); + string memory outputPath = _getStandardJsonInputPath(subfolder, versionAndVariant); if (vm.isFile(outputPath)) { console.log( diff --git a/test/DeployHelper.t.sol b/test/DeployHelper.t.sol index eb39acd..96bdc67 100644 --- a/test/DeployHelper.t.sol +++ b/test/DeployHelper.t.sol @@ -400,6 +400,43 @@ contract RealStandardJsonCheckHelper is DeployHelper { } } +// Helper for testing verification JSON recovery after interrupted CI +contract VerificationRecoveryHelper is DeployHelper { + string internal constant MOCK_STANDARD_JSON = + '{"language":"Solidity","sources":{},"settings":{"optimizer":{"enabled":true}}}'; + + function setUp() public override { + _setUp("test"); + } + + function _getPostDeployInitData() internal virtual override returns (bytes memory) { + return abi.encodeWithSignature("initializeOwner(address)", _deployer); + } + + function deployMockContract() public returns (address) { + bytes memory creationCode = abi.encodePacked(type(MockContract).creationCode, abi.encode(_getEvmSuffix())); + return deploy(creationCode); + } + + function _shouldSkipStandardJsonInput() internal pure override returns (bool) { + return false; + } + + // Return deterministic JSON without FFI + function _generateStandardJsonInput(string memory) internal pure override returns (string memory) { + return MOCK_STANDARD_JSON; + } + + function getStandardJsonPath() public view returns (string memory) { + string memory versionAndVariant = string.concat("1.0.0-MockContract", _getEvmSuffix()); + return _getStandardJsonInputPath(deploymentCategory, versionAndVariant); + } + + function getMockStandardJson() public pure returns (string memory) { + return MOCK_STANDARD_JSON; + } +} + contract DeployHelperTest is Test { TestDeployHelper public helper; address public deployer; @@ -1222,6 +1259,49 @@ contract DeployHelperTest is Test { vm.setEnv("FORCE_DEPLOY", originalForceDeploy); } + function test_VerificationJsonRecoveredAfterInterruptedCI() public { + // Save original env vars + string memory originalSkipStandardJson = vm.envOr("SKIP_STANDARD_JSON_INPUT", string("true")); + + vm.setEnv("SKIP_STANDARD_JSON_INPUT", "false"); + + VerificationRecoveryHelper recoveryHelper = new VerificationRecoveryHelper(); + recoveryHelper.setUp(); + + string memory outputPath = recoveryHelper.getStandardJsonPath(); + + // Clean up any pre-existing file + if (vm.isFile(outputPath)) vm.removeFile(outputPath); + + // Step 1: Deploy the contract (standard JSON saved to disk) + recoveryHelper.deployMockContract(); + assertTrue(vm.isFile(outputPath), "Standard JSON should exist after first deploy"); + + // Step 2: Delete the verification JSON (simulating interrupted CI) + vm.removeFile(outputPath); + assertFalse(vm.isFile(outputPath), "Standard JSON should be deleted"); + + // Step 3: Re-deploy the same contract (already exists on-chain, early return) + recoveryHelper.deployMockContract(); + + // Step 4: Assert the verification JSON was recovered + assertTrue(vm.isFile(outputPath), "Standard JSON should be recovered after re-deploy"); + + // Verify content matches the mock + string memory recovered = vm.readFile(outputPath); + assertEq( + keccak256(bytes(recovered)), + keccak256(bytes(recoveryHelper.getMockStandardJson())), + "Recovered JSON content should match" + ); + + // Clean up + if (vm.isFile(outputPath)) vm.removeFile(outputPath); + + // Restore original env vars + vm.setEnv("SKIP_STANDARD_JSON_INPUT", originalSkipStandardJson); + } + function test_MainnetChainMapping_O1Lookup() public { // Test that mainnet check uses mapping for O(1) lookup // This is verified through gas usage comparison From f3b4a851777d5bd71ef6f506fd3739600a730d9b Mon Sep 17 00:00:00 2001 From: Jack Xu Date: Tue, 3 Mar 2026 16:43:24 +0800 Subject: [PATCH 2/2] fix: isolate recovery test to avoid cross-test snapshot interference Use a dedicated "recovery-test" subfolder for VerificationRecoveryHelper to prevent Forge's vm.revertToState() from restoring stale file-system state (corrupted standard-json-input files) across test boundaries. --- test/DeployHelper.t.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/DeployHelper.t.sol b/test/DeployHelper.t.sol index 96bdc67..1b4b555 100644 --- a/test/DeployHelper.t.sol +++ b/test/DeployHelper.t.sol @@ -400,13 +400,15 @@ contract RealStandardJsonCheckHelper is DeployHelper { } } -// Helper for testing verification JSON recovery after interrupted CI +// Helper for testing verification JSON recovery after interrupted CI. +// Uses a dedicated subfolder ("recovery-test") to avoid file-system interference +// with other tests through Forge's vm.revertToState() snapshot mechanism. contract VerificationRecoveryHelper is DeployHelper { string internal constant MOCK_STANDARD_JSON = '{"language":"Solidity","sources":{},"settings":{"optimizer":{"enabled":true}}}'; function setUp() public override { - _setUp("test"); + _setUp("recovery-test"); } function _getPostDeployInitData() internal virtual override returns (bytes memory) { @@ -1295,8 +1297,11 @@ contract DeployHelperTest is Test { "Recovered JSON content should match" ); - // Clean up + // Clean up standard JSON and deployment artifacts from the isolated subfolder if (vm.isFile(outputPath)) vm.removeFile(outputPath); + string memory latestPath = + string.concat(vm.projectRoot(), "/deployments/recovery-test/", vm.toString(block.chainid), "-latest.json"); + if (vm.isFile(latestPath)) vm.removeFile(latestPath); // Restore original env vars vm.setEnv("SKIP_STANDARD_JSON_INPUT", originalSkipStandardJson);