Skip to main content

Bridge

General Information

The bridge enables asset transfers between Kaia L1 and L2. This document covers:

  • Deposit: Moving assets from Kaia L1 to L2 (direct transaction submission)
  • Withdrawal: Moving assets from L2 to Kaia L1 (via API endpoint)

Key Bridge Addresses

Fixed Addresses

  • ArbSys Contract: 0x0000000000000000000000000000000000000064 (for L2 native KAIA withdrawal)

Deployment-Dependent Addresses

L1 Contracts:

  • Inbox Contract: L1 bridge entry point (for KAIA deposits)
  • L1 Gateway Router: Manages L1 ERC20 token deposits
  • L1 ERC20 Gateway: ERC20 gateway

L2 Contracts:

  • L2 Gateway Router: Manages L2 ERC20 token withdrawals
  • L2 ERC20 Gateway: L2 ERC20 gateway

Note: Deployment-dependent addresses are determined at network deployment time, so always verify you are using the correct address values.

Deposit (Kaia L1 → L2)

Deposits are initiated by sending transactions directly to bridge contracts on Kaia L1. No API endpoints are provided; users must construct and submit transactions directly to the L1 network.

1. KAIA Deposit (Native Token)

To deposit native KAIA from Kaia L1 to L2, call the depositEth() function on the Inbox contract.

Transaction Structure

{
from: "0x...", // User's L1 wallet address
to: "0x...", // Inbox contract address (L1 bridge entry point)
data: "0x439370b1", // depositEth() function selector
value: "0x...", // Amount of KAIA to deposit (in wei)
gasLimit: "0x30d40", // 200000 - sufficient gas
gasPrice: "0x...", // Current L1 network gas price
nonce: "...", // User account nonce
chainId: "..." // L1 chain ID (Kaia Testnet: 1001, Kaia Mainnet: 8217)
}

Field Descriptions

  • from: User's L1 network wallet address performing the deposit
  • to: Inbox contract address - official L1 bridge entry point deployed by bridge operators
  • data: depositEth() function selector - always 0x439370b1
  • value: KAIA amount to deposit (in wei, 1 KAIA = 10^18 wei)
  • gasLimit: 0x30d40 (200000) - sufficient gas for deposit operation
  • gasPrice: Current L1 network gas price needs to be queried
  • nonce: User account's sequential transaction counter
  • chainId:
    • Kaia Testnet: 1001
    • Kaia Mainnet: 8217

Creating and Signing Deposit Transaction

const { ethers } = require('ethers')

// Initialize Provider and Signer
const l1Provider = new ethers.providers.JsonRpcProvider(L1_RPC_URL)
const l1Signer = new ethers.Wallet(PRIVATE_KEY, l1Provider)

// Create Inbox contract interface
const inbox = new ethers.Contract(
INBOX_CONTRACT_ADDRESS, // L1 bridge entry point
['function depositEth() external payable returns (uint256)'],
l1Signer
)

// Method 1: Using contract interface (recommended)
const depositTx = await inbox.depositEth({
value: ethers.BigNumber.from('1000000000000000000'), // 1 KAIA (in wei)
gasLimit: 200000 // Sufficient gas
})

// Method 2: Manual transaction construction
const unsignedTx = {
from: l1Signer.address,
to: INBOX_CONTRACT_ADDRESS, // L1 bridge entry point
data: '0x439370b1', // depositEth() selector
value: '0xde0b6b3a7640000', // 1 KAIA (hex, 10^18 wei)
gasLimit: '0x30d40', // 200000 (hex) - sufficient gas
gasPrice: await l1Provider.getGasPrice(),
nonce: await l1Provider.getTransactionCount(l1Signer.address),
chainId: (await l1Provider.getNetwork()).chainId
}

// Sign transaction
const signedTx = await l1Signer.signTransaction(unsignedTx)

// Send transaction
const txResponse = await l1Provider.sendTransaction(signedTx)
console.log('Deposit transaction:', txResponse.hash)

Notes

  1. Inbox Contract: The to address must be the official Inbox contract deployed on L1
  2. Gas Requirements: L1 transactions require actual gas fees
  3. Value Field: Deposit amount must be specified in the value field in wei
  4. Network Compatibility: Use the correct chain ID and RPC endpoint for your target network

2. ERC20 Token Deposit

This section describes the transaction structure and process for depositing ERC20 tokens from L1 to L2.

Transaction Structure

Step 1: Token Approval Transaction
{
from: "0x...", // User's L1 wallet address
to: "0x...", // ERC20 token contract address
data: "0x095ea7b3...", // approve(spender, amount) encoded data
value: "0x0", // No KAIA needed for ERC20 approval
gasLimit: "0xc350", // 50000 - sufficient gas
gasPrice: "0x...", // Current L1 network gas price
nonce: "...", // User account nonce
chainId: "..." // L1 chain ID (Kaia Testnet: 1001, Kaia Mainnet: 8217)
}
Step 2: Deposit Transaction via L1GatewayRouter
{
from: "0x...", // User's L1 wallet address
to: "0x...", // L1GatewayRouter contract address
data: "0x...", // outboundTransfer() encoded call data
value: "0x...", // L2 execution cost (bridge fee)
gasLimit: "0x7a120", // 500000 - sufficient gas
gasPrice: "0x...", // Current L1 network gas price
nonce: "...", // User account nonce
chainId: "..." // L1 chain ID
}

Field Descriptions

Token Approval Transaction
  • from: User's L1 wallet address performing the deposit
  • to: L1 ERC20 token contract address
  • data: approve(address spender, uint256 amount) function call encoding
    • spender: L1 ERC20 Gateway address (query from router)
    • amount: Token amount to approve (in smallest unit)
  • value: Always 0x0 (no KAIA needed for ERC20 approval)
  • gasLimit: 0xc350 (50000) - sufficient gas for approval operation
Deposit Transaction
  • from: User's L1 wallet address
  • to: L1GatewayRouter contract address - deployed bridge entry point
  • data: outboundTransfer() function call encoding
    • L1 token address
    • L2 recipient address (typically same as sender)
    • Token amount to deposit
    • Max gas for L2 execution
    • Gas price bid for L2 execution
    • Additional data
  • value: KAIA fee for L2 bridge processing
  • gasLimit: 0x7a120 (500000) - sufficient gas for deposit operation

Complete Implementation Flow

const { ethers } = require('ethers')

// ====================================================================
// Initial Setup
// ====================================================================
const l1Provider = new ethers.providers.JsonRpcProvider(L1_RPC_URL)
const l1Signer = new ethers.Wallet(PRIVATE_KEY, l1Provider)

// Load contract addresses from network configuration
const L1_GATEWAY_ROUTER = '0x...' // Get from network configuration
const L1_TOKEN = '0x...' // ERC20 token to deposit

// Create token contract interface
const token = new ethers.Contract(L1_TOKEN, [
'function approve(address spender, uint256 amount) returns (bool)',
'function allowance(address owner, address spender) view returns (uint256)',
'function decimals() view returns (uint8)', // For querying decimals
], l1Signer)

// Create router contract interface
const router = new ethers.Contract(L1_GATEWAY_ROUTER, [
'function getGateway(address) view returns (address)',
'function outboundTransfer(address,address,uint256,uint256,uint256,bytes) payable returns (bytes)',
], l1Signer)

// ====================================================================
// Step 0: Query Token Info and Gateway
// ====================================================================
// Query token decimals
const decimals = await token.decimals()
const depositAmount = ethers.utils.parseUnits('100', decimals) // 100 tokens

// Query gateway from router
const gateway = await router.getGateway(L1_TOKEN) // Query gateway from router

// ====================================================================
// Step 1: Token Approval Transaction
// ====================================================================
const allowance = await token.allowance(l1Signer.address, gateway)
if (allowance.lt(depositAmount)) {
// Encode approve() function data
const approveData = token.interface.encodeFunctionData('approve', [
gateway, // Address to approve (gateway)
depositAmount // Token amount to approve
])

// Construct transaction object
const approveTx = {
from: l1Signer.address,
to: L1_TOKEN, // ERC20 token contract
data: approveData, // approve(gateway, amount)
value: '0x0', // No KAIA needed for ERC20 approval
gasLimit: '0xc350', // 50000 - sufficient gas
gasPrice: await l1Provider.getGasPrice(),
nonce: await l1Provider.getTransactionCount(l1Signer.address),
chainId: (await l1Provider.getNetwork()).chainId
}

// Sign and send transaction
const signedApprove = await l1Signer.signTransaction(approveTx)
const approveReceipt = await l1Provider.sendTransaction(signedApprove)
await approveReceipt.wait()
console.log('Token approval complete:', approveReceipt.hash)
}

// ====================================================================
// Step 2: Deposit Transaction
// ====================================================================
// Set bridge parameters
const maxGas = ethers.BigNumber.from('1000000')
const gasPriceBid = ethers.utils.parseUnits('1', 'gwei')
const bridgeFee = ethers.utils.parseEther('0.01') // Fixed fee for L2 execution and submission costs

// Encode additional data
const extraData = ethers.utils.defaultAbiCoder.encode(
['uint256', 'bytes'],
[bridgeFee, '0x']
)

// Encode outboundTransfer() function data
const depositData = router.interface.encodeFunctionData('outboundTransfer', [
L1_TOKEN, // L1 token address
l1Signer.address, // L2 recipient address
depositAmount, // Token amount to deposit
maxGas, // Max gas for L2 execution
gasPriceBid, // Gas price bid for L2 execution
extraData // Additional data
])

// Construct transaction object
const depositTx = {
from: l1Signer.address,
to: L1_GATEWAY_ROUTER, // L1GatewayRouter contract
data: depositData, // outboundTransfer() call
value: bridgeFee.toHexString(), // Bridge fee (KAIA)
gasLimit: '0x7a120', // 500000 - sufficient gas
gasPrice: await l1Provider.getGasPrice(),
nonce: await l1Provider.getTransactionCount(l1Signer.address),
chainId: (await l1Provider.getNetwork()).chainId
}

// Sign and send transaction
const signedDeposit = await l1Signer.signTransaction(depositTx)
const depositReceipt = await l1Provider.sendTransaction(signedDeposit)
await depositReceipt.wait()
console.log('Deposit complete:', depositReceipt.hash)

Key Requirements

  1. Two-Step Process: ERC20 deposits always require two transactions
  2. Balance Requirements: Need both ERC20 token balance to deposit and KAIA for gas fees
  3. Network-Specific Settings: Contract addresses vary by network deployment
  4. Allowance Optimization: Check current allowance first to avoid unnecessary approval transactions

Withdrawal (L2 → Kaia L1)

Withdrawals are initiated on L2 via the DEX API endpoint. After creating and signing a withdrawal transaction on L2 and calling the /api/v1/wallet/withdraw API, the system automatically completes the withdrawal processing on Kaia L1.

POST /api/v1/wallet/withdraw

Submit a signed withdrawal transaction to move assets from L2 to Kaia L1.

Request Body:

{
"tx": "0x02f8730183...{complete signed transaction hex}"
}

Request Fields:

FieldTypeRequiredDescription
txSTRINGYESComplete signed withdrawal transaction in hex format

Response:

{
"code": 200,
"errMsg": "OK",
"result": "0x4c9980c4fd003e9aae2e7a8a87812382c84a695614225e423aaf1676089dbbfe"
}

3. KAIA Withdrawal (Native Token)

This section describes how to create a raw signed transaction for withdrawing native KAIA from L2 to L1.

Key Points

  • Gas Free: L2 network is gas-free, so gasPrice can be set to 0
  • Nonce: L2 supports both sequential nonces and time-based nonces (Unix timestamp in milliseconds)
  • Chain ID: Must use L2 chain ID
  • Target Contract: Fixed address 0x0000000000000000000000000000000000000064 (ArbSys precompile)

Transaction Structure

{
from: "0x...", // User's L2 wallet address
to: "0x0000000000000000000000000000000000000064", // ArbSys precompile address
data: "0x...", // Encoded withdrawEth(address) function call
value: "0x...", // Amount of KAIA to withdraw (in wei)
gasLimit: "0x7a120", // 500000 - sufficient gas
gasPrice: "0x0", // 0 (gas-free network)
nonce: nonce, // Sequential or time-based nonce
chainId: L2_CHAIN_ID // L2 chain ID
}

Field Descriptions

  • from: User's L2 wallet address performing the withdrawal
  • to: ArbSys precompile address - always 0x0000000000000000000000000000000000000064
  • data: withdrawEth(address destination) function call encoding
    • Function selector: 0x25e16063
    • destination: L1 recipient address (32 bytes with padding)
  • value: Amount of KAIA to withdraw (in wei, 1 KAIA = 10^18 wei)
  • gasLimit: 0x7a120 (500000) - sufficient gas for withdrawal operation
  • gasPrice: 0x0 - can be set to 0 as L2 is gas-free
  • nonce: Sequential nonce or time-based nonce (Date.now())
  • chainId: L2 network chain ID

Creating and Signing Transaction

const { ethers } = require('ethers')

// Initialize L2 RPC Provider and Signer
const l2Provider = new ethers.providers.JsonRpcProvider(L2_RPC_URL)
const l2Signer = new ethers.Wallet(PRIVATE_KEY) // No provider connection for manual signing

// Get L2 network info
const l2Network = await l2Provider.getNetwork()
const l2ChainId = l2Network.chainId // L2 chain ID

// Prepare ArbSys interface
const arbSysInterface = new ethers.utils.Interface([
'function withdrawEth(address destination) external payable returns (uint256)'
])

// Encode function data
const withdrawData = arbSysInterface.encodeFunctionData('withdrawEth', [
L1_RECIPIENT_ADDRESS // L1 recipient address
])

// Use time-based nonce
const nonce = Date.now() // Time-based nonce (milliseconds)

// Construct unsigned transaction
const unsignedTx = {
from: l2Signer.address,
to: '0x0000000000000000000000000000000000000064', // ArbSys precompile
data: withdrawData,
value: ethers.utils.parseEther('1.0').toHexString(), // 1 KAIA
gasLimit: ethers.BigNumber.from(500000).toHexString(), // Sufficient gas
gasPrice: '0x0', // Gas-free so can set to 0
nonce: nonce,
chainId: l2ChainId // L2 chain ID
}

// Sign transaction
const signedTx = await l2Signer.signTransaction(unsignedTx)
console.log('Signed transaction:', signedTx)

// Submit via API (see POST /api/v1/wallet/withdraw above)

4. ERC20 Token Withdrawal

This section describes how to create and sign transactions for withdrawing ERC20 tokens from L2 to L1.

Key Features

  • Gas-Free Network: L2 has no gas fees, so gasPrice can be set to 0
  • Nonce: Both sequential and time-based nonces are supported
  • Chain ID: Use L2 chain ID
  • Raw Signed Transaction: Create pre-signed transaction for API submission

Transaction Structure

{
from: "0x...", // User's L2 wallet address
to: "0x...", // L2 Gateway Router contract address
data: "0x7b3a3c8b...", // outboundTransfer function call data
value: "0x00", // Always 0 for ERC20 transfers
gasLimit: "0x0f4240", // 1000000 - sufficient gas
gasPrice: "0x00", // 0 (gas-free network)
nonce: nonce, // Sequential or time-based nonce
chainId: L2_CHAIN_ID // L2 chain ID
}

Field Descriptions

  • from: L2 wallet address initiating the withdrawal
  • to: L2 Gateway Router contract address - manages token bridging operations
  • data: outboundTransfer(address,address,uint256,bytes) function call encoding
    • L1 token address: L1 token contract address to receive
    • L1 recipient address: L1 address to receive tokens
    • Withdrawal amount: Token quantity in wei
    • Additional data: Typically "0x" (empty data)
  • value: Always 0x00 for ERC20 token transfers
  • gasLimit: 0x0f4240 (1000000) - sufficient gas for withdrawal operation
  • gasPrice: 0x00 - can be set to 0 as L2 is gas-free
  • nonce: Sequential nonce or time-based nonce (Date.now())
  • chainId: L2 network chain ID

Transaction Creation and Signing Process

const { ethers } = require('ethers')

// Configuration
const L2_RPC = 'https://l2-sequencer-testnet.alphasec.trade' // Testnet L2 RPC Endpoint
const PRIVATE_KEY = '0x...'
const L2_GATEWAY_ROUTER = '0x097209B15FB6cEefba90EA10e4c1c5439E6bC1Ea' // Testnet L2 Gateway Router

// Initialize L2 Provider and Signer
const l2Provider = new ethers.providers.JsonRpcProvider(L2_RPC)
const l2Signer = new ethers.Wallet(PRIVATE_KEY)

// Get L2 chain ID
const network = await l2Provider.getNetwork()
const l2ChainId = network.chainId

// Create router interface and encode function
const routerInterface = new ethers.utils.Interface([
'function outboundTransfer(address,address,uint256,bytes) returns (bytes)'
])

// Encode outboundTransfer function data
const transferData = routerInterface.encodeFunctionData('outboundTransfer', [
'0xac76d4a9985aba068dbae07bf5cc10be06a19f12', // L1 Testnet token address
recipient, // L1 recipient address
ethers.BigNumber.from('1000000000000000000'), // 1 token (18 decimals)
'0x' // Additional data (empty)
])

// Use time-based nonce
const nonce = Date.now() // Time-based nonce (milliseconds)

// Create transaction object
const unsignedTx = {
from: l2Signer.address,
to: L2_GATEWAY_ROUTER,
data: transferData,
value: ethers.BigNumber.from(0).toHexString(), // 0x00
gasLimit: ethers.BigNumber.from(1000000).toHexString(), // Sufficient gas
gasPrice: ethers.BigNumber.from(0).toHexString(), // 0x00 (gas-free)
nonce: nonce,
chainId: l2ChainId // L2 chain ID
}

// Sign transaction
const rawSignedTx = await l2Signer.signTransaction(unsignedTx)
console.log('Signed transaction:', rawSignedTx)

// Submit via API (see POST /api/v1/wallet/withdraw above)

Notes

  1. Chain ID: Must use L2 chain ID
  2. Gas Settings: L2 is a gas-free network, so gasPrice can be set to 0
  3. Nonce Options: Choose between sequential nonce or time-based nonce (Date.now())
  4. Transaction Flow: Create and sign on L2 → Submit via API → Automatic processing

Transaction Configuration Guidelines

Kaia L1 Transactions (Deposits)

  • Use standard Kaia gas pricing
  • KAIA required for gas fees
  • Include submission cost for L2 execution
  • Wait for L1 confirmation before expecting funds on L2

L2 Transactions (Withdrawals)

  • Gas price can be set to 0 (gas-free)
  • Set gas limit to a sufficient value
  • Use either sequential or time-based nonces
  • Transactions must target specific precompile/router addresses

Withdrawal Process

  1. Transaction Creation: Create and sign withdrawal transaction on L2
  2. API Submission: Call /api/v1/wallet/withdraw API
  3. Automatic Processing: System automatically handles L2 confirmation, batch submission, and Kaia L1 withdrawal