Files
gh-tenequm-claude-plugins-f…/skills/skill/references/deployment.md
2025-11-30 09:01:14 +08:00

14 KiB

Foundry Deployment Guide

Complete guide to deploying and verifying smart contracts with Foundry.

forge create (Single Contract)

Quick deployment for single contracts.

Basic Usage

# Deploy with constructor args
forge create src/Token.sol:Token \
    --rpc-url sepolia \
    --private-key $PRIVATE_KEY \
    --constructor-args "MyToken" "MTK" 18

# Deploy and verify
forge create src/Token.sol:Token \
    --rpc-url sepolia \
    --private-key $PRIVATE_KEY \
    --broadcast \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY \
    --constructor-args "MyToken" "MTK" 18

# Deploy with value (payable constructor)
forge create src/Vault.sol:Vault \
    --rpc-url sepolia \
    --private-key $PRIVATE_KEY \
    --value 1ether

Using Ledger

forge create src/Token.sol:Token \
    --rpc-url mainnet \
    --ledger \
    --mnemonic-derivation-path "m/44'/60'/0'/0/0"

More powerful and flexible deployment method.

Basic Deploy Script

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {Script, console} from "forge-std/Script.sol";
import {Token} from "../src/Token.sol";

contract DeployToken is Script {
    function run() external {
        // Load private key from environment
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");
        address deployer = vm.addr(deployerKey);

        console.log("Deploying from:", deployer);
        console.log("Balance:", deployer.balance);

        vm.startBroadcast(deployerKey);

        Token token = new Token("MyToken", "MTK", 18);
        console.log("Token deployed to:", address(token));

        // Initial setup
        token.mint(deployer, 1_000_000e18);

        vm.stopBroadcast();
    }
}

Running Scripts

# Dry run (simulation only)
forge script script/Deploy.s.sol:DeployToken --rpc-url sepolia

# Broadcast transactions
forge script script/Deploy.s.sol:DeployToken \
    --rpc-url sepolia \
    --broadcast

# Broadcast and verify
forge script script/Deploy.s.sol:DeployToken \
    --rpc-url sepolia \
    --broadcast \
    --verify

# Resume failed deployment
forge script script/Deploy.s.sol:DeployToken \
    --rpc-url sepolia \
    --broadcast \
    --resume

Script Execution Phases

  1. Local Simulation: Run script, collect vm.broadcast() transactions
  2. On-chain Simulation: (with --rpc-url) Simulate against chain state
  3. Broadcasting: (with --broadcast) Send transactions to network
  4. Verification: (with --verify) Verify contracts on Etherscan

Complex Deployment

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {Script, console} from "forge-std/Script.sol";
import {Token} from "../src/Token.sol";
import {Staking} from "../src/Staking.sol";
import {Governance} from "../src/Governance.sol";

contract DeployProtocol is Script {
    function run() external {
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");
        address admin = vm.envAddress("ADMIN_ADDRESS");

        vm.startBroadcast(deployerKey);

        // Deploy token
        Token token = new Token("Protocol Token", "PTK", 18);
        console.log("Token:", address(token));

        // Deploy staking with token reference
        Staking staking = new Staking(address(token));
        console.log("Staking:", address(staking));

        // Deploy governance with token and staking
        Governance gov = new Governance(
            address(token),
            address(staking),
            admin
        );
        console.log("Governance:", address(gov));

        // Setup permissions
        token.grantRole(token.MINTER_ROLE(), address(staking));
        staking.setGovernance(address(gov));

        vm.stopBroadcast();
    }
}

Configuration-Based Deployment

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {Script, console} from "forge-std/Script.sol";
import {Token} from "../src/Token.sol";

contract DeployConfigured is Script {
    struct Config {
        string name;
        string symbol;
        uint256 initialSupply;
        address admin;
    }

    function run() external {
        Config memory config = getConfig();

        uint256 deployerKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployerKey);

        Token token = new Token(config.name, config.symbol, 18);
        token.mint(config.admin, config.initialSupply);

        if (config.admin != vm.addr(deployerKey)) {
            token.transferOwnership(config.admin);
        }

        vm.stopBroadcast();

        console.log("Deployed:", address(token));
    }

    function getConfig() internal view returns (Config memory) {
        uint256 chainId = block.chainid;

        if (chainId == 1) {
            return Config({
                name: "Production Token",
                symbol: "PROD",
                initialSupply: 100_000_000e18,
                admin: 0x1234567890123456789012345678901234567890
            });
        } else if (chainId == 11155111) {
            return Config({
                name: "Test Token",
                symbol: "TEST",
                initialSupply: 1_000_000e18,
                admin: vm.envAddress("TEST_ADMIN")
            });
        } else {
            revert("Unsupported chain");
        }
    }
}

Deterministic Deployment (CREATE2)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {Script, console} from "forge-std/Script.sol";
import {Token} from "../src/Token.sol";

contract DeployDeterministic is Script {
    // Deterministic deployment factory (present on most chains)
    address constant CREATE2_FACTORY = 0x4e59b44847b379578588920cA78FbF26c0B4956C;

    function run() external {
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");
        bytes32 salt = keccak256("my-token-v1");

        // Predict address
        bytes memory bytecode = abi.encodePacked(
            type(Token).creationCode,
            abi.encode("MyToken", "MTK", 18)
        );
        address predicted = computeCreate2Address(salt, keccak256(bytecode));
        console.log("Predicted address:", predicted);

        // Check if already deployed
        if (predicted.code.length > 0) {
            console.log("Already deployed!");
            return;
        }

        vm.startBroadcast(deployerKey);

        Token token = new Token{salt: salt}("MyToken", "MTK", 18);
        require(address(token) == predicted, "Address mismatch");

        vm.stopBroadcast();

        console.log("Deployed to:", address(token));
    }

    function computeCreate2Address(bytes32 salt, bytes32 initCodeHash)
        internal
        view
        returns (address)
    {
        return address(uint160(uint256(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            initCodeHash
        )))));
    }
}

Multi-Chain Deployment

Sequential Deployment

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import {Script, console} from "forge-std/Script.sol";
import {Token} from "../src/Token.sol";

contract DeployMultiChain is Script {
    function run() external {
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");

        // Deploy to Ethereum
        vm.createSelectFork("mainnet");
        vm.startBroadcast(deployerKey);
        Token mainnetToken = new Token("Token", "TKN", 18);
        vm.stopBroadcast();
        console.log("Mainnet:", address(mainnetToken));

        // Deploy to Arbitrum
        vm.createSelectFork("arbitrum");
        vm.startBroadcast(deployerKey);
        Token arbitrumToken = new Token("Token", "TKN", 18);
        vm.stopBroadcast();
        console.log("Arbitrum:", address(arbitrumToken));

        // Deploy to Optimism
        vm.createSelectFork("optimism");
        vm.startBroadcast(deployerKey);
        Token optimismToken = new Token("Token", "TKN", 18);
        vm.stopBroadcast();
        console.log("Optimism:", address(optimismToken));
    }
}

Run with:

forge script script/DeployMultiChain.s.sol \
    --broadcast \
    --multi \
    --slow \
    --verify

Contract Verification

Auto-Verification

# During deployment
forge create src/Token.sol:Token \
    --rpc-url sepolia \
    --private-key $KEY \
    --verify \
    --etherscan-api-key $ETHERSCAN_KEY \
    --constructor-args "Name" "SYM" 18

# With script
forge script script/Deploy.s.sol --broadcast --verify

Manual Verification

# Verify existing contract
forge verify-contract \
    --chain sepolia \
    --compiler-version 0.8.30 \
    --num-of-optimizations 200 \
    --constructor-args $(cast abi-encode "constructor(string,string,uint8)" "Name" "SYM" 18) \
    0xYourContractAddress \
    src/Token.sol:Token

# Check verification status
forge verify-check \
    --chain sepolia \
    $GUID

Verification with Libraries

forge verify-contract \
    --chain mainnet \
    --libraries src/lib/Math.sol:Math:0xLibraryAddress \
    --libraries src/lib/Utils.sol:Utils:0xUtilsAddress \
    0xContractAddress \
    src/MyContract.sol:MyContract

Configuration for Verification

# foundry.toml
[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
sepolia = { key = "${ETHERSCAN_API_KEY}" }
arbitrum = { key = "${ARBISCAN_API_KEY}" }
optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" }
base = { key = "${BASESCAN_API_KEY}" }
polygon = { key = "${POLYGONSCAN_API_KEY}" }

Broadcast Artifacts

Scripts save transaction data to broadcast/ directory:

broadcast/
└── Deploy.s.sol/
    └── 11155111/          # Chain ID
        ├── run-latest.json     # Latest run
        ├── run-1699999999.json # Timestamped runs
        └── receipts/
            └── ...

Reading Artifacts in Scripts

function readDeployment() internal view returns (address) {
    string memory root = vm.projectRoot();
    string memory path = string.concat(
        root,
        "/broadcast/Deploy.s.sol/11155111/run-latest.json"
    );

    string memory json = vm.readFile(path);
    bytes memory contractAddr = json.parseRaw(".transactions[0].contractAddress");
    return abi.decode(contractAddr, (address));
}

Best Practices

1. Use Environment Variables

uint256 deployerKey = vm.envUint("PRIVATE_KEY");
address admin = vm.envAddress("ADMIN_ADDRESS");
string memory rpcUrl = vm.envString("RPC_URL");

2. Validate Before Broadcasting

function run() external {
    uint256 deployerKey = vm.envUint("PRIVATE_KEY");
    address deployer = vm.addr(deployerKey);

    // Pre-flight checks
    require(deployer.balance > 0.1 ether, "Insufficient balance");
    require(block.chainid == 11155111, "Wrong network");

    vm.startBroadcast(deployerKey);
    // ...
}

3. Log Everything

vm.startBroadcast(deployerKey);

Token token = new Token("Name", "SYM", 18);
console.log("Token deployed to:", address(token));
console.log("  - Name:", token.name());
console.log("  - Symbol:", token.symbol());
console.log("  - Owner:", token.owner());

vm.stopBroadcast();

4. Use Numbered Scripts

script/
├── 01_DeployToken.s.sol
├── 02_DeployStaking.s.sol
├── 03_ConfigurePermissions.s.sol
└── 04_TransferOwnership.s.sol

5. Test Scripts

contract DeployTokenTest is Test {
    DeployToken deployer;

    function setUp() public {
        deployer = new DeployToken();
        // Set required env vars
        vm.setEnv("PRIVATE_KEY", vm.toString(uint256(1)));
    }

    function testDeploy() public {
        deployer.run();
        // Verify deployment
    }
}

6. Handle Failures Gracefully

function run() external {
    uint256 deployerKey = vm.envUint("PRIVATE_KEY");

    vm.startBroadcast(deployerKey);

    try new Token("Name", "SYM", 18) returns (Token token) {
        console.log("Success:", address(token));
    } catch Error(string memory reason) {
        console.log("Failed:", reason);
    }

    vm.stopBroadcast();
}

Upgrade Patterns

Transparent Proxy

import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

contract DeployUpgradeable is Script {
    function run() external {
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployerKey);

        // Deploy implementation
        TokenV1 impl = new TokenV1();

        // Deploy proxy admin
        ProxyAdmin admin = new ProxyAdmin(msg.sender);

        // Deploy proxy
        bytes memory initData = abi.encodeCall(TokenV1.initialize, ("Name", "SYM"));
        TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
            address(impl),
            address(admin),
            initData
        );

        vm.stopBroadcast();

        console.log("Implementation:", address(impl));
        console.log("ProxyAdmin:", address(admin));
        console.log("Proxy:", address(proxy));
    }
}

UUPS Proxy

import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract DeployUUPS is Script {
    function run() external {
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployerKey);

        // Deploy implementation
        TokenV1 impl = new TokenV1();

        // Deploy proxy
        bytes memory initData = abi.encodeCall(TokenV1.initialize, ("Name", "SYM"));
        ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData);

        vm.stopBroadcast();

        console.log("Implementation:", address(impl));
        console.log("Proxy:", address(proxy));
    }
}