Skip to main content
The Base-Solana bridge is currently in testnet only (Base Sepolia ↔ Solana Devnet). The code is a work in progress and has not been audited. Do not use in production!
The Base-Solana bridge enables bidirectional token transfers and message passing between Base and Solana networks. This bridge allows you to:
  • Transfer tokens between Base and Solana
  • Send arbitrary cross-chain messages
  • Deploy wrapped tokens on either chain
This guide covers the bridge architecture and provides practical examples for implementation.

How it works

On Base

The Base bridge contract locks or burns your tokens and emits a message. Validators collect these messages into Merkle trees and post roots to Solana every ~15 minutes. You then prove your message exists in the tree to complete the transfer on Solana. Key Smart contracts:
What is the Twin Contract? The Twin Contract is a smart contract that acts as your execution context on Base. It represents the msg.sender on Base when you send an arbitrary contract call from Solana.

On Solana

The Solana bridge program locks or burns your tokens and emits events. Validators relay these messages to Base where they’re executed through your personal Twin contract - a smart contract that acts as your execution context on Base. Key Programs: You can access the full repository from the link below:
Base Bridge - Official Repositoryhttps://github.com/base/bridge

Bridging Flows

Solana to Base

Flow: Lock SOL → Auto-Relay → Mint wSOL on Base The Solana to Base flow in this example uses automatic relay for seamless transfers. Your SOL is locked in a Solana vault, and the relayer automatically executes the message on Base to mint wrapped SOL.
Solana to Base Examplehttps://github.com/basebase-bridge-examples/tree/main/solToBaseWithAutoRelay
solToBaseWithAutoRelay/index.ts
// Configure
const TO = "0x8c1a617bdb47342f9c17ac8750e0b070c372c721"; // Base address
const AMOUNT = 0.001; // SOL amount

// Bridge SOL with auto-relay
const ixs = [
  getBridgeSolInstruction({
    payer,
    from: payer,
    solVault: solVaultAddress,
    bridge: bridgeAccountAddress,
    outgoingMessage,
    to: toBytes(TO),
    remoteToken: toBytes("0xC5b9112382f3c87AFE8e1A28fa52452aF81085AD"), // wSOL
    amount: BigInt(AMOUNT * 10**9),
  }),
  await buildPayForRelayIx(RELAYER_PROGRAM_ID, outgoingMessage, payer)
];

await buildAndSendTransaction(SOLANA_RPC_URL, ixs, payer);

Wrap Custom SPL Tokens

The example above shows how to bridge native SOL to Base. To bridge custom SPL tokens, you need to create wrapped ERC20 representations on Base using the CrossChainERC20Factory.
Token Wrapping Examplehttps://github.com/basebase-bridge-examples/tree/main/wrapSolTokenOnBase
wrapSolTokenOnBase/index.ts
// Deploy wrapped token on Base
const mintBytes32 = getBase58Codec().encode(SOLANA_SPL_MINT_ADDRESS).toHex();

await client.writeContract({
  address: "0x58207331CBF8Af87BB6453b610E6579D9878e4EA", // Factory
  abi: TokenFactory,
  functionName: "deploy",
  args: [`0x${mintBytes32}`, "Token Name", "SYMBOL", 9],
});

Base to Solana

Flow: Burn wSOL → Wait 15min → Generate Proof → Execute on Solana The Base to Solana flow requires manual proof generation. You burn wrapped SOL on Base, wait for finalization (~15 minutes), then generate a cryptographic proof to execute on Solana and receive native SOL.
Base to Solana Examplehttps://github.com/basebase-bridge-examples/tree/main/bridgeSolFromBaseToSolana
bridgeSolFromBaseToSolana/index.ts
// Step 1: Burn wSOL on Base
const transfer = {
  localToken: "0xC5b9112382f3c87AFE8e1A28fa52452aF81085AD", // wSOL
  remoteToken: pubkeyToBytes32(SOL_ADDRESS),
  to: pubkeyToBytes32(solanaAddress),
  remoteAmount: BigInt(AMOUNT * 10**9),
};

const txHash = await client.writeContract({
  address: "0xB2068ECCDb908902C76E3f965c1712a9cF64171E", // Bridge
  abi: Bridge,
  functionName: "bridgeToken",
  args: [transfer, []],
});

// Step 2: Wait for finalization
const isProvable = await isBridgeMessageProvable(txHash);

// Step 3: Generate proof
const { event, rawProof } = await generateProof(txHash, baseBlockNumber);

// Step 4: Execute on Solana
const proveIx = getProveMessageInstruction({
  nonce: event.message.nonce,
  sender: toBytes(event.message.sender),
  data: toBytes(event.message.data),
  proof: rawProof.map(e => toBytes(e)),
  messageHash: toBytes(event.messageHash),
});

const relayIx = getRelayMessageInstruction({ message: messagePda });
await buildAndSendTransaction(SOLANA_RPC_URL, [proveIx, relayIx], payer);

Utilities

The repository includes utilities for converting between Solana and Base address formats, getting your Solana CLI keypair for signing transactions, and building and sending Solana transactions.
Base Bridge Examples - Utilitieshttps://github.com/basebase-bridge-examples/tree/main/bridgeSolFromBaseToSolana/utils

Address Conversion

Convert Solana pubkey to bytes32 for Base contracts:
example.ts
// Convert Solana pubkey to bytes32 for Base contracts
import { pubkeyToBytes32 } from "./utils/pubkeyToBytes32";

const bytes32Address = pubkeyToBytes32(solanaAddress);

Keypair Management

Get your Solana CLI keypair for signing transactions:
example.ts
import { getSolanaCliConfigKeypairSigner } from "./utils/keypair";

const payer = await getSolanaCliConfigKeypairSigner();

Transaction Building

Build and send Solana transactions:
example.ts
import { buildAndSendTransaction } from "./utils/buildAndSendTransaction";

const signature = await buildAndSendTransaction(SOLANA_RPC_URL, ixs, payer);

Sol2Base: Full Stack Example

Sol2Base - Full Stack Bridge Apphttps://github.com/base/sol2base
Sol2Base is a production-ready Next.js application that demonstrates how to build a complete frontend for the Base-Solana bridge. It features a “hacker” aesthetic with Matrix-style animations and includes wallet integration, CDP faucet, ENS/Basename resolution, and real-time transaction monitoring.

Bridge Service Implementation

The core bridge service handles SOL transfers with automatic relay and address resolution:
src/lib/bridge.ts
export class SolanaBridge {
  private connection: Connection;

  constructor() {
    this.connection = new Connection(SOLANA_DEVNET_CONFIG.rpcUrl, 'confirmed');
  }

  async createBridgeTransaction(
    walletAddress: PublicKey,
    amount: number,
    destinationAddress: string,
    signTransaction: (transaction: Transaction) => Promise<Transaction>
  ): Promise<string> {
    // Import address resolver and real bridge
    const { addressResolver } = await import('./addressResolver');
    const { realBridgeImplementation } = await import('./realBridgeImplementation');
    
    // Resolve destination address (handles ENS/basename)
    const resolvedAddress = await addressResolver.resolveAddress(destinationAddress);

    // Validate amount
    if (amount < BRIDGE_CONFIG.minBridgeAmount / Math.pow(10, 9)) {
      throw new Error(`Minimum bridge amount is ${BRIDGE_CONFIG.minBridgeAmount / Math.pow(10, 9)} SOL`);
    }

    // Create the real bridge transaction
    const transaction = await realBridgeImplementation.createBridgeTransaction(
      walletAddress,
      amount,
      resolvedAddress
    );

    // Submit the transaction
    const signature = await realBridgeImplementation.submitBridgeTransaction(
      transaction,
      walletAddress,
      signTransaction
    );

    return signature;
  }
}

Address Resolution Service

Supports ENS names and Basenames for user-friendly addressing:
src/lib/addressResolver.ts
export class AddressResolver {
  async resolveAddress(input: string): Promise<string> {
    const trimmedInput = input.trim();

    // If it's already a valid Ethereum address, return as-is
    if (this.isValidEthereumAddress(trimmedInput)) {
      return trimmedInput;
    }

    // Handle ENS names (.eth)
    if (trimmedInput.endsWith('.eth') && !trimmedInput.endsWith('.base.eth')) {
      return await this.resolveEns(trimmedInput);
    }

    // Handle basenames (.base.eth or .base)
    if (trimmedInput.endsWith('.base.eth') || trimmedInput.endsWith('.base')) {
      return await this.resolveBasename(trimmedInput);
    }

    throw new Error('Invalid address format');
  }

  private async resolveEns(ensName: string): Promise<string> {
    const response = await fetch(`https://api.ensdata.net/${ensName}`);
    const data = await response.json();
    
    if (!data.address) {
      throw new Error(`ENS name ${ensName} does not resolve to an address`);
    }

    return data.address;
  }
}

React Bridge Interface

Complete UI component with wallet integration and form validation:
src/components/BridgeInterface.tsx
export const BridgeInterface: React.FC = () => {
  const { publicKey, connected, signTransaction } = useWallet();
  const [solBalance, setSolBalance] = useState<number>(0);
  const [transactions, setTransactions] = useState<BridgeTransaction[]>([]);

  // Handle bridge transaction
  const handleBridge = async (amount: number, destinationAddress: string) => {
    if (!publicKey || !signTransaction) {
      setError('Wallet not connected');
      return;
    }

    try {
      const txHash = await solanaBridge.createBridgeTransaction(
        publicKey, 
        amount, 
        destinationAddress,
        signTransaction
      );

      // Add to transaction history
      const newTransaction: BridgeTransaction = {
        txHash,
        amount,
        destinationAddress,
        status: 'confirmed',
        timestamp: Date.now(),
        type: 'bridge'
      };

      setTransactions(prev => [newTransaction, ...prev]);
      await loadBalances();

    } catch (err) {
      setError(err instanceof Error ? err.message : 'Bridge transaction failed');
    }
  };

  return (
    <div className="max-w-4xl mx-auto space-y-8">
      <BalanceDisplay solBalance={solBalance} />
      <FaucetButton onFaucet={handleSolFaucet} />
      <BridgeForm onBridge={handleBridge} maxAmount={solBalance} />
      <TransactionStatus transactions={transactions} />
    </div>
  );
};

Bridge Form with Address Resolution

Smart form component with ENS/Basename support and validation:
src/components/BridgeForm.tsx
export const BridgeForm: React.FC<BridgeFormProps> = ({ onBridge, maxAmount }) => {
  const [amount, setAmount] = useState<string>('');
  const [destinationAddress, setDestinationAddress] = useState<string>('');
  const [resolvedAddress, setResolvedAddress] = useState<string>('');
  const [isResolvingAddress, setIsResolvingAddress] = useState<boolean>(false);

  // Debounced address resolution
  const resolveAddress = useCallback(async (address: string) => {
    if (!address.trim()) return;

    setIsResolvingAddress(true);
    try {
      const type = addressResolver.getInputType(address);
      
      if (type === 'Ethereum Address') {
        setResolvedAddress(address);
      } else {
        const resolved = await addressResolver.resolveAddress(address);
        setResolvedAddress(resolved);
      }
    } catch (error) {
      setErrors(prev => ({ 
        ...prev, 
        address: error instanceof Error ? error.message : 'Failed to resolve address'
      }));
    } finally {
      setIsResolvingAddress(false);
    }
  }, []);

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="number"
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
        placeholder="Enter SOL amount"
      />
      
      <input
        type="text"
        value={destinationAddress}
        onChange={(e) => setDestinationAddress(e.target.value)}
        placeholder="0x..., Basename, or ENS"
      />
      
      {resolvedAddress && resolvedAddress !== destinationAddress && (
        <div className="resolved-address">
Resolved to: {resolvedAddress}
        </div>
      )}
      
      <button type="submit" disabled={!amount || !resolvedAddress}>
        Bridge to Base
      </button>
    </form>
  );
};

Setup and Development

Terminal
# Clone and setup
git clone https://github.com/base/sol2base.git
cd sol2base
npm install --legacy-peer-deps

# Environment setup
cp env.template .env.local
# Add Coinbase Developer Platform (CDP) API credentials for faucet (optional)

# Start development server
npm run dev
# Open http://localhost:3000
Get your Coinbase Developer Platform (CDP) API credentials from the the portal.The example above uses the Coinbase Developer Platform faucet for SOL. To get access to the faucet API, you can follow the instructions here.

Contract Addresses

Base Sepolia

{
  "Bridge": "0xB2068ECCDb908902C76E3f965c1712a9cF64171E",
  "CrossChainERC20Factory": "0x58207331CBF8Af87BB6453b610E6579D9878e4EA",
  "WrappedSOL": "0xC5b9112382f3c87AFE8e1A28fa52452aF81085AD"
}

Solana Devnet

{
  "BridgeProgram": "HSvNvzehozUpYhRBuCKq3Fq8udpRocTmGMUYXmCSiCCc",
  "BaseRelayerProgram": "ExS1gcALmaA983oiVpvFSVohi1zCtAUTgsLj5xiFPPgL"
}

Troubleshooting

  • Ensure sufficient ETH for gas fees
  • For ERC20 tokens, approve the bridge contract first using approve()
  • Verify token addresses are correct and match the expected format
  • Check that your private key is correctly set in the .env file
  • Wait at least 15 minutes for message relay
  • Check that your Base transaction was successful and included a MessageRegistered event
  • Verify you’re using the correct network (testnet/devnet)
  • Ensure the Solana bridge has processed the Base block number
  • Ensure you’re using the latest Base block number from the Solana bridge
  • Verify the message hash matches the original transaction
  • Check that the proof was generated at the correct block height
  • Make sure all account addresses are correctly derived
  • Verify you have sufficient SOL to pay for relay fees
  • Check that the Base Relayer program is properly configured
  • Ensure the outgoing message was created successfully
  • Monitor both Solana and Base explorers for transaction status

Security

Important Security Notes:
  • Bridge is in active development and unaudited
  • Only use testnet funds (Solana devnet SOL and Base Sepolia ETH)
  • Validate all addresses before bridging
  • Monitor transactions on both chains
  • Keep your private keys secure and never share them

Resources

I