Разработка cross-chain моста
Cross-chain мост — это система, которая позволяет перемещать активы или данные между независимыми блокчейн-сетями. На первый взгляд задача простая: заблокировать токены на цепи A, выпустить эквивалент на цепи B. На практике — это один из наиболее технически и security-сложных продуктов в Web3. Взломы мостов составляют большую часть всех убытков в крипто ($2B+ потеряно на Ronin, Wormhole, Nomad, Harmony).
Архитектурные паттерны
Lock-and-Mint (wrapped tokens)
Классическая схема:
- Пользователь блокирует ETH в Lock контракте на Ethereum
- Мост выпускает wETH (wrapped ETH) на Polygon
- При обратном переводе: burn wETH на Polygon → unlock ETH на Ethereum
Риски: весь залог сосредоточен в одном контракте на Ethereum. Взлом = потеря всего locked TVL. Wormhole потерял $320M именно так.
Burn-and-Mint (native tokens)
Применяется для токенов с cross-chain minting capability (USDC через Circle CCTP, USDT через Tether bridge):
- Burn USDC на Ethereum (Circle уничтожает обеспечение)
- Mint нативный USDC на Arbitrum (Circle выпускает новое обеспечение)
Преимущество: нет locked TVL = нет single point of failure. Но требует контроля над token contract на всех цепях.
Liquidity Pool (liquidity network)
Используется в Stargate, Hop Protocol:
- На каждой цепи пул ликвидности
- Пользователь вносит USDC на Ethereum пул → получает USDC из Arbitrum пула
- LP провайдеры получают комиссии за обеспечение ликвидности
Преимущество: быстрое исполнение без ожидания finality. Риск: imbalanced pools (больше изъятий с одной стороны чем пополнений).
Native verification (light client bridges)
Самый децентрализованный вариант: смарт-контракт на цепи B верифицирует заголовки блоков цепи A через light client. Доказывает что транзакция произошла без доверия к валидаторам.
Примеры: ICS-23 (Cosmos IBC), Rainbow Bridge (NEAR → Ethereum). Сложность: высокая gas стоимость верификации, особенно для PoW (Ethereum pre-merge).
Optimistic bridges
Оптимистичная верификация: сообщения принимаются как валидные, но есть период (обычно 30 мин — 7 дней) в течение которого watcher может оспорить и заблокировать мошенническую транзакцию. Используется в Across Protocol, Connext (частично).
Трейдофф: безопасность vs скорость. 7-дневный период = медленно, но очень безопасно.
Детали реализации
Lock контракт (Ethereum)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract BridgeLock is ReentrancyGuard, Pausable, AccessControl {
bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");
// TVL лимиты для защиты
mapping(address => uint256) public tokenTVLLimits;
mapping(address => uint256) public tokenCurrentTVL;
// Ежедневные лимиты
mapping(address => uint256) public dailyBridgeLimit;
mapping(address => uint256) public dailyBridgedAmount;
mapping(address => uint256) public lastResetTimestamp;
// Nonce для предотвращения replay
mapping(bytes32 => bool) public processedDeposits;
event Deposit(
bytes32 indexed depositId,
address indexed sender,
address indexed token,
uint256 amount,
uint256 destinationChainId,
address recipient
);
function deposit(
address token,
uint256 amount,
uint256 destinationChainId,
address recipient
) external nonReentrant whenNotPaused returns (bytes32 depositId) {
require(amount > 0, "Zero amount");
require(tokenTVLLimits[token] > 0, "Token not supported");
// TVL check
require(
tokenCurrentTVL[token] + amount <= tokenTVLLimits[token],
"TVL limit exceeded"
);
// Daily limit check
_checkAndUpdateDailyLimit(token, amount);
// Генерируем уникальный ID депозита
depositId = keccak256(abi.encodePacked(
msg.sender, token, amount, destinationChainId, recipient,
block.chainid, block.number, block.timestamp
));
require(!processedDeposits[depositId], "Duplicate deposit");
processedDeposits[depositId] = true;
// Переводим токены
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
tokenCurrentTVL[token] += amount;
emit Deposit(depositId, msg.sender, token, amount, destinationChainId, recipient);
}
// Только RELAYER_ROLE может разблокировать
function unlock(
bytes32 depositId,
address token,
uint256 amount,
address recipient,
bytes calldata proof
) external onlyRole(RELAYER_ROLE) nonReentrant {
// Верифицируем proof (подписи валидаторов или merkle proof)
require(_verifyProof(depositId, token, amount, recipient, proof), "Invalid proof");
// Идемпотентность
require(!processedUnlocks[depositId], "Already unlocked");
processedUnlocks[depositId] = true;
tokenCurrentTVL[token] -= amount;
IERC20(token).safeTransfer(recipient, amount);
emit Unlock(depositId, recipient, token, amount);
}
}
Mint контракт (целевая цепь)
contract BridgeMint is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
// Маппинг originTxHash → уже обработан
mapping(bytes32 => bool) public mintedDeposits;
function mint(
bytes32 depositId,
address recipient,
uint256 amount,
bytes calldata validatorSignatures
) external onlyRole(MINTER_ROLE) {
require(!mintedDeposits[depositId], "Already minted");
// Верифицируем M-of-N подписи от валидаторов
_verifyValidatorSignatures(depositId, recipient, amount, validatorSignatures);
mintedDeposits[depositId] = true;
_mint(recipient, amount);
emit Minted(depositId, recipient, amount);
}
function burn(uint256 amount, uint256 targetChainId, address recipient) external {
_burn(msg.sender, amount);
emit Burned(msg.sender, amount, targetChainId, recipient);
}
}
Валидатор / Relayer
Off-chain компонент, который мониторит события на исходной цепи и инициирует mint/unlock на целевой:
class BridgeRelayer {
private validators: Signer[];
private threshold: number;
async watchSourceChain() {
this.sourceBridge.on("Deposit", async (depositId, sender, token, amount, destChain, recipient, event) => {
// Ждём достаточно подтверждений
await this.waitForConfirmations(event.blockNumber, REQUIRED_CONFIRMATIONS);
// Каждый валидатор подписывает данные депозита
const signatures = await this.collectValidatorSignatures(
depositId, token, amount, destChain, recipient
);
if (signatures.length >= this.threshold) {
await this.executeMint(destChain, depositId, recipient, amount, signatures);
}
});
}
private async collectValidatorSignatures(
depositId: string,
token: string,
amount: bigint,
destChain: number,
recipient: string
): Promise<string[]> {
const messageHash = ethers.solidityPackedKeccak256(
["bytes32", "address", "uint256", "uint256", "address"],
[depositId, token, amount, destChain, recipient]
);
const signatures = await Promise.all(
this.validators.map(v => v.signMessage(ethers.getBytes(messageHash)))
);
return signatures;
}
}
Безопасность — критические моменты
Replay attack protection. Одна и та же подписанная транзакция не должна быть исполнена дважды или на другой цепи. Решение: включать chainId и уникальный nonce в подписываемые данные.
Signature malleability. ECDSA подписи математически malleable — из одной можно получить другую валидную. OpenZeppelin ECDSA.recover решает это, но важно использовать его, а не raw ecrecover.
Reentrancy. unlock/mint функции должны обновлять состояние ДО внешних вызовов (transfer). Паттерн checks-effects-interactions или nonReentrant модификатор.
TVL caps. Ограничение суммарного TVL на контракте снижает максимальный ущерб от взлома. Если cap $10M — максимальный loss $10M, не $500M.
Timelock для upgrades. Изменения в bridge контракте должны иметь timelock (минимум 48 часов). Это даёт пользователям время вывести средства если изменение кажется подозрительным.
Emergency pause. Guardian role может остановить все входящие/исходящие переводы при обнаружении аномалий. Автоматически через circuit breaker (аномально большой withdraw).
Мониторинг
Мост без мониторинга — это не production. Минимальный набор алертов:
- TVL резкое снижение (>10% за 5 минут)
- Аномально крупные транзакции (>1% от TVL)
- Несоответствие между locked и minted (invariant check)
- Validator downtime (нет подписей от валидатора >N минут)
Выбор существующего решения vs собственный мост
Собственный мост нужен если:
- Кастомная токен-экономика (не стандартный ERC-20)
- Специфические требования к безопасности
- Несупортируемые цепи в существующих протоколах
- Полный контроль над fee структурой
Использовать существующий (LayerZero, Axelar, CCIP) если:
- Стандартный ERC-20 bridging
- Нужна скорость запуска
- Нет ресурсов на security audit кастомного bridge
Стоимость аудита кастомного bridge — $50-200k. Это обязательная статья, не опциональная.
Стек
| Компонент | Технология |
|---|---|
| Smart contracts | Solidity 0.8.x + OpenZeppelin + Foundry |
| Validator network | Node.js + TypeScript + ethers.js |
| Relayer | Node.js + Bull queue + Redis |
| Monitoring | Grafana + Prometheus + PagerDuty |
| Frontend | React + wagmi + viem |
| Infrastructure | AWS ECS + RDS + CloudWatch |
Сроки
- Базовый lock-and-mint (2 цепи, ERC-20): 6-8 недель
- Multi-chain поддержка (+3 цепи): +4-6 недель
- Security audit: обязателен, 4-8 недель
- Production hardening + мониторинг: +3-4 недели
- Итого: 4-5 месяцев







