Cross-Chain Bridge Development

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
Cross-Chain Bridge Development
Complex
from 2 weeks to 3 months
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

Cross-chain Bridge Development

A cross-chain bridge is a system that allows moving assets or data between independent blockchain networks. At first glance, the task seems simple: lock tokens on chain A, issue equivalent on chain B. In practice, it's one of the most technically and security-complex products in Web3. Bridge exploits comprise a large portion of all crypto losses ($2B+ lost on Ronin, Wormhole, Nomad, Harmony).

Architectural Patterns

Lock-and-Mint (wrapped tokens)

The classic scheme:

  1. User locks ETH in Lock contract on Ethereum
  2. Bridge issues wETH (wrapped ETH) on Polygon
  3. On reverse transfer: burn wETH on Polygon → unlock ETH on Ethereum

Risks: all collateral concentrated in one contract on Ethereum. Hack = loss of entire locked TVL. Wormhole lost $320M exactly this way.

Burn-and-Mint (native tokens)

Applied for tokens with cross-chain minting capability (USDC via Circle CCTP, USDT via Tether bridge):

  1. Burn USDC on Ethereum (Circle destroys backing)
  2. Mint native USDC on Arbitrum (Circle issues new backing)

Advantage: no locked TVL = no single point of failure. But requires control over token contract on all chains.

Liquidity Pool (liquidity network)

Used in Stargate, Hop Protocol:

  1. Liquidity pool on each chain
  2. User deposits USDC on Ethereum pool → receives USDC from Arbitrum pool
  3. LP providers receive fees for providing liquidity

Advantage: fast execution without waiting for finality. Risk: imbalanced pools (more withdrawals from one side than deposits).

Native verification (light client bridges)

Most decentralized option: smart contract on chain B verifies block headers of chain A through light client. Proves transaction occurred without relying on validators.

Examples: ICS-23 (Cosmos IBC), Rainbow Bridge (NEAR → Ethereum). Complexity: high gas cost for verification, especially for PoW (Ethereum pre-merge).

Optimistic bridges

Optimistic verification: messages accepted as valid, but there's a period (usually 30 min — 7 days) during which a watcher can challenge and block fraudulent transaction. Used in Across Protocol, Connext (partially).

Tradeoff: security vs speed. 7-day period = slow, but very safe.

Implementation Details

Lock Contract (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 limits for protection
    mapping(address => uint256) public tokenTVLLimits;
    mapping(address => uint256) public tokenCurrentTVL;
    
    // Daily limits
    mapping(address => uint256) public dailyBridgeLimit;
    mapping(address => uint256) public dailyBridgedAmount;
    mapping(address => uint256) public lastResetTimestamp;
    
    // Nonce for replay prevention
    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);
        
        // Generate unique deposit 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;
        
        // Transfer tokens
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        tokenCurrentTVL[token] += amount;
        
        emit Deposit(depositId, msg.sender, token, amount, destinationChainId, recipient);
    }
    
    // Only RELAYER_ROLE can unlock
    function unlock(
        bytes32 depositId,
        address token,
        uint256 amount,
        address recipient,
        bytes calldata proof
    ) external onlyRole(RELAYER_ROLE) nonReentrant {
        // Verify proof (validator signatures or merkle proof)
        require(_verifyProof(depositId, token, amount, recipient, proof), "Invalid proof");
        
        // Idempotency
        require(!processedUnlocks[depositId], "Already unlocked");
        processedUnlocks[depositId] = true;
        
        tokenCurrentTVL[token] -= amount;
        IERC20(token).safeTransfer(recipient, amount);
        
        emit Unlock(depositId, recipient, token, amount);
    }
}

Mint Contract (destination chain)

contract BridgeMint is ERC20, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    
    // Mapping originTxHash → already processed
    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");
        
        // Verify M-of-N signatures from validators
        _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);
    }
}

Validator / Relayer

Off-chain component that monitors source chain events and initiates mint/unlock on destination:

class BridgeRelayer {
  private validators: Signer[];
  private threshold: number;
  
  async watchSourceChain() {
    this.sourceBridge.on("Deposit", async (depositId, sender, token, amount, destChain, recipient, event) => {
      // Wait for sufficient confirmations
      await this.waitForConfirmations(event.blockNumber, REQUIRED_CONFIRMATIONS);
      
      // Each validator signs deposit data
      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;
  }
}

Security — Critical Points

Replay attack protection. Same signed transaction should not execute twice or on another chain. Solution: include chainId and unique nonce in signed data.

Signature malleability. ECDSA signatures are mathematically malleable — one can be transformed into another valid one. OpenZeppelin ECDSA.recover solves this, but it's important to use it, not raw ecrecover.

Reentrancy. unlock/mint functions must update state BEFORE external calls (transfer). Checks-effects-interactions pattern or nonReentrant modifier.

TVL caps. Limiting total TVL on contract reduces maximum hack damage. If cap is $10M — max loss is $10M, not $500M.

Timelock for upgrades. Bridge contract changes must have timelock (minimum 48 hours). This gives users time to withdraw funds if change seems suspicious.

Emergency pause. Guardian role can stop all incoming/outgoing transfers when anomalies detected. Automatically via circuit breaker (abnormally large withdrawal).

Monitoring

Bridge without monitoring is not production. Minimum set of alerts:

  • TVL sudden drop (>10% in 5 minutes)
  • Abnormally large transactions (>1% of TVL)
  • Mismatch between locked and minted (invariant check)
  • Validator downtime (no signatures from validator >N minutes)

Choosing Existing Solution vs Custom Bridge

Build custom bridge if:

  • Custom token economics (not standard ERC-20)
  • Specific security requirements
  • Unsupported chains in existing protocols
  • Full control over fee structure

Use existing (LayerZero, Axelar, CCIP) if:

  • Standard ERC-20 bridging
  • Need fast launch
  • No resources for custom bridge security audit

Custom bridge audit cost — $50-200k. This is mandatory, not optional.

Stack

Component Technology
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

Timelines

  • Basic lock-and-mint (2 chains, ERC-20): 6-8 weeks
  • Multi-chain support (+3 chains): +4-6 weeks
  • Security audit: mandatory, 4-8 weeks
  • Production hardening + monitoring: +3-4 weeks
  • Total: 4-5 months