Skip to content

Conversation

@clement-ux
Copy link
Contributor

@clement-ux clement-ux commented Jan 23, 2026

Overview

Major refactoring of the deployment framework to improve speed, scalability, and developer experience. This transforms the deployment system from a basic script-based approach to a sophisticated, maintainable framework.

71 files changed | +3,733 lines | -2,687 lines

Motivation

The old deployment process had several limitations that made it difficult to maintain as the number of deployment scripts grew:

1. Manual Script Registration

Every new deployment script required:

  • Manual import at the top of DeployManager.sol
  • Manual instantiation in the run() function
  • Risk of forgetting to add it (there was even a TODO: "Use vm.readDir to recursively build this?")
// OLD: 15+ manual imports and instantiations
import {UpgradeLidoARMMainnetScript} from "./mainnet/003_UpgradeLidoARMScript.sol";
// ... more imports

function run() external {
    _runDeployFile(new UpgradeLidoARMMainnetScript());
    // ... more manual calls
}

2. Inefficient JSON Serialization

The old approach read and wrote JSON line-by-line, which was:

  • Slow (multiple file reads per execution)
  • Error-prone (had a warning comment about "EOF while parsing" bugs)
  • Data lossy (each vm.serializeUint call overwrote previous data)

3. AddressResolver Recreated Per Script

A new AddressResolver was instantiated in every script's run() function, wasting gas and setup time.

4. Address Passing via Constructor

Scripts had to receive addresses as constructor parameters, creating tight coupling:

// OLD: Complex constructor dependencies
_runDeployFile(new UpgradeOriginARMScript(
    getDeployedAddressInBuild("HARVESTER"),
    getDeployedAddressInBuild("ORIGIN_ARM"),
    getDeployedAddressInBuild("SILO_VARLAMORE_S_MARKET")
));

Key Changes

1. Dynamic Script Discovery

Scripts are now auto-discovered from chain-specific folders (mainnet/, sonic/). No more manual registration.

// NEW: Automatic discovery
VmSafe.DirEntry[] memory files = vm.readDir(path);
for (uint256 i; i < resultSize; i++) {
    _runDeployFile(address(vm.deployCode(contractName)));
}

2. Struct-Based JSON with Arrays

JSON is now parsed once using struct deserialization, and written once at the end:

// NEW: Fast struct parsing
Root memory root = abi.decode(vm.parseJson(deployment), (Root));

New JSON format:

{
  "contracts": [{"name": "LIDO_ARM", "implementation": "0x..."}],
  "executions": [{"name": "003_UpgradeLidoARMScript", "timestamp": 123}]
}

3. Persistent Resolver at Deterministic Address

A single Resolver contract is deployed via vm.etch to a deterministic address. All scripts access the same instance:

// Deterministic address: keccak256("Resolver")
Resolver internal resolver = Resolver(address(uint160(uint256(keccak256("Resolver")))));

4. Resolver-Based Address Lookups

Scripts now query the Resolver directly - no constructor params needed:

// NEW: Simple lookups
address proxy = resolver.implementations("LIDO_ARM");

5. Standardized Deployment Lifecycle

New AbstractDeployScript provides a clean lifecycle with override hooks:

  • _execute() - Deploy contracts, register them
  • _buildGovernanceProposal() - Define governance actions
  • _fork() - Post-deployment verification

6. Multi-Chain Support

Built-in support for multiple chains via folder-based organization:

  • Chain ID 1 (Mainnet) → script/deploy/mainnet/
  • Chain ID 146 (Sonic) → script/deploy/sonic/

Benefits

Aspect Before After
Adding new script Manual import + instantiation Just add file to folder
JSON operations Multiple reads per script Single read at start, single write at end
Address resolution Constructor params or file read O(1) Resolver lookup
Resolver creation New instance per script Single persistent instance
Script organization All imports in one file Folder-based, auto-discovered
Governance support Ad-hoc Built-in with simulation

CI Improvements

The refactoring also improves CI pipeline performance by parallelizing test execution across multiple chains. Fork tests now run concurrently for each supported chain (Mainnet, Sonic), reducing overall CI run time.

New Architecture

DeployManager.setUp()
  ├── Determine state (FORK_TEST, FORK_DEPLOYING, REAL_DEPLOYING)
  ├── Load/create deployment JSON
  └── Deploy Resolver to deterministic address

DeployManager.run()
  ├── _preDeployment() → Load JSON into Resolver
  ├── Read scripts from chain folder
  ├── For each script: _runDeployFile()
  │   ├── Check skip() / proposalExecuted()
  │   ├── Run script.run() if not in history
  │   └── Or call handleGovernanceProposal() if already deployed
  └── _postDeployment() → Save Resolver data to JSON

AbstractDeployScript.run()
  ├── Get state from Resolver
  ├── Start broadcast/prank
  ├── Execute _execute()
  ├── Stop broadcast/prank
  ├── Store contracts in Resolver
  ├── Build & handle governance proposal
  └── Run _fork() for verification

How to Write a New Deployment Script

  1. Create file: script/deploy/mainnet/NNN_YourScript.s.sol
  2. Follow the naming convention:
    • File: NNN_DescriptiveName.s.sol
    • Contract: $NNN_DescriptiveName
    • Constructor: "NNN_DescriptiveName"
contract $017_MyUpgrade is AbstractDeployScript("017_MyUpgrade") {
    using GovHelper for GovProposal;

    bool public constant override skip = false;
    bool public constant override proposalExecuted = false;

    MyContract public newImpl;

    function _execute() internal override {
        // Look up existing contracts
        address proxy = resolver.implementations("MY_CONTRACT");

        // Deploy new contracts
        newImpl = new MyContract();
        _recordDeployment("MY_CONTRACT_IMPL", address(newImpl));
    }

    function _buildGovernanceProposal() internal override {
        govProposal.setDescription("Upgrade MyContract");
        govProposal.action(
            resolver.implementations("MY_CONTRACT"),
            "upgradeTo(address)",
            abi.encode(address(newImpl))
        );
    }

    function _fork() internal override {
        // Verify deployment
    }
}

See script/deploy/mainnet/000_Example.s.sol for a complete template.

@clement-ux clement-ux marked this pull request as ready for review January 23, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants