Base L2 Features Guide
Base is built on the OP Stack, providing all the benefits of Layer 2 scaling while maintaining full Ethereum compatibility. This guide covers Layer 2 specific features, optimizations, and best practices for building on Base.
Layer 2 Architecture
OP Stack Foundation
Base leverages Optimism's proven OP Stack technology:
- Optimistic Rollup: Transactions are assumed valid by default
- Fraud Proof System: Invalid transactions can be challenged
- Ethereum Security: Inherits Ethereum's security guarantees
- EVM Compatibility: Run existing Ethereum contracts without modification
Key Components
Ultra-Low Transaction Costs
Gas Fee Structure
Base's Layer 2 architecture dramatically reduces transaction costs:
// Example: Compare gas costs
const estimateBaseCosts = async () => {
const provider = new ethers.JsonRpcProvider('https://api.blockeden.xyz/base/<your-api-key>');
// Simple transfer
const transferGas = 21000;
const gasPrice = await provider.getFeeData();
const baseCost = transferGas * gasPrice.gasPrice;
console.log(`Transfer cost on Base: ${ethers.formatEther(baseCost)} ETH`);
// Typically < $0.01
// ERC-20 transfer
const erc20Gas = 65000;
const erc20Cost = erc20Gas * gasPrice.gasPrice;
console.log(`ERC-20 transfer cost: ${ethers.formatEther(erc20Cost)} ETH`);
// Typically < $0.02
// Uniswap V3 swap
const swapGas = 150000;
const swapCost = swapGas * gasPrice.gasPrice;
console.log(`DEX swap cost: ${ethers.formatEther(swapCost)} ETH`);
// Typically < $0.05
};
L2 Gas Optimization
// Optimize transactions for L2
const optimizeForBase = {
// Use appropriate gas settings
gasLimit: 1000000, // Conservative estimate
gasPrice: ethers.parseUnits('0.001', 'gwei'), // Very low gas price
// Or use EIP-1559
maxFeePerGas: ethers.parseUnits('0.01', 'gwei'),
maxPriorityFeePerGas: ethers.parseUnits('0.001', 'gwei')
};
// Deploy contracts efficiently
const deployContract = async (factory, args) => {
const contract = await factory.deploy(...args, {
gasLimit: 3000000,
gasPrice: ethers.parseUnits('0.001', 'gwei')
});
console.log(`Deployment cost: ~$0.005`); // Extremely low
return contract;
};
Fast Finality
Finality Types on Base
- Soft Finality: ~2 seconds (sequencer confirmation)
- Safe Finality: ~12 seconds (published to L1)
- Hard Finality: ~7 days (challenge period complete)
// Monitor transaction finality
class FinalityTracker {
constructor(provider) {
this.provider = provider;
}
async trackFinality(txHash) {
console.log(`Tracking finality for ${txHash}`);
// 1. Wait for soft finality (sequencer)
const receipt = await this.provider.waitForTransaction(txHash, 1);
console.log(`✓ Soft finality: Block ${receipt.blockNumber}`);
// 2. Wait for safe finality (L1 publication)
const safeReceipt = await this.provider.waitForTransaction(txHash, 6);
console.log(`✓ Safe finality: ${safeReceipt.confirmations} confirmations`);
// 3. Hard finality (challenge period) - 7 days
console.log(`✓ Hard finality: Available after 7-day challenge period`);
return {
softFinality: receipt.blockNumber,
safeFinality: safeReceipt.blockNumber,
hardFinalityTime: Date.now() + (7 * 24 * 60 * 60 * 1000)
};
}
}
// Usage
const tracker = new FinalityTracker(provider);
const finality = await tracker.trackFinality('0x...');
Cross-Layer Communication
L1 to L2 Deposits
Deposit ETH or tokens from Ethereum to Base:
// Deposit ETH from L1 to L2
const depositETH = async (amount) => {
// L1 Bridge Contract
const l1BridgeAddress = '0x3154Cf16ccdb4C6d922629664174b904d80F2C35';
const l1Bridge = new ethers.Contract(l1BridgeAddress, bridgeABI, l1Signer);
const tx = await l1Bridge.depositETH(20000, '0x', {
value: ethers.parseEther(amount),
gasLimit: 100000
});
console.log(`Deposit transaction: ${tx.hash}`);
// Monitor L2 for deposit completion
await monitorL2Deposit(tx.hash);
};
// Monitor L2 for deposit completion
const monitorL2Deposit = async (l1TxHash) => {
const l2Provider = new ethers.JsonRpcProvider('https://api.blockeden.xyz/base/<your-api-key>');
// Check for deposit events on L2
const filter = {
address: '0x4200000000000000000000000000000000000010', // L2 Bridge
topics: [
'0x...', // Deposit event signature
ethers.zeroPadValue(l1TxHash, 32)
]
};
const logs = await l2Provider.getLogs(filter);
console.log(`Deposit completed on L2: ${logs.length > 0}`);
};
L2 to L1 Withdrawals
Withdraw funds from Base back to Ethereum:
// Initiate withdrawal from L2 to L1
const initiateWithdrawal = async (amount, recipient) => {
const l2BridgeAddress = '0x4200000000000000000000000000000000000010';
const l2Bridge = new ethers.Contract(l2BridgeAddress, bridgeABI, l2Signer);
const tx = await l2Bridge.withdraw(
'0x0000000000000000000000000000000000000000', // ETH
ethers.parseEther(amount),
20000,
'0x',
{
gasLimit: 100000
}
);
console.log(`Withdrawal initiated: ${tx.hash}`);
// Wait for 7-day challenge period
const completionTime = Date.now() + (7 * 24 * 60 * 60 * 1000);
console.log(`Withdrawal available for completion after: ${new Date(completionTime)}`);
return {
l2TxHash: tx.hash,
completionTime: completionTime
};
};
// Complete withdrawal on L1 (after challenge period)
const completeWithdrawal = async (withdrawalData) => {
// This requires the withdrawal proof from L2
const l1BridgeAddress = '0x3154Cf16ccdb4C6d922629664174b904d80F2C35';
const l1Bridge = new ethers.Contract(l1BridgeAddress, bridgeABI, l1Signer);
// Generate withdrawal proof (simplified)
const proof = await generateWithdrawalProof(withdrawalData.l2TxHash);
const tx = await l1Bridge.finalizeWithdrawal(proof, {
gasLimit: 200000
});
console.log(`Withdrawal completed on L1: ${tx.hash}`);
};
Message Passing
L1 to L2 Messages
Send arbitrary data from Ethereum to Base:
// Send message from L1 to L2
const sendL1ToL2Message = async (target, message) => {
const l1MessengerAddress = '0x866E82a600A1414e583f7F13623F1aC5d58b0Afa';
const l1Messenger = new ethers.Contract(l1MessengerAddress, messengerABI, l1Signer);
const tx = await l1Messenger.sendMessage(
target, // L2 contract address
message, // Encoded message data
1000000, // L2 gas limit
{
gasLimit: 200000
}
);
console.log(`L1 to L2 message sent: ${tx.hash}`);
return tx.hash;
};
// Receive message on L2
contract L2Receiver {
address public constant L2_MESSENGER = 0x4200000000000000000000000000000000000007;
modifier onlyFromL1(address l1Sender) {
require(
msg.sender == L2_MESSENGER &&
IL2CrossDomainMessenger(L2_MESSENGER).xDomainMessageSender() == l1Sender,
"Only from L1"
);
_;
}
function receiveMessage(bytes calldata data) external onlyFromL1(authorizedL1Contract) {
// Process the message from L1
processL1Message(data);
}
}
L2 to L1 Messages
Send messages from Base to Ethereum:
// Send message from L2 to L1
const sendL2ToL1Message = async (target, message) => {
const l2MessengerAddress = '0x4200000000000000000000000000000000000007';
const l2Messenger = new ethers.Contract(l2MessengerAddress, messengerABI, l2Signer);
const tx = await l2Messenger.sendMessage(
target, // L1 contract address
message, // Encoded message data
1000000, // L1 gas limit
{
gasLimit: 100000
}
);
console.log(`L2 to L1 message sent: ${tx.hash}`);
// Message will be available on L1 after challenge period
return {
l2TxHash: tx.hash,
availableOnL1After: Date.now() + (7 * 24 * 60 * 60 * 1000)
};
};
// Relay message on L1 (after challenge period)
const relayL2ToL1Message = async (messageData) => {
const l1MessengerAddress = '0x866E82a600A1414e583f7F13623F1aC5d58b0Afa';
const l1Messenger = new ethers.Contract(l1MessengerAddress, messengerABI, l1Signer);
// Generate message proof
const proof = await generateMessageProof(messageData.l2TxHash);
const tx = await l1Messenger.relayMessage(...proof, {
gasLimit: 300000
});
console.log(`Message relayed on L1: ${tx.hash}`);
};
Precompiled Contracts
Base includes several precompiled contracts for L2 functionality:
L2 Standard Bridge
// L2 Standard Bridge: 0x4200000000000000000000000000000000000010
const L2_STANDARD_BRIDGE = '0x4200000000000000000000000000000000000010';
// Bridge tokens to L1
const bridgeToL1 = async (tokenAddress, amount, recipient) => {
const bridge = new ethers.Contract(L2_STANDARD_BRIDGE, bridgeABI, signer);
const tx = await bridge.withdrawTo(
tokenAddress,
recipient,
amount,
20000,
'0x'
);
return tx.hash;
};
L2 Cross Domain Messenger
// L2 Cross Domain Messenger: 0x4200000000000000000000000000000000000007
const L2_MESSENGER = '0x4200000000000000000000000000000000000007';
// Send message to L1
const sendMessageToL1 = async (target, data) => {
const messenger = new ethers.Contract(L2_MESSENGER, messengerABI, signer);
const tx = await messenger.sendMessage(target, data, 1000000);
return tx.hash;
};
L2 Block Context
// L2 to L1 Message Passer: 0x4200000000000000000000000000000000000016
const L2_MESSAGE_PASSER = '0x4200000000000000000000000000000000000016';
// Get L1 block information
const getL1BlockInfo = async () => {
const provider = new ethers.JsonRpcProvider('https://api.blockeden.xyz/base/<your-api-key>');
// L1 block number that this L2 block is based on
const l1BlockNumber = await provider.call({
to: '0x4200000000000000000000000000000000000015', // L1 Block contract
data: '0x64ca5bb6' // l1BlockNumber()
});
return parseInt(l1BlockNumber, 16);
};
Performance Optimizations
Batch Transactions
// Batch multiple operations
const batchOperations = async (operations) => {
const multicallAddress = '0x...; // Multicall contract
const multicall = new ethers.Contract(multicallAddress, multicallABI, signer);
const calls = operations.map(op => ({
target: op.target,
callData: op.data
}));
const tx = await multicall.aggregate(calls, {
gasLimit: 500000
});
console.log(`Batched ${operations.length} operations: ${tx.hash}`);
return tx;
};
Contract Deployment Optimization
// Optimize contract deployment for L2
const deployOptimized = async (bytecode, args) => {
const factory = new ethers.ContractFactory(abi, bytecode, signer);
// Use CREATE2 for deterministic addresses
const salt = ethers.randomBytes(32);
const contract = await factory.deploy(...args, {
gasLimit: 3000000,
gasPrice: ethers.parseUnits('0.001', 'gwei'),
// CREATE2 deployment
salt: salt
});
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log(`Contract deployed to: ${address}`);
console.log(`Deployment cost: ~$0.01`);
return contract;
};
Gas Estimation for L2
// Accurate gas estimation for Base
const estimateL2Gas = async (contract, method, args) => {
try {
// Estimate gas
const gasEstimate = await contract[method].estimateGas(...args);
// Add buffer for L2 (usually not needed)
const gasWithBuffer = gasEstimate * 110n / 100n;
// Get current gas price
const feeData = await contract.provider.getFeeData();
const cost = gasWithBuffer * feeData.gasPrice;
return {
gasLimit: gasWithBuffer,
gasPrice: feeData.gasPrice,
estimatedCost: ethers.formatEther(cost),
estimatedCostUSD: parseFloat(ethers.formatEther(cost)) * 2000 // Assume $2000/ETH
};
} catch (error) {
console.error('Gas estimation failed:', error);
throw error;
}
};
// Usage
const gasInfo = await estimateL2Gas(contract, 'transfer', [recipient, amount]);
console.log(`Estimated cost: ${gasInfo.estimatedCost} ETH (~$${gasInfo.estimatedCostUSD.toFixed(4)})`);
Development Best Practices
L2-Optimized Smart Contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract BaseOptimized {
// Use packed structs to minimize storage
struct UserData {
uint128 balance; // 16 bytes
uint64 timestamp; // 8 bytes
uint32 count; // 4 bytes
uint32 flags; // 4 bytes
// Total: 32 bytes (1 slot)
}
mapping(address => UserData) public users;
// Batch operations to save gas
function batchUpdate(
address[] calldata addresses,
uint128[] calldata balances
) external {
require(addresses.length == balances.length, "Length mismatch");
for (uint i = 0; i < addresses.length; i++) {
users[addresses[i]].balance = balances[i];
users[addresses[i]].timestamp = uint64(block.timestamp);
}
}
// Use events efficiently
event BatchUpdated(address indexed user, uint128 balance);
// Optimize for L2 gas patterns
function efficientTransfer(address to, uint128 amount) external {
UserData storage sender = users[msg.sender];
UserData storage recipient = users[to];
require(sender.balance >= amount, "Insufficient balance");
// Single storage update pattern
sender.balance -= amount;
recipient.balance += amount;
// Single event
emit BatchUpdated(to, amount);
}
}
Error Handling for L2
// L2-specific error handling
class BaseErrorHandler {
static async handleTransaction(txPromise) {
try {
const tx = await txPromise;
const receipt = await tx.wait();
if (receipt.status === 0) {
throw new Error(`Transaction failed: ${tx.hash}`);
}
return {
success: true,
hash: tx.hash,
gasUsed: receipt.gasUsed,
effectiveGasPrice: receipt.effectiveGasPrice,
cost: ethers.formatEther(receipt.gasUsed * receipt.effectiveGasPrice)
};
} catch (error) {
return this.categorizeError(error);
}
}
static categorizeError(error) {
if (error.code === 'INSUFFICIENT_FUNDS') {
return {
success: false,
type: 'INSUFFICIENT_FUNDS',
message: 'Insufficient ETH for gas fees',
suggestion: 'Bridge more ETH to Base'
};
}
if (error.message.includes('gas required exceeds allowance')) {
return {
success: false,
type: 'GAS_LIMIT',
message: 'Gas limit too low',
suggestion: 'Increase gas limit'
};
}
if (error.message.includes('nonce too low')) {
return {
success: false,
type: 'NONCE_ERROR',
message: 'Transaction nonce error',
suggestion: 'Reset account nonce'
};
}
return {
success: false,
type: 'UNKNOWN',
message: error.message,
suggestion: 'Check transaction details and try again'
};
}
}
Monitoring and Analytics
Transaction Analytics
// Monitor Base transaction patterns
class BaseAnalytics {
constructor(apiKey) {
this.provider = new ethers.JsonRpcProvider(`https://api.blockeden.xyz/base/${apiKey}`);
this.metrics = {
totalTxs: 0,
totalGasUsed: 0n,
totalCost: 0n,
avgGasPrice: 0n
};
}
async analyzeBlock(blockNumber) {
const block = await this.provider.getBlock(blockNumber, true);
const blockMetrics = {
number: block.number,
timestamp: new Date(block.timestamp * 1000),
txCount: block.transactions.length,
gasUsed: block.gasUsed,
gasLimit: block.gasLimit,
utilization: Number(block.gasUsed * 100n / block.gasLimit),
baseFeePerGas: block.baseFeePerGas
};
// Analyze individual transactions
const txAnalysis = block.transactions.map(tx => ({
hash: tx.hash,
from: tx.from,
to: tx.to,
value: tx.value,
gasLimit: tx.gasLimit,
gasPrice: tx.gasPrice,
cost: tx.gasLimit * tx.gasPrice
}));
return {
block: blockMetrics,
transactions: txAnalysis
};
}
async getNetworkStats() {
const latest = await this.provider.getBlockNumber();
const block = await this.provider.getBlock(latest);
return {
latestBlock: latest,
blockTime: 2, // ~2 seconds on Base
gasPrice: await this.provider.getFeeData(),
networkUtilization: Number(block.gasUsed * 100n / block.gasLimit)
};
}
}
Real-time Monitoring
// Monitor Base network in real-time
class BaseMonitor {
constructor(apiKey) {
this.ws = new WebSocket(`wss://api.blockeden.xyz/base/${apiKey}`);
this.stats = {
blocksPerMinute: 0,
avgTxsPerBlock: 0,
avgGasUsed: 0,
networkHealth: 'good'
};
this.setupMonitoring();
}
setupMonitoring() {
this.ws.onopen = () => {
console.log('Connected to Base monitoring');
this.subscribeToBlocks();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.processBlockData(data);
};
}
subscribeToBlocks() {
const subscribe = {
jsonrpc: '2.0',
method: 'eth_subscribe',
params: ['newHeads'],
id: 1
};
this.ws.send(JSON.stringify(subscribe));
}
processBlockData(data) {
if (data.method === 'eth_subscription') {
const block = data.params.result;
const blockNumber = parseInt(block.number, 16);
const gasUsed = parseInt(block.gasUsed, 16);
const gasLimit = parseInt(block.gasLimit, 16);
console.log(`Block ${blockNumber}: ${gasUsed.toLocaleString()} gas used (${((gasUsed/gasLimit)*100).toFixed(1)}%)`);
this.updateStats(block);
}
}
updateStats(block) {
// Update rolling statistics
this.stats.avgGasUsed = parseInt(block.gasUsed, 16);
this.stats.networkHealth = this.calculateHealth(block);
console.log('Network Stats:', this.stats);
}
calculateHealth(block) {
const gasUsed = parseInt(block.gasUsed, 16);
const gasLimit = parseInt(block.gasLimit, 16);
const utilization = gasUsed / gasLimit;
if (utilization < 0.5) return 'excellent';
if (utilization < 0.7) return 'good';
if (utilization < 0.9) return 'moderate';
return 'congested';
}
}
Security Considerations
L2 Security Model
- Optimistic Assumptions: Transactions are assumed valid
- Challenge Period: 7-day window for fraud proofs
- Sequencer Risk: Single point of failure (being decentralized)
- Bridge Security: Cross-layer communication risks