Automatic Multi-Network Deployment System

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Automatic Multi-Network Deployment System
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1217
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1046
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Development of Multichain Automatic Deployment System

The problem emerges on the third or fourth deployment of same protocol to different networks: someone deployed to Arbitrum with different constant value, Base used different OpenZeppelin version, proxy addresses weren't saved properly, and now it's unclear what's deployed where. Multichain auto-deploy solves exactly this — reproducibility and deployment tracking.

Deterministic Addresses via CREATE2

Key requirement for multichain deployment: same addresses across all networks. This simplifies user experience, documentation, and cross-chain integration.

CREATE2 allows computing contract address before deployment:

address = keccak256(0xff ++ deployerAddress ++ salt ++ keccak256(bytecode))[12:]

If deployer has same address on all EVM networks (via Nick's Factory or own deployer via deterministicDeploy), bytecode is same, salt is same — address will be same everywhere.

Foundry supports this natively:

// deploy script
function run() external {
    bytes32 salt = keccak256("MyProtocol_v1.0.0");
    
    vm.broadcast();
    address proxy = factory.deployProxy(
        address(implementation),
        salt,
        abi.encodeCall(MyContract.initialize, (owner, params))
    );
    
    console.log("Deployed at:", proxy);
    console.log("Chain:", block.chainid);
}
# Deploy to multiple networks
forge script script/Deploy.s.sol --rpc-url arbitrum --broadcast
forge script script/Deploy.s.sol --rpc-url base --broadcast
forge script script/Deploy.s.sol --rpc-url optimism --broadcast

Problem: if bytecode differs between networks (e.g., hardcoded chainId or address), CREATE2 address will be different. Must avoid compile-time constant addresses in bytecode.

Deployment System Structure

Network Configuration

Single source of truth for all networks in deploy.config.ts:

export interface NetworkConfig {
  chainId: number;
  rpcUrl: string;
  deployer: string;         // deployer address
  gasPrice?: bigint;        // override for unstable gas networks
  confirmations: number;    // blocks to wait
  verifier?: "etherscan" | "blockscout" | "none";
  verifierUrl?: string;
  nativeCurrency: string;
  contracts: {
    // addresses of dependent contracts in this network
    usdc?: string;
    weth?: string;
    uniswapRouter?: string;
  };
}

export const networks: Record<string, NetworkConfig> = {
  arbitrum: {
    chainId: 42161,
    rpcUrl: process.env.ARBITRUM_RPC!,
    deployer: DEPLOYER_ADDRESS,
    confirmations: 1,
    verifier: "etherscan",
    nativeCurrency: "ETH",
    contracts: {
      usdc: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
      weth: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
    },
  },
  base: {
    chainId: 8453,
    rpcUrl: process.env.BASE_RPC!,
    deployer: DEPLOYER_ADDRESS,
    confirmations: 1,
    verifier: "blockscout",
    verifierUrl: "https://base.blockscout.com/api",
    nativeCurrency: "ETH",
    contracts: {
      usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      weth: "0x4200000000000000000000000000000000000006",
    },
  },
};

Artifacts and State Management

After each deployment need to save contract addresses. Foundry saves to broadcast/ directory, but inconvenient for multichain tracking.

Better approach — generate deployments.json:

interface DeploymentRecord {
  network: string;
  chainId: number;
  contracts: Record<string, {
    address: string;
    implementationAddress?: string;
    abi: string;            // IPFS hash or path
    deployedAt: number;     // block number
    txHash: string;
    version: string;        // semver
  }>;
  deployedBy: string;
  timestamp: string;
}

This file is committed to repository — single source of truth for addresses. CI/CD updates it after each deployment.

Deployment Script with Retry and Verification

import { createPublicClient, createWalletClient, http } from "viem";

async function deployWithRetry(
  network: NetworkConfig,
  contractName: string,
  deployFn: () => Promise<`0x${string}`>,
  maxRetries = 3
): Promise<`0x${string}`> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const address = await deployFn();
      
      // Wait for confirmations
      const client = createPublicClient({ transport: http(network.rpcUrl) });
      await client.waitForTransactionReceipt({
        hash: address,
        confirmations: network.confirmations,
      });
      
      console.log(`✓ ${contractName} on ${network.chainId}: ${address}`);
      return address;
    } catch (err) {
      if (attempt === maxRetries - 1) throw err;
      console.log(`Retry ${attempt + 1}/${maxRetries}: ${err.message}`);
      await sleep(2000 * (attempt + 1));
    }
  }
  throw new Error("unreachable");
}

Automatic Contract Verification

After deployment, contracts must be verified on block explorer — mandatory for protocols wanting user trust.

Foundry:

forge verify-contract \
  --chain-id 42161 \
  --num-of-optimizations 200 \
  --compiler-version v0.8.24 \
  $CONTRACT_ADDRESS \
  src/MyContract.sol:MyContract \
  --etherscan-api-key $ARBITRUM_ETHERSCAN_KEY

For Blockscout (Base, some L2):

forge verify-contract \
  --verifier blockscout \
  --verifier-url https://base.blockscout.com/api \
  $CONTRACT_ADDRESS \
  src/MyContract.sol:MyContract

Automation in script: verify immediately after deployment, don't wait separate step. Save verification status in deployments.json.

CI/CD Pipeline

GitHub Actions Workflow

name: Deploy Protocol

on:
  push:
    tags:
      - "v*"  # deploy on release tagging

jobs:
  deploy:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        network: [arbitrum, base, optimism]
      max-parallel: 1  # sequentially, avoid race conditions in deployments.json
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1
      
      - name: Run tests
        run: forge test --fork-url ${{ secrets.MAINNET_RPC }}
      
      - name: Deploy to ${{ matrix.network }}
        env:
          DEPLOYER_PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
          RPC_URL: ${{ secrets[format('{0}_RPC', matrix.network)] }}
        run: |
          forge script script/Deploy.s.sol \
            --rpc-url $RPC_URL \
            --private-key $DEPLOYER_PRIVATE_KEY \
            --broadcast \
            --verify
      
      - name: Update deployments.json
        run: node scripts/update-deployments.js ${{ matrix.network }}
      
      - name: Commit deployments
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "chore: update deployments for ${{ matrix.network }} @ ${{ github.ref_name }}"
          file_pattern: deployments.json

Private Key Management in CI

Never store deployer private key in CI secrets as hex string without additional protection. Better:

  • AWS KMS + custom signer: key in KMS, signing via API. CI secrets leak doesn't compromise key.
  • Hardware wallet via Frame + WalletConnect: deployment with manual confirmation for mainnet.
  • Separate deploy wallet with minimal balance (gas only), not connected to treasury.

Multichain Addressing in Protocol

If contracts interact cross-chain (cross-chain calls), need to manage peer contract addresses:

contract CrossChainRegistry {
    mapping(uint256 => address) public peers; // chainId => peer address
    
    function setPeer(uint256 chainId, address peer) external onlyOwner {
        peers[chainId] = peer;
        emit PeerSet(chainId, peer);
    }
    
    function _validateCrossChainSender(uint256 srcChainId, address sender) internal view {
        require(peers[srcChainId] != address(0), "Unknown chain");
        require(peers[srcChainId] == sender, "Invalid peer");
    }
}

If using LayerZero or Wormhole — they provide own registry mechanisms, but basic logic same.

Monitoring Deployed Contracts

After deployment to 5+ networks need to monitor all instances:

// Aggregated monitoring via events
const deployments = require("./deployments.json");

for (const [network, data] of Object.entries(deployments)) {
  const client = createPublicClient({ transport: http(data.rpcUrl) });
  
  // Watch critical events across all networks
  client.watchContractEvent({
    address: data.contracts.core.address,
    abi: CORE_ABI,
    eventName: "EmergencyPause",
    onLogs: (logs) => alertOps(`PAUSE on ${network}`, logs),
  });
}

Stack and Tools

Component Tool
Compile & test Foundry (forge, cast, anvil)
Deploy scripts TypeScript + viem or Foundry scripts
State tracking deployments.json in git
CI/CD GitHub Actions / GitLab CI
Key management AWS KMS or Ledger for mainnet
Verification Etherscan API / Blockscout API
Monitoring Tenderly + own alerts

Development timeline for deployment system: 1–2 weeks for EVM-compatible networks. Adding non-EVM networks (Solana, TON) requires separate toolchain and significantly more complex.