Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Module 7: Deployment & Operations

Difficulty: Beginner

Estimated reading time: ~25 minutes

πŸ“š Table of Contents

From Local to Production


πŸ’‘ From Local to Production

πŸ’‘ Concept: The Deployment Pipeline

Why this matters: The gap between β€œtests pass locally” and β€œproduction-ready” is where most protocols fail. Nomad Bridge hack ($190M) was caused by a deployment initialization error. The code was correct. The deployment was not.

The production path:

Local development (anvil)
    ↓ forge test
Testnet deployment (Sepolia)
    ↓ forge script --broadcast --verify
Contract verification (Etherscan)
    ↓ verify source code matches bytecode
Ownership transfer (Safe multisig)
    ↓ transfer admin to multisig
Monitoring setup (Tenderly/Defender)
    ↓ alert on key events and state changes
Mainnet deployment
    ↓ same script, different network
Post-deployment verification
    ↓ read state, verify configuration

πŸ” Deep dive: Foundry Book - Deploying covers the full scripting workflow.

πŸ”— DeFi Pattern Connection

How real protocols handle deployment:

ProtocolDeployment PatternWhy
Uniswap V4CREATE2 deterministic + immutable coreSame address on every chain, no proxy overhead
Aave V3Factory pattern + governance proposalPoolAddressesProvider deploys all components atomically
Permit2CREATE2 with zero-nonce deployerCanonical address 0x000000000022D473... on every chain (← Module 3)
SafeCREATE2 proxy factoryDeterministic wallet addresses before deployment
MakerDAOSpell-based deploymentEach upgrade is a β€œspell” contract voted through governance

The pattern: Production DeFi deployment is never β€œrun a script once.” It’s:

  1. Deterministic β€” Same address across chains (CREATE2)
  2. Atomic β€” Deploy + initialize in one transaction (prevent front-running)
  3. Governed β€” Multisig or governance approval before execution
  4. Verified β€” Source code verified immediately after deployment

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œHow do you handle multi-chain deployments?”

    • Good answer: β€œSame Foundry script with different RPC URLs”
    • Great answer: β€œI use CREATE2 for deterministic addresses across chains, with a deployer contract that ensures the same address everywhere. The deployment script verifies chain-specific parameters (token addresses, oracle feeds) from a config file, and I run fork tests against each target chain before broadcasting. Post-deployment, I verify on each chain’s block explorer and run the same integration test suite against the live deployments”
  2. β€œWhat can go wrong during deployment?”

    • Good answer: β€œInitialization front-running, wrong constructor args”
    • Great answer: β€œThe biggest risk is initialization: if deploy and initialize aren’t atomic, an attacker front-runs initialize() and takes ownership (← Module 6 Wormhole example). Second is address-dependent configuration β€” hardcoded token addresses that differ between chains. Third is gas estimation: a script that works on Sepolia may need different gas on mainnet during congestion. I always dry-run with forge script (no --broadcast) first”

Interview Red Flags:

  • 🚩 Deploying without dry-running first
  • 🚩 Not knowing about CREATE2 deterministic deployment
  • 🚩 Deploying proxy + initialize in separate transactions
  • 🚩 Not verifying contracts on block explorers

Pro tip: Study how Permit2 achieved its canonical 0x000000000022D4... address across every chain β€” it’s the textbook CREATE2 deployment. Being able to walk through deterministic deployment from salt selection to address prediction shows you understand the full deployment stack, not just forge script --broadcast.


πŸ’‘ Concept: Deployment Scripts

πŸ“Š Why Solidity scripts > JavaScript:

FeatureSolidity Scripts βœ…JavaScript
TestableCan write tests for deploymentHard to test
ReusableSame script: local, testnet, mainnetOften need separate files
Type-safeCompiler catches errorsRuntime errors
DRYUse contract imports directlyDuplicate ABIs/addresses
// script/Deploy.s.sol
import "forge-std/Script.sol";
import {VaultV1} from "../src/VaultV1.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract DeployScript is Script {
    function run() public returns (address) {
        // Load environment variables
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");
        address tokenAddress = vm.envAddress("VAULT_TOKEN");
        address initialOwner = vm.envOr("INITIAL_OWNER", vm.addr(deployerKey));

        console.log("=== UUPS Vault Deployment ===");
        console.log("Network:", block.chainid);
        console.log("Deployer:", vm.addr(deployerKey));
        console.log("Token:", tokenAddress);

        vm.startBroadcast(deployerKey);

        // Deploy implementation
        VaultV1 implementation = new VaultV1();
        console.log("Implementation:", address(implementation));

        // Deploy proxy with initialization
        bytes memory initData = abi.encodeCall(
            VaultV1.initialize,
            (tokenAddress, initialOwner)
        );
        ERC1967Proxy proxy = new ERC1967Proxy(
            address(implementation),
            initData
        );
        console.log("Proxy:", address(proxy));

        // Verify initialization
        VaultV1 vault = VaultV1(address(proxy));
        require(vault.owner() == initialOwner, "Init failed");
        require(address(vault.token()) == tokenAddress, "Token mismatch");

        vm.stopBroadcast();

        console.log("\n=== Next Steps ===");
        console.log("1. Verify on Etherscan (if not auto-verified)");
        console.log("2. Transfer ownership to Safe multisig");
        console.log("3. Test deposit/withdraw");

        return address(proxy);
    }
}
# Dry run (simulation) βœ…
forge script script/Deploy.s.sol --rpc-url $SEPOLIA_RPC

# Deploy + verify in one command βœ…
forge script script/Deploy.s.sol \
    --rpc-url $SEPOLIA_RPC \
    --broadcast \
    --verify \
    --etherscan-api-key $ETHERSCAN_KEY

# Resume a failed broadcast (e.g., if verification timed out) βœ…
forge script script/Deploy.s.sol --rpc-url $SEPOLIA_RPC --resume

⚑ Common pitfall: Forgetting to fund the deployer address with testnet/mainnet ETH before broadcasting. The script simulates successfully but fails on broadcast with β€œinsufficient funds.”

πŸ” Deep dive: Foundry - Best Practices for Writing Scripts covers testing scripts, error handling, and multi-chain deployments. Cyfrin Updraft - Deploying with Foundry provides hands-on tutorials.

πŸ’» Quick Try:

After deploying any contract (even on a local anvil instance), interact with it using cast:

# Start a local anvil node (in another terminal)
anvil

# Deploy a simple contract
forge create src/VaultV1.sol:VaultV1 --rpc-url http://localhost:8545 --private-key 0xac0974...

# Read state (no gas cost)
cast call $CONTRACT_ADDRESS "owner()" --rpc-url http://localhost:8545
cast call $CONTRACT_ADDRESS "totalSupply()" --rpc-url http://localhost:8545

# Write state (costs gas)
cast send $CONTRACT_ADDRESS "deposit(uint256)" 1000000 --rpc-url http://localhost:8545 --private-key 0xac0974...

# Decode return data
cast call $CONTRACT_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url http://localhost:8545 | cast to-dec  # (In newer Foundry versions, use `cast to-base <value> 10`)

# Read storage slots directly (useful for debugging proxies)
cast storage $CONTRACT_ADDRESS 0 --rpc-url http://localhost:8545

cast is your Swiss Army knife for interacting with deployed contracts. Master it β€” you’ll use it constantly for post-deployment verification and debugging.

πŸ” Deep Dive: CREATE2 Deterministic Deployment

The problem: When deploying to multiple chains, CREATE gives different addresses because the deployer’s nonce differs across chains. This breaks cross-chain composability β€” users and protocols need to know your address in advance.

The solution: CREATE2 computes the address from deployer + salt + initcode, not the nonce:

CREATE2 address = keccak256(0xff ++ deployer ++ salt ++ keccak256(initcode))[12:]
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CREATE vs CREATE2                    β”‚
β”‚                                                   β”‚
β”‚  CREATE:                                          β”‚
β”‚  address = keccak256(sender, nonce)[12:]           β”‚
β”‚  β”œβ”€β”€ Depends on nonce (different per chain)       β”‚
β”‚  └── Non-deterministic across chains ❌           β”‚
β”‚                                                   β”‚
β”‚  CREATE2:                                         β”‚
β”‚  address = keccak256(0xff, sender, salt, initCodeHash)[12:] β”‚
β”‚  β”œβ”€β”€ Same sender + same salt + same code          β”‚
β”‚  └── = Same address on every chain βœ…             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Production example β€” Permit2:

Permit2 uses the same canonical address (0x000000000022D473030F116dDEE9F6B43aC78BA3) on every chain. This is why Uniswap, 1inch, and every other protocol can hardcode the Permit2 address.

// Foundry script using CREATE2
contract DeterministicDeploy is Script {
    function run() public {
        vm.startBroadcast();

        // Same salt on every chain = same address
        bytes32 salt = keccak256("my-protocol-v1");

        MyContract c = new MyContract{salt: salt}(constructorArgs);

        console.log("Deployed at:", address(c));
        // This address will be identical on mainnet, Arbitrum, Optimism, etc.

        vm.stopBroadcast();
    }
}
# Predict the address before deployment
cast create2 --starts-with 0x --salt $SALT --init-code-hash $HASH

When to use CREATE2:

  • Multi-chain protocols (same address everywhere)
  • Factory patterns (predict child addresses before deployment)
  • Vanity addresses (cosmetic, but Permit2’s 0x000000000022D4... is memorable)
  • Counterfactual wallets in Account Abstraction (← Module 4)

πŸŽ“ Intermediate Example: Multi-Chain Deployment Pattern

contract MultiChainDeploy is Script {
    struct ChainConfig {
        string rpcUrl;
        address weth;
        address usdc;
        address chainlinkEthUsd;
    }

    function getConfig() internal view returns (ChainConfig memory) {
        if (block.chainid == 1) {
            return ChainConfig({
                rpcUrl: "mainnet",
                weth: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2,
                usdc: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48,
                chainlinkEthUsd: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
            });
        } else if (block.chainid == 42161) {
            return ChainConfig({
                rpcUrl: "arbitrum",
                weth: 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1,
                usdc: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831,
                chainlinkEthUsd: 0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612
            });
        } else {
            revert("Unsupported chain");
        }
    }

    function run() public {
        ChainConfig memory config = getConfig();
        uint256 deployerKey = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployerKey);

        // Deploy with CREATE2 for same address across chains
        bytes32 salt = keccak256("my-vault-v1");
        VaultV1 impl = new VaultV1{salt: salt}();

        // Chain-specific initialization
        ERC1967Proxy proxy = new ERC1967Proxy{salt: salt}(
            address(impl),
            abi.encodeCall(VaultV1.initialize, (config.weth, config.chainlinkEthUsd))
        );

        vm.stopBroadcast();
    }
}

The pattern: Configuration varies per chain, but the deployment structure is identical. This is how production protocols achieve consistent addresses and behavior across L1 and L2s.

⚠️ Common Mistakes

// ❌ WRONG: Deploy and initialize in separate transactions
vm.startBroadcast(deployerKey);
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), "");
vm.stopBroadcast();
// ... later ...
VaultV1(address(proxy)).initialize(owner);  // Attacker front-runs this!

// βœ… CORRECT: Atomic deploy + initialize
vm.startBroadcast(deployerKey);
ERC1967Proxy proxy = new ERC1967Proxy(
    address(impl),
    abi.encodeCall(VaultV1.initialize, (owner))  // Initialized in constructor
);
vm.stopBroadcast();

// ❌ WRONG: Hardcoded addresses across chains
address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;  // Mainnet only!
// Deploying this to Arbitrum points to a wrong/nonexistent contract

// βœ… CORRECT: Chain-specific configuration
function getUSDC() internal view returns (address) {
    if (block.chainid == 1) return 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    if (block.chainid == 42161) return 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;
    revert("Unsupported chain");
}

// ❌ WRONG: No post-deployment verification
vm.startBroadcast(deployerKey);
new ERC1967Proxy(address(impl), initData);
vm.stopBroadcast();
// Hope it worked... 🀞

// βœ… CORRECT: Verify state after deployment
VaultV1 vault = VaultV1(address(proxy));
require(vault.owner() == expectedOwner, "Owner mismatch");
require(address(vault.token()) == expectedToken, "Token mismatch");
require(vault.totalSupply() == 0, "Unexpected initial state");

πŸ’‘ Concept: Contract Verification

Why this matters: Unverified contracts can’t be audited by users. Verified contracts prove that deployed bytecode matches published source code. This is mandatory for any serious protocol.

Used by: Etherscan, Blockscout, Sourcify

# βœ… Automatic verification (preferred)
forge script script/Deploy.s.sol \
    --rpc-url $SEPOLIA_RPC \
    --broadcast \
    --verify \
    --etherscan-api-key $ETHERSCAN_KEY

# βœ… Manual verification (if auto-verify failed)
forge verify-contract <ADDRESS> src/VaultV1.sol:VaultV1 \
    --chain sepolia \
    --etherscan-api-key $ETHERSCAN_KEY \
    --constructor-args $(cast abi-encode "constructor()" )

# For proxy verification:
# 1. Verify implementation
forge verify-contract <IMPL_ADDRESS> src/VaultV1.sol:VaultV1 \
    --chain sepolia \
    --etherscan-api-key $ETHERSCAN_KEY

# 2. Verify proxy (Etherscan auto-detects [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967) proxies)
#    Just mark it as a proxy in the Etherscan UI

⚑ Common pitfall: Constructor arguments. If your contract has constructor parameters, you MUST provide them with --constructor-args. Use cast abi-encode to format them correctly.

Common verification failures:

  • Optimizer settings mismatch β€” The verification service must use the exact same optimizer and runs settings as your compilation
  • Constructor args encoding β€” Complex constructor arguments need ABI-encoded bytes appended to the deployment bytecode
  • Library linking β€” If your contract uses external libraries, provide their deployed addresses

Sourcify vs Etherscan:

  • Etherscan: Partial match (only checks bytecode), most widely used
  • Sourcify: Full match (checks metadata hash too), decentralized, gaining adoption

Proxy verification workflow:

  1. Verify the implementation contract first
  2. Verify the proxy contract (usually just the ERC1967Proxy bytecode)
  3. On Etherscan: β€œMore Options” β†’ β€œIs this a proxy?” β†’ auto-detects implementation
  4. The proxy’s Read/Write tabs will then show the implementation’s ABI

πŸ’‘ Concept: Safe Multisig for Ownership

Why this matters: A single private key is a single point of failure. Every significant protocol exploit includes the phrase β€œβ€¦and the admin key was compromised.” Ronin Bridge hack ($625M) - single key access.

⚠️ For any protocol managing real value, a single-key owner is unacceptable.

Use Safe (formerly Gnosis Safe) β€” battle-tested, used by Uniswap, Aave, Compound

The pattern:

  1. Deploy with your development key as owner
  2. Verify everything works (test transactions)
  3. Deploy or use existing Safe multisig:
    • Mainnet: use a hardware wallet-backed Safe
    • Testnet: create a 2-of-3 Safe for testing
  4. Call transferOwnership(safeAddress) (or the 2-step variant for safety)
  5. Confirm the transfer from the Safe UI
  6. Verify the new owner on-chain:
    cast call $PROXY "owner()" --rpc-url $RPC_URL
    # Should return: Safe address βœ…
    

πŸ—οΈ Safe resources:

⚑ Common pitfall: Using 1-of-N multisig. That’s just a single key with extra steps. Use at minimum 2-of-3 for testing, 3-of-5+ for production.

How Safe works technically:

  • Proxy + Singleton pattern β€” Each Safe is a minimal proxy pointing to a shared singleton (GnosisSafe.sol)
  • CREATE2 deployment β€” Safe addresses are deterministic based on owners + threshold + salt
  • Module system β€” Extensible via modules (e.g., allowance module for recurring payments)

Safe transaction flow:

  1. Propose β€” Any owner creates a transaction (stored off-chain on Safe’s service)
  2. Collect signatures β€” Other owners sign the transaction hash
  3. Execute β€” Once threshold reached, anyone can submit the multi-signed transaction

Pro tip: Use Safe’s batch transaction feature (via Transaction Builder app) to deploy and configure multiple contracts atomically.


πŸ’‘ Concept: Monitoring and Alerting

Why this matters: You need to know when things go wrong before users tweet about it. Cream Finance exploit ($130M) - repeated attacks over several hours. Monitoring could have limited damage.

The tools:

1. Tenderly

Transaction simulation, debugging, and monitoring.

Set up alerts for:

  • ⚠️ Failed transactions (might indicate attack attempts)
  • ⚠️ Unusual parameter values (e.g., price > 2x normal)
  • ⚠️ Oracle price deviations
  • πŸ’° Large deposits/withdrawals (whale watching)
  • πŸ” Admin function calls (ownership transfer, upgrades)

Tenderly Dashboard

2. OpenZeppelin Defender

Automated operations and monitoring:

  • Sentinel: Monitor transactions and events, trigger alerts
  • Autotasks: Scheduled transactions (keeper-like functions)
  • Admin: Manage upgrades through UI with multisig integration
  • Relay: Gasless transaction infrastructure

Defender Docs

πŸ” Deep dive: OpenZeppelin - Introducing Defender Sentinels explains smart contract monitoring and emergency response patterns. OpenZeppelin - Monitor Documentation provides setup guides for Sentinels with Forta integration.

3. On-chain Events

Every significant state change should emit an event. This isn’t just good practiceβ€”it’s essential for monitoring, indexing, and incident response.

// βœ… GOOD: Emit events for all state changes
event Deposit(address indexed user, uint256 amount, uint256 shares);
event Withdraw(address indexed user, uint256 shares, uint256 amount);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event UpgradeAuthorized(address indexed implementation);

function deposit(uint256 amount) external {
    // ... logic ...
    emit Deposit(msg.sender, amount, shares);
}

Event monitoring pattern:

// Monitor all Withdraw events, filter large amounts in application code
const filter = vault.filters.Withdraw();
vault.on(filter, (sender, receiver, amount, event) => {
    if (amount > ethers.parseEther("1000000")) {
        alertOps(`Large withdrawal: ${ethers.formatEther(amount)} by ${sender}`);
    }
});

⚑ Common pitfall: Not indexing the right parameters. You can only index up to 3 parameters per event. Choose the ones you’ll filter by (usually addresses and IDs).

⚠️ Common Mistakes

// ❌ WRONG: Single EOA as protocol owner
contract Vault is Ownable {
    constructor() Ownable(msg.sender) {}  // Deployer EOA = single point of failure
    // If the key leaks, attacker owns the entire protocol
}

// βœ… CORRECT: Transfer ownership to multisig after deployment
// Step 1: Deploy with EOA (convenient for setup)
// Step 2: Verify everything works
// Step 3: Transfer to Safe
vault.transferOwnership(safeMultisigAddress);

// ❌ WRONG: No events on critical state changes
function setFee(uint256 newFee) external onlyOwner {
    fee = newFee;  // Silent β€” no monitoring tool can detect this change
}

// βœ… CORRECT: Emit events for every admin action
event FeeUpdated(uint256 oldFee, uint256 newFee, address indexed updatedBy);
function setFee(uint256 newFee) external onlyOwner {
    emit FeeUpdated(fee, newFee, msg.sender);
    fee = newFee;
}

// ❌ WRONG: No emergency pause mechanism
// When an exploit starts, no way to stop the damage

// βœ… CORRECT: Include pausable for emergency response
contract Vault is Pausable, Ownable {
    function deposit(uint256 amount) external whenNotPaused { /* ... */ }
    function pause() external onlyOwner { _pause(); }  // Guardian can stop bleeding
}

πŸ”— DeFi Pattern Connection

How real protocols handle operations:

  1. Uniswap Governance β€” Timelock + Governor:

    • Protocol changes go through on-chain governance proposal
    • 2-day voting period, 2-day timelock delay
    • Anyone can see upcoming changes before execution
    • Lesson: Transparency builds trust more than multisig alone
  2. Aave Guardian β€” Emergency multisig + governance:

    • Normal upgrades: Full governance process (propose β†’ vote β†’ timelock β†’ execute)
    • Emergency: Guardian multisig can pause markets instantly
    • Lesson: Two paths β€” slow/safe for upgrades, fast for emergencies
  3. MakerDAO Spells β€” Executable code as governance:

    • Each change is a β€œspell” β€” a contract that executes the change
    • Spell code is public and auditable before voting
    • Once voted, the spell executes atomically
    • Lesson: Governance proposals should be code, not descriptions
  4. Incident Response Pattern:

    Detection (Tenderly alert) β†’ 30 seconds
        ↓
    Triage (is this an exploit?) β†’ 5 minutes
        ↓
    Pause protocol (Guardian multisig) β†’ 10 minutes
        ↓
    Root cause analysis β†’ hours
        ↓
    Fix + test + deploy β†’ hours/days
        ↓
    Post-mortem β†’ days
    
    • Having pause() functionality and a responsive multisig can be the difference between $0 and $100M+ lost

πŸ’Ό Job Market Context

What DeFi teams expect you to know:

  1. β€œHow would you set up operations for a new protocol?”

    • Good answer: β€œSafe multisig for admin, Tenderly for monitoring”
    • Great answer: β€œI’d separate concerns: a 3-of-5 multisig for routine operations (fee changes, parameter updates), a separate Guardian multisig for emergencies (pause), and a governance timelock for upgrades. Monitoring with Tenderly alerts on admin function calls, large token movements, and oracle deviations. Event emission for every state change so we can build dashboards and respond to anomalies. I’d also write runbooks for common scenarios β€” β€˜oracle goes stale’, β€˜exploit detected’, β€˜governance proposal needs execution’”
  2. β€œWhat’s your deployment checklist before mainnet?”

    • Good answer: β€œTests pass, contract verified, multisig set up”
    • Great answer: β€œPre-deployment: all tests pass including fork tests against mainnet, forge inspect confirms storage layout, dry-run with forge script (no broadcast). Deployment: atomic deploy+initialize, verify source on Etherscan/Sourcify immediately. Post-deployment: read all state variables with cast call to confirm configuration, transfer ownership to multisig, set up monitoring alerts, do a small real transaction to verify end-to-end, document all addresses in a deployment manifest”

Interview Red Flags:

  • 🚩 Single-key ownership for any protocol managing value
  • 🚩 No monitoring or alerting strategy
  • 🚩 Not knowing about Safe multisig
  • 🚩 No post-deployment verification process

Pro tip: The best DeFi teams have incident response playbooks before anything goes wrong. Being able to discuss operational security β€” pause mechanisms, monitoring thresholds, communication channels β€” shows you think about protocols holistically, not just the code.


🎯 Build Exercise: Deployment Capstone

Workspace: workspace/script/ β€” deployment script: DeployUUPSVault.s.sol, tests: DeployUUPSVault.t.sol

This is the capstone exercise for Part 1:

  1. Write a complete deployment script for your UUPS vault from Module 6:

    • Load configuration from environment variables
    • Deploy implementation
    • Deploy proxy with initialization
    • Verify initialization succeeded
    • Log all addresses and next steps
  2. Deploy to Sepolia testnet:

    forge script script/Deploy.s.sol \
        --rpc-url $SEPOLIA_RPC \
        --broadcast \
        --verify \
        --etherscan-api-key $ETHERSCAN_KEY
    
  3. Verify the contract on Etherscan:

    • βœ… Check both implementation and proxy are verified
    • βœ… Verify proxy is detected as EIP-1967 proxy
    • βœ… Test β€œRead Contract” and β€œWrite Contract” tabs
  4. (Optional) Set up a Safe multisig on Sepolia:

    • Create a 2-of-3 Safe at safe.global
    • Transfer vault ownership to the Safe
    • Execute a test transaction (deposit) through the Safe
  5. Post-deployment verification script:

    # Verify owner
    cast call $PROXY "owner()" --rpc-url $SEPOLIA_RPC
    
    # Verify token
    cast call $PROXY "token()" --rpc-url $SEPOLIA_RPC
    
    # Verify version
    cast call $PROXY "version()" --rpc-url $SEPOLIA_RPC
    

🎯 Goal: Understand the full lifecycle from development to deployment. This pipeline is what you’ll use in Part 2 when deploying your builds to testnets for more realistic testing.


πŸ“‹ Summary: Deployment and Operations

βœ“ Covered:

  • Deployment pipeline β€” local β†’ testnet β†’ mainnet
  • Solidity scripts β€” testable, reusable, type-safe deployment
  • Contract verification β€” Etherscan, Sourcify
  • Safe multisig β€” eliminating single-key risk
  • Monitoring β€” Tenderly, Defender, event-based alerts

Key takeaway: Deployment is where code meets reality. A perfect contract with a broken deployment is useless. Test your deployment scripts as rigorously as your contracts.


πŸ“– How to Study Production Deployment Scripts

When you look at a protocol’s script/ directory, here’s how to navigate it:

Step 1: Find the main deployment script Usually named Deploy.s.sol, DeployProtocol.s.sol, or similar. This is the entry point.

Step 2: Look for the configuration pattern How does the script handle different chains?

  • Environment variables (vm.envAddress)
  • Chain-specific config files
  • if (block.chainid == 1) branching
  • Separate config contracts

Step 3: Trace the deployment order Contracts are deployed in dependency order. The script reveals the architecture:

1. Deploy libraries (no dependencies)
2. Deploy core contracts (depend on libraries)
3. Deploy proxies (wrap core contracts)
4. Initialize (set parameters, link contracts)
5. Transfer ownership (to multisig/governance)

Step 4: Check post-deployment verification Good scripts verify state after deployment:

require(vault.owner() == expectedOwner, "Owner mismatch");
require(vault.token() == expectedToken, "Token mismatch");

Step 5: Look for upgrade scripts Separate from initial deployment β€” these handle proxy upgrades with storage layout checks.

Don’t get stuck on: Helper utilities and test-specific deployment code. Focus on the production deployment path.

Backward references (← concepts from earlier modules):

ModuleConceptHow It Connects
← M1 Modern Solidityabi.encodeCallType-safe initialization data in deployment scripts β€” compiler catches mismatched args
← M1 Modern SolidityCustom errorsDeployment validation failures with rich error data
← M2 EVM ChangesEIP-7702 delegationDelegation targets must exist before EOA delegates β€” deployment order matters
← M3 Token ApprovalsPermit2 CREATE2Gold standard for deterministic multi-chain deployment β€” canonical address everywhere
← M3 Token ApprovalsDOMAIN_SEPARATORIncludes block.chainid β€” verify it differs per chain after deployment
← M4 Account AbstractionCREATE2 factoriesERC-4337 wallet factories use counterfactual addresses β€” wallet exists before deployment
← M5 Foundryforge scriptPrimary deployment tool β€” simulation, broadcast, resume
← M5 Foundrycast commandsPost-deployment interaction: cast call for reads, cast send for writes
← M6 Proxy PatternsAtomic deploy+initUUPS proxy must deploy + initialize in one tx to prevent front-running
← M6 Proxy PatternsStorage layout checksforge inspect storage-layout before any upgrade deployment

Part 2 connections:

Part 2 ModuleDeployment PatternApplication
M1: Token MechanicsToken deploymentERC-20 deployment with initial supply, fee configuration, and access control setup
M2: AMMsFactory patternPool creation through factory contracts β€” deterministic pool addresses from token pairs
M3: OraclesFeed configurationChain-specific Chainlink feed addresses β€” different on every L2
M4: LendingMulti-contract deployAave V3 deploys Pool + Configurator + Oracle + aTokens atomically via AddressesProvider
M5: Flash LoansArbitrage scriptsFlash loan deployment with DEX router addresses per chain
M6: StablecoinsCDP deploymentMulti-contract CDP engine with oracle + liquidation + stability modules
M7: VaultsStrategy deploymentVault + strategy deploy scripts with yield source configuration per chain
M8: SecurityPost-deploy auditDeployment verification as security practice β€” check all state before going live
M9: IntegrationFull pipelineEnd-to-end deployment: factory β†’ pools β†’ oracles β†’ governance β†’ monitoring

πŸ“– Production Study Order

Study these deployment scripts in this order β€” each builds on patterns from the previous:

#RepositoryWhy Study ThisKey Files
1Foundry Book - ScriptingOfficial patterns β€” learn the Script base class and vm.broadcastTutorial examples
2Morpho Blue scriptsClean, minimal production deployment β€” single contract, no proxiesDeploy.s.sol
3Uniswap V4 scriptsCREATE2 deterministic deployment β€” immutable core patternDeployPoolManager.s.sol
4Permit2 deploymentCanonical CREATE2 address β€” the gold standard for multi-chain deploymentDeployPermit2.s.sol
5Aave V3 deployFull production pipeline β€” multi-contract, multi-chain, proxy + beacondeploy/, config/
6Safe deploymentFactory + CREATE2 for deterministic wallet addressesdeploy scripts

Reading strategy: Start with the Foundry Book for idioms, then Morpho for the simplest real deployment. Move to Uniswap/Permit2 for CREATE2 mastery. Finish with Aave for the most complex deployment you’ll encounter β€” multi-contract, multi-chain, proxy architecture. Safe shows CREATE2 applied to wallet infrastructure.


πŸ“š Resources

Deployment & Scripting

Safe Multisig

Monitoring & Operations

Testnets & Faucets

Post-Deployment Security


πŸŽ‰ Part 1 Complete!

You’ve now covered:

  • βœ… Solidity 0.8.x modern features
  • βœ… EVM-level changes (Dencun, Pectra)
  • βœ… Modern token approval patterns (EIP-2612, Permit2)
  • βœ… Account abstraction (ERC-4337, EIP-7702)
  • βœ… Foundry testing workflow
  • βœ… Proxy patterns and upgradeability
  • βœ… Production deployment pipeline

You’re ready for Part 2: Reading and building production DeFi protocols (Uniswap, Aave, MakerDAO).


Navigation: ← Module 6: Proxy Patterns | Part 2: DeFi Protocols β†’