Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions src/DeployHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
85 changes: 85 additions & 0 deletions test/DeployHelper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,45 @@ contract RealStandardJsonCheckHelper is DeployHelper {
}
}

// 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("recovery-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;
Expand Down Expand Up @@ -1222,6 +1261,52 @@ 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 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);
}

function test_MainnetChainMapping_O1Lookup() public {
// Test that mainnet check uses mapping for O(1) lookup
// This is verified through gas usage comparison
Expand Down