Разработка системы автоматического деплоя на несколько сетей
Проблема возникает на третьем или четвёртом деплое одного и того же протокола в разные сети: кто-то задеплоил на Arbitrum с другим значением константы, на Base использовалась другая версия OpenZeppelin, адреса прокси не сохранились нормально, и теперь непонятно что где задеплоено. Multichain auto-deploy решает именно это — воспроизводимость и трекинг деплоев.
Детерминированные адреса через CREATE2
Главное требование к мультичейн деплою: одинаковые адреса на всех сетях. Это упрощает пользовательский опыт, документацию и кросс-чейн интеграции.
CREATE2 позволяет вычислить адрес контракта до деплоя:
address = keccak256(0xff ++ deployerAddress ++ salt ++ keccak256(bytecode))[12:]
Если deployer имеет одинаковый адрес на всех EVM-сетях (через Nick's Factory или собственный deployer через deterministicDeploy), байткод одинаков, salt одинаков — адрес будет одинаков везде.
Foundry поддерживает это нативно:
// 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);
}
# Деплой на несколько сетей
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
Проблема: если байткод отличается между сетями (например, hardcoded chainId или address), CREATE2-адрес будет другим. Нужно избегать compile-time константных адресов в байткоде.
Структура деплой-системы
Конфигурация сетей
Единый источник истины для всех сетей в deploy.config.ts:
export interface NetworkConfig {
chainId: number;
rpcUrl: string;
deployer: string; // адрес деплоера
gasPrice?: bigint; // override для сетей с нестабильным gas
confirmations: number; // сколько блоков ждать
verifier?: "etherscan" | "blockscout" | "none";
verifierUrl?: string;
nativeCurrency: string;
contracts: {
// адреса зависимых контрактов в этой сети
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 и state management
После каждого деплоя нужно сохранять адреса контрактов. Foundry сохраняет в broadcast/ директорию, но это неудобно для multichain трекинга.
Лучший подход — генерировать deployments.json:
interface DeploymentRecord {
network: string;
chainId: number;
contracts: Record<string, {
address: string;
implementationAddress?: string;
abi: string; // IPFS хеш или путь
deployedAt: number; // block number
txHash: string;
version: string; // semver
}>;
deployedBy: string;
timestamp: string;
}
Этот файл коммитится в репозиторий — является единственным источником правды об адресах. CI/CD обновляет его после каждого деплоя.
Деплой скрипт с retry и 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();
// Ждём подтверждений
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");
}
Автоматическая верификация контрактов
После деплоя контракты нужно верифицировать на block explorer — это обязательно для протоколов, которые хотят доверие пользователей.
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
Для Blockscout (Base, некоторые L2):
forge verify-contract \
--verifier blockscout \
--verifier-url https://base.blockscout.com/api \
$CONTRACT_ADDRESS \
src/MyContract.sol:MyContract
Автоматизация в скрипте: верифицировать сразу после деплоя, не ждать отдельного шага. Сохранять статус верификации в deployments.json.
CI/CD pipeline
GitHub Actions workflow
name: Deploy Protocol
on:
push:
tags:
- "v*" # деплой при тегировании релиза
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
network: [arbitrum, base, optimism]
max-parallel: 1 # последовательно, чтобы не было race conditions в 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
Управление приватными ключами в CI
Никогда не хранить приватный ключ деплоера в CI secrets как hex-строку без дополнительной защиты. Лучше:
- AWS KMS + custom signer: ключ в KMS, signing через API. Утечка CI secrets не компрометирует ключ.
- Hardware wallet через Frame + WalletConnect: деплой с ручным подтверждением для mainnet.
- Отдельный деплой-кошелёк с минимальным балансом (только на gas), не связанный с treasury.
Multichain адресация в протоколе
Если контракты взаимодействуют между сетями (cross-chain вызовы), нужно управлять адресами peer-контрактов:
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");
}
}
Если используется LayerZero или Wormhole — они предоставляют собственные registry механизмы, но базовая логика та же.
Мониторинг задеплоенных контрактов
После деплоя на 5+ сетей нужно мониторить все инстансы:
// Агрегированный мониторинг через events
const deployments = require("./deployments.json");
for (const [network, data] of Object.entries(deployments)) {
const client = createPublicClient({ transport: http(data.rpcUrl) });
// Следим за критичными событиями на всех сетях
client.watchContractEvent({
address: data.contracts.core.address,
abi: CORE_ABI,
eventName: "EmergencyPause",
onLogs: (logs) => alertOps(`PAUSE on ${network}`, logs),
});
}
Стек и инструменты
| Компонент | Инструмент |
|---|---|
| Compile & test | Foundry (forge, cast, anvil) |
| Deploy scripts | TypeScript + viem или Foundry scripts |
| State tracking | deployments.json в git |
| CI/CD | GitHub Actions / GitLab CI |
| Key management | AWS KMS или Ledger для mainnet |
| Верификация | Etherscan API / Blockscout API |
| Мониторинг | Tenderly + собственные алерты |
Срок разработки системы деплоя: 1–2 недели для EVM-совместимых сетей. Добавление non-EVM сетей (Solana, TON) требует отдельного toolchain и значительно сложнее.







