Programmatic Gateway
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;
}
await new Promise(resolve => setTimeout(resolve, 30000)); // Check every 30 seconds
}
}
async function generateProof(depositId) {
// 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],
"latest"
]);
// 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(depositTxHash, depositBlockNumber, depositId) {
// 1. Wait for state oracle update
console.log("Waiting for state oracle update...");
await waitForStateUpdate(depositBlockNumber);
// 2. Generate proof
console.log("Generating proof...");
const proof = await generateProof(depositId);
// 3. Claim tokens with proof
const bridge = new ethers.Contract(SONIC_CONTRACTS.BRIDGE, BRIDGE_ABI, sonicSigner);
const tx = await bridge.claim(depositTxHash, 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. 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,
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;
}
await new Promise(resolve => setTimeout(resolve, 30000)); // Check every 30 seconds
}
}
async function generateWithdrawalProof(withdrawalId) {
// 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],
"latest"
]);
// 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(withdrawalTxHash, withdrawalBlockNumber, withdrawalId) {
// 1. Wait for state oracle update
console.log("Waiting for state oracle update...");
await waitForEthStateUpdate(withdrawalBlockNumber);
// 2. Generate proof
console.log("Generating proof...");
const proof = await generateWithdrawalProof(withdrawalId);
// 3. Claim tokens with proof
const deposit = new ethers.Contract(ETH_CONTRACTS.TOKEN_DEPOSIT, TOKEN_DEPOSIT_ABI, ethSigner);
const tx = await deposit.claim(withdrawalTxHash, 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.transactionHash, deposit.blockNumber, deposit.depositId);
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.transactionHash,
withdrawal.blockNumber,
withdrawal.withdrawalId
);
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(uint256 nonce, address token, uint256 amount) external",
"function claim(bytes32 txHash, bytes calldata proof) external"
];
const BRIDGE_ABI = [
"function withdraw(uint256 nonce, address token, uint256 amount) external",
"function claim(bytes32 txHash, bytes calldata proof) external"
];
Important Notes
State Updates
Ethereum → Sonic: Monitor StateOracle.lastBlockNum until it's >= deposit block
Sonic → Ethereum: Monitor StateOracle.lastBlockNum until it's >= withdrawal block
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)))
Gas Fees
Keep extra ETH for gas on both networks
Claim operations typically cost more gas due to proof verification
Security
Never share private keys
Always verify contract addresses
Test with small amounts first
Use the same private key for both networks
Monitoring
Monitor transaction status on both networks
Keep transaction hashes for reference
Verify successful claims before proceeding
Monitor StateOracle updates for claim timing
Last updated