Module 7: Deployment & Operations
Difficulty: Beginner
Estimated reading time: ~25 minutes
π Table of Contents
From Local to Production
- The Deployment Pipeline
- Deployment Scripts
- Contract Verification
- Safe Multisig for Ownership
- Monitoring and Alerting
- Build Exercise: Deployment Capstone
π‘ 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:
| Protocol | Deployment Pattern | Why |
|---|---|---|
| Uniswap V4 | CREATE2 deterministic + immutable core | Same address on every chain, no proxy overhead |
| Aave V3 | Factory pattern + governance proposal | PoolAddressesProvider deploys all components atomically |
| Permit2 | CREATE2 with zero-nonce deployer | Canonical address 0x000000000022D473... on every chain (β Module 3) |
| Safe | CREATE2 proxy factory | Deterministic wallet addresses before deployment |
| MakerDAO | Spell-based deployment | Each upgrade is a βspellβ contract voted through governance |
The pattern: Production DeFi deployment is never βrun a script once.β Itβs:
- Deterministic β Same address across chains (
CREATE2) - Atomic β Deploy + initialize in one transaction (prevent front-running)
- Governed β Multisig or governance approval before execution
- Verified β Source code verified immediately after deployment
πΌ Job Market Context
What DeFi teams expect you to know:
-
βHow do you handle multi-chain deployments?β
- Good answer: βSame Foundry script with different RPC URLsβ
- Great answer: βI use
CREATE2for 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β
-
β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 withforge script(no--broadcast) firstβ
Interview Red Flags:
- π© Deploying without dry-running first
- π© Not knowing about
CREATE2deterministic 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:
| Feature | Solidity Scripts β | JavaScript |
|---|---|---|
| Testable | Can write tests for deployment | Hard to test |
| Reusable | Same script: local, testnet, mainnet | Often need separate files |
| Type-safe | Compiler catches errors | Runtime errors |
| DRY | Use contract imports directly | Duplicate 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. Usecast abi-encodeto format them correctly.
Common verification failures:
- Optimizer settings mismatch β The verification service must use the exact same
optimizerandrunssettings 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:
- Verify the implementation contract first
- Verify the proxy contract (usually just the ERC1967Proxy bytecode)
- On Etherscan: βMore Optionsβ β βIs this a proxy?β β auto-detects implementation
- 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:
- Deploy with your development key as owner
- Verify everything works (test transactions)
- Deploy or use existing Safe multisig:
- Mainnet: use a hardware wallet-backed Safe
- Testnet: create a 2-of-3 Safe for testing
- Call
transferOwnership(safeAddress)(or the 2-step variant for safety) - Confirm the transfer from the Safe UI
- Verify the new owner on-chain:
cast call $PROXY "owner()" --rpc-url $RPC_URL # Should return: Safe address β
ποΈ Safe resources:
- Safe App β create and manage Safes
- Safe Contracts β source code
- Safe Transaction Service β API for off-chain signature collection
β‘ 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:
- Propose β Any owner creates a transaction (stored off-chain on Safeβs service)
- Collect signatures β Other owners sign the transaction hash
- 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)
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
π 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:
-
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
-
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
-
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
-
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
- Having
πΌ Job Market Context
What DeFi teams expect you to know:
-
β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ββ
-
β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 inspectconfirms storage layout, dry-run withforge script(no broadcast). Deployment: atomic deploy+initialize, verify source on Etherscan/Sourcify immediately. Post-deployment: read all state variables withcast callto 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:
-
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
-
Deploy to Sepolia testnet:
forge script script/Deploy.s.sol \ --rpc-url $SEPOLIA_RPC \ --broadcast \ --verify \ --etherscan-api-key $ETHERSCAN_KEY -
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
-
(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
-
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.
π Cross-Module Concept Links
Backward references (β concepts from earlier modules):
| Module | Concept | How It Connects |
|---|---|---|
| β M1 Modern Solidity | abi.encodeCall | Type-safe initialization data in deployment scripts β compiler catches mismatched args |
| β M1 Modern Solidity | Custom errors | Deployment validation failures with rich error data |
| β M2 EVM Changes | EIP-7702 delegation | Delegation targets must exist before EOA delegates β deployment order matters |
| β M3 Token Approvals | Permit2 CREATE2 | Gold standard for deterministic multi-chain deployment β canonical address everywhere |
| β M3 Token Approvals | DOMAIN_SEPARATOR | Includes block.chainid β verify it differs per chain after deployment |
| β M4 Account Abstraction | CREATE2 factories | ERC-4337 wallet factories use counterfactual addresses β wallet exists before deployment |
| β M5 Foundry | forge script | Primary deployment tool β simulation, broadcast, resume |
| β M5 Foundry | cast commands | Post-deployment interaction: cast call for reads, cast send for writes |
| β M6 Proxy Patterns | Atomic deploy+init | UUPS proxy must deploy + initialize in one tx to prevent front-running |
| β M6 Proxy Patterns | Storage layout checks | forge inspect storage-layout before any upgrade deployment |
Part 2 connections:
| Part 2 Module | Deployment Pattern | Application |
|---|---|---|
| M1: Token Mechanics | Token deployment | ERC-20 deployment with initial supply, fee configuration, and access control setup |
| M2: AMMs | Factory pattern | Pool creation through factory contracts β deterministic pool addresses from token pairs |
| M3: Oracles | Feed configuration | Chain-specific Chainlink feed addresses β different on every L2 |
| M4: Lending | Multi-contract deploy | Aave V3 deploys Pool + Configurator + Oracle + aTokens atomically via AddressesProvider |
| M5: Flash Loans | Arbitrage scripts | Flash loan deployment with DEX router addresses per chain |
| M6: Stablecoins | CDP deployment | Multi-contract CDP engine with oracle + liquidation + stability modules |
| M7: Vaults | Strategy deployment | Vault + strategy deploy scripts with yield source configuration per chain |
| M8: Security | Post-deploy audit | Deployment verification as security practice β check all state before going live |
| M9: Integration | Full pipeline | End-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:
| # | Repository | Why Study This | Key Files |
|---|---|---|---|
| 1 | Foundry Book - Scripting | Official patterns β learn the Script base class and vm.broadcast | Tutorial examples |
| 2 | Morpho Blue scripts | Clean, minimal production deployment β single contract, no proxies | Deploy.s.sol |
| 3 | Uniswap V4 scripts | CREATE2 deterministic deployment β immutable core pattern | DeployPoolManager.s.sol |
| 4 | Permit2 deployment | Canonical CREATE2 address β the gold standard for multi-chain deployment | DeployPermit2.s.sol |
| 5 | Aave V3 deploy | Full production pipeline β multi-contract, multi-chain, proxy + beacon | deploy/, config/ |
| 6 | Safe deployment | Factory + CREATE2 for deterministic wallet addresses | deploy 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
- Foundry Book - Solidity Scripting β full tutorial
- Foundry Book - Deploying β
forge scriptreference - Etherscan Verification β API docs
Safe Multisig
- Safe App β create and manage Safes
- Safe Contracts β source code
- Safe Documentation β full docs
- Safe Transaction Service API
Monitoring & Operations
- Tenderly β monitoring and simulation
- OpenZeppelin Defender β automated ops
- Blocknative Mempool Explorer β real-time transaction monitoring
Testnets & Faucets
- Sepolia Faucet (Alchemy)
- Sepolia Faucet (Infura)
- Chainlist β RPC endpoints for all networks
Post-Deployment Security
- Nomad Bridge postmortem β initialization error ($190M)
- Ronin Bridge postmortem β compromised keys ($625M)
- Rekt News β exploit case studies
π 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 β