LogoLogo
WebsiteLinksMedia KitGitHub
  • Introduction
  • Sonic
    • Overview
    • S Token
    • Wallets
    • Sonic Gateway
    • Build on Sonic
      • Getting Started
      • Deploy Contracts
      • Verify Contracts
      • Tooling and Infra
      • Native USDC
      • Contract Addresses
      • Network Parameters
      • Programmatic Gateway
      • Learn Solidity
    • Node Deployment
      • Archive Node
      • Validator Node
      • Node Recovery
    • FAQ
  • Funding
    • Sonic Airdrop
      • Sonic Points
      • Sonic Gems
      • Game Gems
      • Sonic Boom
        • Winners
    • Fee Monetization
      • Apply
      • Dispute
      • FeeM Vault
    • Innovator Fund
    • Sonic & Sodas
  • MIGRATION
    • Overview
    • App Token Migration
    • Migration FAQ
  • Technology
    • Overview
    • Consensus
    • Database Storage
    • Proof of Stake
    • Audits
Powered by GitBook

© 2025 Sonic Labs

On this page
  • Sonic Bridge: Programmatic Usage Guide
  • Contract Addresses
  • Setup
  • Bridge Operations
  • Complete Example
  • Required ABIs
  • Important Notes
Export as PDF
  1. Sonic
  2. Build on Sonic

Programmatic Gateway

This page explains how to use the Sonic Gateway in your application or script to transfer ERC-20 assets from Ethereum to Sonic and back.

Sonic Bridge: Programmatic Usage Guide

Contract Addresses

// Ethereum (L1)
const ETH_CONTRACTS = {
    TOKEN_DEPOSIT: "0xa1E2481a9CD0Cb0447EeB1cbc26F1b3fff3bec20",
    TOKEN_PAIRS: "0xf2b1510c2709072C88C5b14db90Ec3b6297193e4",
    STATE_ORACLE: "0xB7e8CC3F5FeA12443136f0cc13D81F109B2dEd7f"
};

// Sonic (L2)
const SONIC_CONTRACTS = {
    BRIDGE: "0x9Ef7629F9B930168b76283AdD7120777b3c895b3",
    TOKEN_PAIRS: "0x134E4c207aD5A13549DE1eBF8D43c1f49b00ba94",
    STATE_ORACLE: "0x836664B0c0CB29B7877bCcF94159CC996528F2C3"
};

Setup

// Network RPC endpoints
const ETHEREUM_RPC = "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY";
const SONIC_RPC = "https://rpc.soniclabs.com";

// Initialize providers
const ethProvider = new ethers.providers.JsonRpcProvider(ETHEREUM_RPC);
const sonicProvider = new ethers.providers.JsonRpcProvider(SONIC_RPC);

// Initialize signer with your private key
const PRIVATE_KEY = "your-private-key";
const ethSigner = new ethers.Wallet(PRIVATE_KEY, ethProvider);
const sonicSigner = new ethers.Wallet(PRIVATE_KEY, sonicProvider);

Bridge Operations

1. Ethereum to Sonic Transfer

async function bridgeToSonic(tokenAddress, amount) {
    // 1. Check if token is supported
    const tokenPairs = new ethers.Contract(ETH_CONTRACTS.TOKEN_PAIRS, TOKEN_PAIRS_ABI, ethProvider);
    const mintedToken = await tokenPairs.originalToMinted(tokenAddress);
    if (mintedToken === ethers.constants.AddressZero) {
        throw new Error("Token not supported");
    }

    // 2. Approve token spending
    const token = new ethers.Contract(tokenAddress, ERC20_ABI, ethSigner);
    const approveTx = await token.approve(ETH_CONTRACTS.TOKEN_DEPOSIT, amount);
    await approveTx.wait();

    // 3. Deposit tokens
    const deposit = new ethers.Contract(ETH_CONTRACTS.TOKEN_DEPOSIT, TOKEN_DEPOSIT_ABI, ethSigner);
    const tx = await deposit.deposit(Date.now(), tokenAddress, amount);
    const receipt = await tx.wait();

    return {
        transactionHash: receipt.transactionHash,
        mintedToken,
        blockNumber: receipt.blockNumber,
        depositId: receipt.events.find(e => e.event === 'Deposit').args.id
    };
}

2. Claim Tokens on Sonic

async function waitForStateUpdate(depositBlockNumber) {
    const stateOracle = new ethers.Contract(SONIC_CONTRACTS.STATE_ORACLE, STATE_ORACLE_ABI, sonicProvider);
    
    while (true) {
        const currentBlockNum = await stateOracle.lastBlockNum();
        if (currentBlockNum >= depositBlockNumber) {
            return currentBlockNum;
        }
        await new Promise(resolve => setTimeout(resolve, 30000)); // Check every 30 seconds
    }
}

async function generateProof(depositId, blockNum) {
    // Generate storage slot for deposit
    const storageSlot = ethers.utils.keccak256(
        ethers.utils.defaultAbiCoder.encode(['uint256', 'uint8'], [depositId, 7])
    );
    
    // Get proof from Ethereum node
    const proof = await ethProvider.send("eth_getProof", [
        ETH_CONTRACTS.TOKEN_DEPOSIT,
        [storageSlot],
        ethers.utils.hexValue(blockNum) // important to use current stateOracle.lastBlockNum
    ]);
    
    // Encode proof in required format
    return ethers.utils.RLP.encode([
        ethers.utils.RLP.encode(proof.accountProof),
        ethers.utils.RLP.encode(proof.storageProof[0].proof)
    ]);
}

async function claimOnSonic(depositBlockNumber, depositId, tokenAddress, amount) {
    // 1. Wait for state oracle update
    console.log("Waiting for state oracle update to block ", depositBlockNumber);
    const blockNum = await waitForStateUpdate(depositBlockNumber);
    
    // 2. Generate proof
    console.log("Generating proof...");
    const proof = await generateProof(depositId, blockNum);
    
    // 3. Claim tokens with proof
    console.log("Claiming tokens...");
    const bridge = new ethers.Contract(SONIC_CONTRACTS.BRIDGE, BRIDGE_ABI, sonicSigner);
    const tx = await bridge.claim(depositId, tokenAddress, amount, proof);
    const receipt = await tx.wait();

    return receipt.transactionHash;
}

3. Sonic to Ethereum Transfer

async function bridgeToEthereum(tokenAddress, amount) {
    // 1. Check if token is supported
    const tokenPairs = new ethers.Contract(SONIC_CONTRACTS.TOKEN_PAIRS, TOKEN_PAIRS_ABI, sonicProvider);
    const originalToken = await tokenPairs.mintedToOriginal(tokenAddress);
    if (originalToken === ethers.constants.AddressZero) {
        throw new Error("Token not supported");
    }
    
    // 2. Approve token spending
    const token = new ethers.Contract(tokenAddress, ERC20_ABI, sonicSigner);
    const approveTx = await token.approve(SONIC_CONTRACTS.BRIDGE, amount);
    console.log("waiting... ", approveTx.hash);
    await approveTx.wait();

    // 3. Initiate withdrawal
    const bridge = new ethers.Contract(SONIC_CONTRACTS.BRIDGE, BRIDGE_ABI, sonicSigner);
    const tx = await bridge.withdraw(Date.now(), originalToken, amount);
    const receipt = await tx.wait();

    return {
        transactionHash: receipt.transactionHash,
        originalToken,
        blockNumber: receipt.blockNumber,
        withdrawalId: receipt.events.find(e => e.event === 'Withdrawal').args.id
    };
}

4. Claim Tokens on Ethereum

async function waitForEthStateUpdate(withdrawalBlockNumber) {
    const stateOracle = new ethers.Contract(ETH_CONTRACTS.STATE_ORACLE, STATE_ORACLE_ABI, ethProvider);
    
    while (true) {
        const currentBlockNum = await stateOracle.lastBlockNum();
        if (currentBlockNum >= withdrawalBlockNumber) {
            return currentBlockNum;
        }
        await new Promise(resolve => setTimeout(resolve, 30000)); // Check every 30 seconds
    }
}

async function generateWithdrawalProof(withdrawalId, blockNum) {
    // Generate storage slot for withdrawal
    const storageSlot = ethers.utils.keccak256(
        ethers.utils.defaultAbiCoder.encode(['uint256', 'uint8'], [withdrawalId, 1])
    );
    
    // Get proof from Sonic node
    const proof = await sonicProvider.send("eth_getProof", [
        SONIC_CONTRACTS.BRIDGE,
        [storageSlot],
        ethers.utils.hexValue(blockNum) // important to use current stateOracle.lastBlockNum
    ]);
    
    // Encode proof in required format
    return ethers.utils.RLP.encode([
        ethers.utils.RLP.encode(proof.accountProof),
        ethers.utils.RLP.encode(proof.storageProof[0].proof)
    ]);
}

async function claimOnEthereum(withdrawalBlockNumber, withdrawalId, tokenAddress, amount) {
    // 1. Wait for state oracle update
    console.log("Waiting for state oracle update to block ", withdrawalBlockNumber);
    const blockNum = await waitForEthStateUpdate(withdrawalBlockNumber);
    
    // 2. Generate proof
    console.log("Generating proof...");
    const proof = await generateWithdrawalProof(withdrawalId, blockNum);
    
    // 3. Claim tokens with proof
    console.log("Claim tokens with proof...");
    const deposit = new ethers.Contract(ETH_CONTRACTS.TOKEN_DEPOSIT, TOKEN_DEPOSIT_ABI, ethSigner);
    const tx = await deposit.claim(withdrawalId, tokenAddress, amount, proof);
    const receipt = await tx.wait();

    return receipt.transactionHash;
}

Complete Example

async function bridgeUSDC() {
    try {
        // USDC details
        const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
        const amount = ethers.utils.parseUnits("100", 6); // USDC has 6 decimals

        // 1. Bridge USDC to Sonic
        console.log("Initiating bridge to Sonic...");
        const deposit = await bridgeToSonic(USDC_ADDRESS, amount);
        console.log(`Deposit successful: ${deposit.transactionHash}`);

        // 2. Claim USDC on Sonic
        console.log("Waiting for state update and claiming on Sonic...");
        const claimTx = await claimOnSonic(deposit.blockNumber, deposit.depositId, USDC_ADDRESS, amount);
        console.log(`Claim successful: ${claimTx}`);

        // Later: Bridge back to Ethereum
        console.log("Initiating bridge back to Ethereum...");
        const withdrawal = await bridgeToEthereum(deposit.mintedToken, amount);
        console.log(`Withdrawal initiated: ${withdrawal.transactionHash}`);

        // Claim on Ethereum
        console.log("Waiting for state update and claiming on Ethereum...");
        const finalClaim = await claimOnEthereum(
            withdrawal.blockNumber,
            withdrawal.withdrawalId,
            withdrawal.originalToken,
            amount
        );
        console.log(`Final claim successful: ${finalClaim}`);
    } catch (error) {
        console.error("Bridge operation failed:", error.message);
        throw error;
    }
}

Required ABIs

const STATE_ORACLE_ABI = [
    "function lastBlockNum() external view returns (uint256)",
    "function lastState() external view returns (bytes32)"
];

const ERC20_ABI = [
    "function approve(address spender, uint256 amount) external returns (bool)",
    "function allowance(address owner, address spender) external view returns (uint256)"
];

const TOKEN_PAIRS_ABI = [
    "function originalToMinted(address) external view returns (address)",
    "function mintedToOriginal(address) external view returns (address)"
];

const TOKEN_DEPOSIT_ABI = [
    "function deposit(uint96 uid, address token, uint256 amount) external",
    "function claim(uint256 id, address token, uint256 amount, bytes calldata proof) external",
    "event Deposit(uint256 indexed id, address indexed owner, address token, uint256 amount)"
];

const BRIDGE_ABI = [
    "function withdraw(uint96 uid, address token, uint256 amount) external",
    "function claim(uint256 id, address token, uint256 amount, bytes calldata proof) external",
    "event Withdrawal(uint256 indexed id, address indexed owner, address token, uint256 amount)"
];

Important Notes

  1. State Updates

    • Ethereum → Sonic: Monitor StateOracle.lastBlockNum until it's >= deposit block

    • Sonic → Ethereum: Monitor StateOracle.lastBlockNum until it's >= withdrawal block

  2. Proofs

    • Required for all claim operations

    • Generated using eth_getProof RPC call with correct storage slots

    • Must be RLP encoded in format: RLP.encode([RLP.encode(accountProof), RLP.encode(storageProof)])

    • Storage slots are calculated using:

      • Deposits: keccak256(abi.encode(depositId, uint8(7)))

      • Withdrawals: keccak256(abi.encode(withdrawalId, uint8(1)))

  3. Gas Fees

    • Keep enough ETH/S for gas on both networks

    • Claim operations typically cost more gas due to proof verification

  4. Security

    • Never share private keys

    • Always verify contract addresses

    • Test with small amounts first

    • Use the same private key for both networks

  5. Monitoring

    • Monitor transaction status on both networks

    • Keep transaction hashes for reference

    • Verify successful claims before proceeding

    • Monitor StateOracle updates for claim timing

PreviousNetwork ParametersNextLearn Solidity

Last updated 1 month ago