Skip to content

Instantly share code, notes, and snippets.

@thaarok
Created April 22, 2025 19:40
Show Gist options
  • Save thaarok/9155659bf94822116ed2c003810e9020 to your computer and use it in GitHub Desktop.
Save thaarok/9155659bf94822116ed2c003810e9020 to your computer and use it in GitHub Desktop.
Sonic Gateway usage sample
#!/usr/bin/env node
const { ethers } = require("ethers");
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)"
];
// Ethereum (L1)
const ETH_CONTRACTS = { // testnet deployment
TOKEN_DEPOSIT: "0xDF0A2E5361701140111fD70F9628231B63680d27",
TOKEN_PAIRS: "0x4aBf40965d008856a1Eac0d2585Dd631259fA424",
STATE_ORACLE: "0xAD785C64704c07d4898c5F948ed2CCBA59531E55"
};
// Sonic (L2)
const SONIC_CONTRACTS = { // testnet deployment
BRIDGE: "0xA5495Fa50E94f56024F9C7e442b8ad29B03f459b",
TOKEN_PAIRS: "0xA7cD9611Aa6214d1b6f2240e54A7f3B80aD60453",
STATE_ORACLE: "0x3F5Ef194f6DBf260AB5e131780C1Cd48cE239E40"
};
// Network RPC endpoints
const ETHEREUM_RPC = "https://ethereum-sepolia-rpc.publicnode.com";
const SONIC_RPC = "https://rpc.blaze.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 = "...";
const ethSigner = new ethers.Wallet(PRIVATE_KEY, ethProvider);
const sonicSigner = new ethers.Wallet(PRIVATE_KEY, sonicProvider);
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
};
}
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;
}
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
};
}
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;
}
async function bridgeUSDC() {
// USDC details
const USDC_ADDRESS = "0xd98b590ebe0a3ed8c144170ba4122d402182976f"; // testnet deployment
let amount = 1000;
// 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}`);
//const deposit = { mintedToken: "0x326599186C997717cC07e7A1305a7fddFC21647A" };
amount = 100;
// 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}`);
}
bridgeUSDC();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment