Token Migration System Development (v1 → v2)

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Token Migration System Development (v1 → v2)
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Token Bridge Contract Development

A token bridge is infrastructure that enables moving assets between incompatible blockchains. From the user's perspective it's simple: locked 100 USDC on Ethereum, got 100 USDC on Arbitrum. Behind that simplicity lies one of the most vulnerable classes of smart contracts: Ronin ($625M), Wormhole ($320M), Nomad ($190M) — all hacks happened through bridges.

This is not coincidence. A bridge by definition manages locked assets on one chain and issues synthetic assets on another. Hacking a bridge = stealing all locked funds at once. Complexity is increased by the fact that system security depends on cross-chain message security — this is a fundamentally hard problem.

Architectural Patterns

Lock-and-Mint vs Burn-and-Release

Lock-and-Mint: token is locked on source chain, wrapped version is minted on destination chain. Example: WBTC — BTC locked with custodian, ERC-20 WBTC minted on Ethereum.

Advantage: original token needs no modifications (no burn function needed). Disadvantage: liquidity is fragmented — wrapped token on each chain is separate.

Burn-and-Release: native token is burned on source chain, unlocked on destination chain. Requires token to have cross-chain-aware logic or be specially designed (Circle CCTP for USDC uses this model).

Liquidity pool model (hub-and-spoke): liquidity pool of native token on each chain. User deposits on one side, receives from pool on other. How Hop Protocol and Across Protocol work. Advantage: native tokens on both sides. Disadvantage: pools need liquidity, otherwise bridge doesn't work.

For custom projects: if your token and you control its contract — Burn-and-Release is simpler and safer (no locked funds as attack target). If bridging someone else's token — Lock-and-Mint.

Message Verification Models

This is the key architectural choice. How does destination chain know that an event on source chain actually happened?

Optimistic verification (Nomad, Across): message is considered valid if no one disputes it within period (usually 30 minutes to several hours). Disadvantage: latency. Advantage: cheaper to operate. Nomad was hacked due to dispute logic error — trusted message could be replicated with different payload.

Multisig verification (most production bridges): N of M validators sign event confirmation. Wormhole used 19 guardians. Vulnerability: compromising threshold number of keys. Ronin was hacked this way — 5 of 9 validator keys were compromised.

Light client verification (zkBridge, IBC): destination chain verifies consensus proof of source chain. Most secure but expensive on gas. ZK-based verification (Succinct, =nil; Foundation) allows compressing proof, making it practical.

Native bridges (Arbitrum, Optimism canonical bridge): use rollup's own fraud proof or validity proof mechanism. Most secure, but only for specific L1-L2 pair and with 7-day withdrawal period (optimistic rollups).

Detailed Lock-and-Mint Bridge Implementation

Source chain contract (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");
        
        // Generate unique nonce for this 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);
    }
}

Critical moment with nonce: it must be unpredictable and unique. Simple counter (nonce++) is vulnerable — attacker can precompute nonce and attempt replay. Including blockhash adds unpredictability.

Destination chain contract (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 event for relayers
        emit TokensBurned(wrappedToken, msg.sender, recipient, amount, destinationChain);
    }
}

Validator Signature Verification

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"); // protection from duplicates
        lastSigner = signer;
    }
    return true;
}

Protection against signature duplication through sorting — classic pattern from Gnosis Safe. Without signer > lastSigner check, one validator can sign N times and pass threshold.

Relayer Infrastructure

Relayer is off-chain service that monitors events on source chain and initiates transactions on destination chain.

Reliable Relayer Architecture

class BridgeRelayer {
    async watchSourceChain() {
        const filter = lockerContract.filters.TokensLocked();
        
        sourceProvider.on(filter, async (event) => {
            // Wait for confirmations (finality)
            const receipt = await this.waitForFinality(event.transactionHash);
            
            // Collect signatures from validators
            const signatures = await this.collectSignatures(event);
            
            // Submit to destination chain with retry
            await this.submitWithRetry(event, signatures);
        });
    }
    
    async waitForFinality(txHash: string): Promise<TransactionReceipt> {
        // For Ethereum: 12 blocks (~2.5 minutes)
        // For Polygon: 128 blocks (Bor finality)
        // For Arbitrum: 1 block is enough (sequencer finality for L2→L2)
        const CONFIRMATIONS = this.config.requiredConfirmations[this.sourceChainId];
        return await sourceProvider.waitForTransaction(txHash, CONFIRMATIONS);
    }
}

Finality is critical parameter. Ethereum has probabilistic finality, but with PoS checkpoint finality every ~12 minutes. If relayer submits mint before source transaction finality, reorg on source chain creates situation: mint happened but lock didn't. This enabled some early bridge attacks.

Retry and Idempotency

Destination chain transaction might fail: insufficient gas, nonce collision, destination chain congestion. Relayer should retry with exponential backoff. Idempotency is ensured by mintedNonces[nonce] check in contract — repeated mint with same nonce is rejected.

Security: Top 5 Attack Vectors

1. Replay attack between networks

Message valid for Arbitrum is replicated on Optimism. Protection: include block.chainid (EIP-155) and destinationChainId in signed message.

2. Signature malleability

ECDSA allows two valid s values for one signature. OpenZeppelin ECDSA.recover since version 4.7.3 checks s in lower half of curve. Never use ecrecover directly.

3. Reentrancy in release/mint

If release calls safeTransfer before updating processedNonces — attacker through callback can repeat the call. Checks-Effects-Interactions pattern + nonReentrant are mandatory.

4. Validator key compromise

Solution: threshold signature scheme (TSS) instead of regular multisig. TSS generates distributed key — no one knows full private key. Even with one participant compromised, key is unrecoverable. Libraries: tss-lib (Binance), Silence Laboratories SDK.

5. Infinite mint through upgradeable proxy

If Minter is upgradeable proxy, upgrade function must be under timelock + multisig. Wormhole Solana exploit was through direct call to deprecated function without check.

Testing

Foundry is perfect for bridges: fork tests allow working with real mainnet state.

function test_bridgeRoundTrip() public {
    // Fork Ethereum mainnet
    vm.createSelectFork(vm.envString("ETH_RPC"), 19_000_000);
    
    // Simulate lock on Ethereum
    vm.prank(user);
    locker.lock(USDC, 1000e6, user, ARBITRUM_CHAIN_ID);
    
    // Collect validator signatures (mock)
    bytes[] memory sigs = _signMessage(messageHash, validatorKeys);
    
    // Switch to Arbitrum fork
    vm.createSelectFork(vm.envString("ARB_RPC"), 180_000_000);
    
    // Mint on Arbitrum
    minter.mint(USDC_ARB, user, 1000e6, nonce, sigs);
    assertEq(wrappedUSDC.balanceOf(user), 1000e6);
}

Tests for edge cases: double-spend through nonce replay, wrong chain ID in signature, threshold signatures with duplicate addresses.

Stack and Timeline

Contracts: Solidity 0.8.x + Foundry + OpenZeppelin 5.x + Hardhat (for multi-chain deploy scripts). Relayer: TypeScript + viem + BullMQ (task queue) + PostgreSQL (pending transfers storage). Monitoring: Tenderly for alerts + Grafana for relayer metrics.

Component Complexity Timeline
Locker + Minter contracts High 2–3 weeks
Signature verification Medium 1 week
Relayer service High 2–3 weeks
Wrapped token factory Low 3–5 days
Tests (unit + fork) High 2 weeks
Audit (external) 3–6 weeks

Audit is mandatory. Not as formality, but as deployment condition. Minimum one specialized auditor with bridge project experience. Contract managing locked funds, without audit — this is risk of losing entire TVL.

Total timeline from kick-off to mainnet: 3–5 months with audit. Cost calculated after clarifying chain pairs, verification model, and decentralization requirements for validator network.