Розробка контракту токен-бриджа
Токен-бридж — це інфраструктура, яка дозволяє перемістити активи між несумісними блокчейнами. З точки зору користувача все просто: заблокував 100 USDC на Ethereum, отримав 100 USDC на Arbitrum. За цією простотою криється один із найуразливіших класів смарт-контрактів: Ronin ($625M), Wormhole ($320M), Nomad ($190M) — усі взломи сталися через мости.
Це не випадок. Бридж за визначенням управляє заблокованими активами на одній ланцюзі та випускає синтетичні активи на іншій. Взломати bridge = вкрасти всі заблоковані кошти відразу. Складність посилюється тим, що безпека системи залежить від безпеки cross-chain повідомлень — а це принципово важка задача.
Архітектурні паттерни
Lock-and-Mint vs Burn-and-Release
Lock-and-Mint: токен блокується на source chain, на destination chain мінтується wrapped версія. Приклад: WBTC — BTC заблокований у кастодіяна, ERC-20 WBTC заминчений на Ethereum.
Перевага: оригінальний токен не потребує змін (не потрібна функція burn). Недолік: ліквідність фрагментована — wrapped токен на кожній ланцюзі окремий.
Burn-and-Release: нативний токен сжигається на source chain, розблоковується на destination chain. Потребує, щоб токен мав cross-chain-aware логіку або був спеціально спроектований (Circle CCTP для USDC використовує саме цю модель).
Модель liquidity pool (хаб-і-спок): на кожній ланцюзі пул ліквідності нативного токена. Користувач депонує на одній стороні, отримує з пулу на іншій. Так працює Hop Protocol та Across Protocol. Перевага: нативні токени на обох сторонах. Недолік: пулам потрібна ліквідність, інакше бридж не працює.
Для кастомного проекту: якщо токен ваш та ви контролюєте його контракт — Burn-and-Release простіше та безпечніше (немає заблокованих коштів як цілі атаки). Якщо бриджите чужий токен — Lock-and-Mint.
Моделі верифікації повідомлень
Це ключовий архітектурний вибір. Як destination chain знає, що подія на source chain дійсно сталася?
Optimistic верифікація (Nomad, Across): повідомлення вважається валідним, якщо ніхто його не оспорив протягом періоду (зазвичай 30 хвилин — кілька годин). Недолік: latency. Перевага: дешевше в експлуатації. Nomad був взломаний через помилку в логіці оспорювання — довіре повідомлення можна було реплікувати з іншим payload.
Multisig верифікація (більшість production-мостів): N з M валідаторів підписують підтвердження події. Wormhole використовував 19 guardians. Уразливість: компрометація порогового кількості ключів. Ronin був взломаний саме так — 5 з 9 валідатор ключів були скомпрометовані.
Light client верифікація (zkBridge, IBC): destination chain верифікує consensus proof source chain. Найбезпечніше, але дорого по газу. ZK-based верифікація (Succinct, =nil; Foundation) дозволяє стиснути proof, роблячи це практичним.
Native bridges (Arbitrum, Optimism canonical bridge): використовують власний fraud proof або validity proof механізм rollup. Максимально безпечно, але тільки для конкретної пари L1-L2 та з 7-денним withdrawal період (optimistic rollups).
Детальна реалізація Lock-and-Mint бриджа
Контракт source chain (Locker)
contract BridgeLocker {
mapping(uint32 => bool) public supportedChains;
mapping(bytes32 => bool) public processedNonces;
event TokensLocked(
address indexed token,
address indexed sender,
address indexed recipient,
uint256 amount,
uint32 destinationChain,
bytes32 nonce
);
function lock(
address token,
uint256 amount,
address recipient,
uint32 destinationChain
) external nonReentrant {
require(supportedChains[destinationChain], "Chain not supported");
require(amount > 0, "Zero amount");
// Генеруємо унікальний nonce для цього transfer
bytes32 nonce = keccak256(abi.encodePacked(
block.chainid,
destinationChain,
msg.sender,
recipient,
token,
amount,
block.timestamp,
blockhash(block.number - 1)
));
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
emit TokensLocked(token, msg.sender, recipient, amount, destinationChain, nonce);
}
function release(
address token,
address recipient,
uint256 amount,
bytes32 nonce,
bytes[] calldata signatures
) external {
require(!processedNonces[nonce], "Already processed");
require(_verifySignatures(token, recipient, amount, nonce, signatures), "Invalid signatures");
processedNonces[nonce] = true;
IERC20(token).safeTransfer(recipient, amount);
}
}
Критичний момент з nonce: він має бути непередбачуваним та унікальним. Простий лічильник (nonce++) уразливий — атакуючий може передбачити nonce та спробувати replay. Включення blockhash додає непередбачуваність.
Контракт destination chain (Minter)
contract BridgeMinter {
mapping(address => address) public wrappedTokens; // original → wrapped
mapping(bytes32 => bool) public mintedNonces;
function mint(
address originalToken,
address recipient,
uint256 amount,
bytes32 nonce,
bytes[] calldata signatures
) external {
require(!mintedNonces[nonce], "Already minted");
require(_verifySignatures(originalToken, recipient, amount, nonce, signatures), "Invalid");
mintedNonces[nonce] = true;
address wrapped = wrappedTokens[originalToken];
if (wrapped == address(0)) {
wrapped = _deployWrappedToken(originalToken);
wrappedTokens[originalToken] = wrapped;
}
IWrappedToken(wrapped).mint(recipient, amount);
emit TokensMinted(originalToken, wrapped, recipient, amount, nonce);
}
function burn(
address wrappedToken,
uint256 amount,
address recipient,
uint32 destinationChain
) external nonReentrant {
IWrappedToken(wrappedToken).burnFrom(msg.sender, amount);
// emit подію для relayers
emit TokensBurned(wrappedToken, msg.sender, recipient, amount, destinationChain);
}
}
Верифікація підписів валідаторів
function _verifySignatures(
address token,
address recipient,
uint256 amount,
bytes32 nonce,
bytes[] calldata signatures
) internal view returns (bool) {
require(signatures.length >= threshold, "Not enough signatures");
bytes32 messageHash = keccak256(abi.encodePacked(
block.chainid,
token,
recipient,
amount,
nonce
));
bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash(messageHash);
address lastSigner = address(0);
for (uint256 i = 0; i < signatures.length; i++) {
address signer = ECDSA.recover(ethSignedHash, signatures[i]);
require(isValidator[signer], "Not a validator");
require(signer > lastSigner, "Duplicate signer"); // захист від дублів
lastSigner = signer;
}
return true;
}
Захист від дублювання підписів через сортування — класичний паттерн з Gnosis Safe. Без перевірки signer > lastSigner один валідатор може підписати N разів та пройти threshold.
Relayer інфраструктура
Relayer — off-chain сервіс, який моніторить події на source chain та ініціює транзакції на destination chain.
Архітектура надійного relayer
class BridgeRelayer {
async watchSourceChain() {
const filter = lockerContract.filters.TokensLocked();
sourceProvider.on(filter, async (event) => {
// Чекаємо підтверджень (finality)
const receipt = await this.waitForFinality(event.transactionHash);
// Збираємо підписи від валідаторів
const signatures = await this.collectSignatures(event);
// Відправляємо на destination chain з retry
await this.submitWithRetry(event, signatures);
});
}
async waitForFinality(txHash: string): Promise<TransactionReceipt> {
// Для Ethereum: 12 блоків (~2.5 хвилини)
// Для Polygon: 128 блоків (Bor finality)
// Для Arbitrum: достатньо 1 блока (sequencer finality для L2→L2)
const CONFIRMATIONS = this.config.requiredConfirmations[this.sourceChainId];
return await sourceProvider.waitForTransaction(txHash, CONFIRMATIONS);
}
}
Finality — критичний параметр. Ethereum має probabilistic finality, але з PoS checkpoint finality кожні ~12 хвилин. Якщо relayer відправляє mint до finality source-транзакції, reorg на source chain створює ситуацію: mint сталося, але lock — ні. Так були можливі деякі атаки в ранніх мостах.
Retry та idempotency
Destination chain транзакція може fail: insufficient gas, nonce collision, destination chain congestion. Relayer повинен retry з exponential backoff. Idempotency забезпечується перевіркою mintedNonces[nonce] у контракті — повторний mint з тим же nonce відхиляється.
Безпека: топ-5 векторів атак
1. Replay attack між сітями
Повідомлення, валідне для Arbitrum, реплікується на Optimism. Захист: включити block.chainid (EIP-155) та destinationChainId у підписане повідомлення.
2. Signature malleability
ECDSA допускає два валідних значення s для однієї підписи. OpenZeppelin ECDSA.recover з версії 4.7.3 перевіряє s у нижній половині кривої. Ніколи не використовуйте ecrecover напрямик.
3. Reentrancy при release/mint
Якщо release вызиває safeTransfer до оновлення processedNonces — атакуючий через callback може повторити виклик. Checks-Effects-Interactions паттерн + nonReentrant обов'язкові.
4. Компрометація ключів валідаторів
Рішення: threshold signature scheme (TSS) замість звичайного multisig. TSS генерує розподілений ключ — ніхто не знає повний приватний ключ. Навіть при компрометації одного учасника ключ не відновимий. Бібліотеки: tss-lib (Binance), Silence Laboratories SDK.
5. Нескінченний mint через upgradeable proxy
Якщо Minter — upgradeable proxy, upgrade функція має бути під timelock + multisig. Wormhole Solana exploit був через прямий виклик deprecated функції без перевірки.
Тестування
Foundry ідеально підходить для бриджей: fork тесті дозволяють працювати з реальним state mainnet.
function test_bridgeRoundTrip() public {
// Fork Ethereum mainnet
vm.createSelectFork(vm.envString("ETH_RPC"), 19_000_000);
// Симулюємо lock на Ethereum
vm.prank(user);
locker.lock(USDC, 1000e6, user, ARBITRUM_CHAIN_ID);
// Збираємо підписи валідаторів (mock)
bytes[] memory sigs = _signMessage(messageHash, validatorKeys);
// Переключаємося на Arbitrum fork
vm.createSelectFork(vm.envString("ARB_RPC"), 180_000_000);
// Мінтимо на Arbitrum
minter.mint(USDC_ARB, user, 1000e6, nonce, sigs);
assertEq(wrappedUSDC.balanceOf(user), 1000e6);
}
Тести для edge-cases: double-spend через nonce replay, неправильний chain ID у підписі, threshold підписів з дублюючимися адресами.
Стек та терміни
Контракти: Solidity 0.8.x + Foundry + OpenZeppelin 5.x + Hardhat (для multi-chain deploy скриптів). Relayer: TypeScript + viem + BullMQ (черга задач) + PostgreSQL (зберігання pending transfers). Мониторинг: Tenderly для алертів + Grafana для метрик relayer.
| Компонент | Складність | Термін |
|---|---|---|
| Locker + Minter контракти | Висока | 2–3 тижні |
| Signature verification | Середня | 1 тиждень |
| Relayer сервіс | Висока | 2–3 тижні |
| Wrapped token factory | Низька | 3–5 днів |
| Тести (unit + fork) | Висока | 2 тижні |
| Аудит (зовнішній) | — | 3–6 тижнів |
Аудит — обов'язковий. Не як формальність, а як умова деплоя. Мінімум один спеціалізований аудитор з досвідом bridge-проектів. Контракт, що управляє заблокованими коштами, без аудиту — це ризик втрати всього TVL.
Загальний термін від kick-off до mainnet: 3–5 місяців з врахуванням аудиту. Вартість розраховується після уточнення chain пар, моделі верифікації та вимог до decentralization validator-мережі.







