Розробка системи токен-міграції (v1 → v2)

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка системи токен-міграції (v1 → v2)
Середній
~3-5 днів
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1307
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1219
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    920
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1147
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    610
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    885

Розробка контракту токен-бриджа

Токен-бридж — це інфраструктура, яка дозволяє перемістити активи між несумісними блокчейнами. З точки зору користувача все просто: заблокував 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-мережі.