CI/CD Setup for Smart Contract Deployment
Deploying smart contract manually via hardhat deploy --network mainnet — this is one-time action that becomes problem on regular protocol updates. Without automation: tests run selectively, deployment from developer's machine (not always up-to-date), verification forgotten, contract addresses stored in Notion and often lost. Configured CI/CD pipeline closes all these issues.
Smart Contract Pipeline Anatomy
Unlike backend CI/CD, smart contract pipeline has specific stage: mainnet deployment is irreversible, rollback impossible. This means gates before production deployment must be strict.
Typical GitHub Actions workflow structure:
PR opened → lint + compile → unit tests → coverage check → merge to main
Merge to main → staging deploy (testnet) → integration tests → manual approval → mainnet deploy → verification → monitoring setup
Lint and compilation — cheapest step. npx hardhat compile + solhint (or forge build + forge fmt --check). Failing compile in PR — immediate feedback.
Unit tests with coverage — coverage check via hardhat-coverage or forge coverage. Set minimum threshold: 85% line coverage, 100% for critical functions (withdraw, mint, admin). Pull request doesn't merge if threshold not reached — enforced via required status checks in GitHub.
Staging deploy — deployment to Sepolia/Polygon Amoy/BSC Testnet automatically on each main merge. Addresses saved to GitHub Actions artifacts and/or separate deployments/ repository.
Integration tests — tests against real deployed contract on testnet. Verify integrations: oracles, DEX routers, bridge interfaces. Sometimes reveals issues unit tests with mocks missed.
Manual approval — mandatory gate before mainnet. GitHub Environments with required reviewers. No auto-deploy to mainnet without human confirmation.
Managing Private Keys in CI
Mainnet deployment requires private key or mnemonic. Storing in GitHub Secrets — minimum standard. Better — AWS Secrets Manager or HashiCorp Vault with short-lived credentials via OIDC.
Pattern we use for production: deployer is separate EOA with minimal balance (gas only), without admin rights. Contract ownership immediately transferred to multisig (Safe) after deployment. Compromised deployer key doesn't give attacker contract control.
In GitHub Actions:
- name: Deploy to mainnet
env:
PRIVATE_KEY: ${{ secrets.DEPLOYER_PRIVATE_KEY }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
run: |
npx hardhat run scripts/deploy.ts --network mainnet
npx hardhat verify --network mainnet $CONTRACT_ADDRESS
Deployment Artifacts and Address Registry
After each deployment save artifacts: contract address, block number of deployment, transaction hash, ABI. Use hardhat-deploy plugin — automatically saves deployments to deployments/{networkName}/ directory. These files committed to repository — source of truth for contract addresses.
For multi-chain protocols configure deployment matrix:
strategy:
matrix:
network: [mainnet, polygon, arbitrum, optimism]
Each network — separate Job with corresponding RPC endpoints and API keys.
Post-Deployment Monitoring
Deployment is not final. Configure Tenderly Alerts or OpenZeppelin Defender Sentinels on critical events: unusual transaction volume, admin function calls, pause event emissions. Alert goes to Telegram/Slack.
For upgradeable contracts — monitoring via The Graph or Ponder: index Upgraded, AdminChanged events and trigger alert on any change.
Timeline
Basic pipeline (lint + test + testnet deploy + verify): 1 day setup. Full pipeline with mainnet approval gate, matrix deploys and post-deploy monitoring: 2-3 days.







