Skip to main content
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
  • Combine both flows (transfer with arbitrary calls)
  • Deploy wrapped tokens on either chain
This guide covers the bridge architecture, the production addresses, and practical implementation patterns.

How it works

On Base

The Base bridge contract locks or burns tokens when sending tokens to Solana, and mints or unlocks tokens when receiving tokens from Solana. The Bridge contract itself builds Merkle trees from outgoing messages. Validators verify the Merkle root every ~300 finalized blocks and relay it to Solana. You then prove your message exists in the tree to complete the transfer on Solana.
Tokens that are native to Base are locked and tokens that are native to Solana are burned when bridging to Solana. Tokens that are native to Solana are minted and tokens that are native to Base are unlocked when bridging to Base.
Key Smart contracts:
What is the Twin Contract?
Each Solana wallet deterministically maps to a Twin contract on Base. When you attach a contract call to a bridge message, the call is executed from this Twin contract, ie. the Twin becomes msg.sender on Base.

On Solana

The Solana bridge program handles token transfers by locking or burning tokens and emitting events. For messaging, validators relay these events to Base where they are executed through your Twin contract. Key Programs (Solana Mainnet-Beta):
The relayer program is not part of the core bridge. It is an optional convenience layer that can pay Base gas fees on behalf of the Solana user in the Solana → Base direction.The user would still need to pay the gas fee by adding PayForRelay to the Solana transaction. If the user does not add PayForRelay, the relayer program will not pay the gas fee.
You can access the full repository here:

Bridging Flows

Solana to Base

Flow: Lock SOL/SPL → (Optional) Pay for relay → Validators approve → Mint + execute on Base The Solana to Base bridge uses a pull-based model that requires 3 steps:
  1. Initiate the bridge on Solana - Lock your SOL or native SPL token in a Solana vault
  2. Wait for validators to pre-approve the message - Validators verify and approve your bridge message
  3. Execute the message on Base - The approved message is executed on Base to mint SOL and execute any additional arbitrary calls
When bridging from Solana to Base, native SOL/SPL are locked and ERC20 SOL is minted on Base.
Reference scripts (auto-relay, token wrapping, CLI utilities) live in the scripts/ directory of the official repository:

Auto-Relay Example

This is a sample script that shows how to bridge SOL with auto-relay
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"), // SOL on Base
    amount: BigInt(AMOUNT * 10**9),
  }),
  await buildPayForRelayIx(RELAYER_PROGRAM_ID, outgoingMessage, payer)
];

await buildAndSendTransaction(SOLANA_RPC_URL, ixs, payer);
For more details, see the Solana to Base Relay Script.

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.
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 ERC20 SOL on Base → Wait for finalization → Generate Merkle proof → Execute on Solana Burn wrapped tokens on Base, wait for the message to become provable, then execute the proof on Solana to unlock the native asset. This path offers full custody and requires a prover.
bridgeSolFromBaseToSolana/index.ts
// Step 1: Burn SOL on Base
const transfer = {
  localToken: "0xC5b9112382f3c87AFE8e1A28fa52452aF81085AD", // SOL (on Base)
  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.

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);

Terminally Onchain Example

Terminally Onchain is a production Next.js app that exposes the bridge via a command terminal UI. Users connect a Solana wallet, type commands such as to bridge and call a contract on Base:
bridge 0.0001 sol 0xYourTwin --call-contract 0x311935Cd80B76769bF2ecC9D8Ab7635b2139cf82 \
  --call-selector "transfer(address,uint256)" \
  --call-args 0x0000000000000000000000000000000000000000 100000000000000
The workflow:
  1. Parse command: The terminal parser resolves the asset, destination, and optional Base call (selector + args + value).
  2. Stage bridge: queueBridge validates SPL overrides, ABI-encodes the Base call via encodeFunctionData, and stages relay overrides.
  3. Execute: solanaBridge.bridge() resolves the destination (ENS/Basename), ensures balances, and calls realBridgeImplementation to sign and send the Solana transaction.
  4. Relay + Call: If relay gas is prepaid, the Base Relayer executes the attached call from the user’s Twin contract immediately after ERC20 SOL is minted.
Key implementation references:
  • src/lib/bridge.ts: Asset resolution (supports mint addresses), environment-aware RPC connections, and call attachment support.
  • src/lib/realBridgeImplementation.ts: Builds Solana transactions with PayForRelay + bridge_sol/bridge_spl instructions, using per-environment PDAs and gas-fee receivers.
  • src/components/MainContent.tsx: Terminal UI with command staging, log viewer, and ABI encoding for arbitrary Base calls.
  • src/components/WalletConnection.tsx: Fetches the deterministic Twin address on Base Mainnet/Sepolia for the connected Solana wallet.

Running the Terminal

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

# Configure env (RPC URLs, relayer addresses, CDP API keys, etc.)
cp env.template .env.local

npm run dev   # defaults to http://localhost:3000
The terminal exposes both Base Sepolia ↔ Solana Devnet and Base Mainnet ↔ Solana Mainnet. Use the network dropdown in the UI to switch.Set CDP_API_KEY in your .env file to get access to the faucet.

Contract Addresses

Base Mainnet

{
  "Bridge": "0x3eff766C76a1be2Ce1aCF2B69c78bCae257D5188",
  "BridgeValidator": "0xAF24c1c24Ff3BF1e6D882518120fC25442d6794B",
  "CrossChainERC20Factory": "0xDD56781d0509650f8C2981231B6C917f2d5d7dF2",
  "SOL": "0x311935Cd80B76769bF2ecC9D8Ab7635b2139cf82"
}

Solana Mainnet

{
  "BridgeProgram": "HNCne2FkVaNghhjKXapxJzPaBvAKDG1Ge3gqhZyfVWLM",
  "BaseRelayerProgram": "g1et5VenhfJHJwsdJsDbxWZuotD5H4iELNG61kS4fb9"
}

Base Sepolia

{
  "Bridge": "0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B",
  "BridgeValidator": "0xa80C07DF38fB1A5b3E6a4f4FAAB71E7a056a4EC7",
  "CrossChainERC20Factory": "0x488EB7F7cb2568e31595D48cb26F63963Cc7565D",
  "SOL": "0xCace0c896714DaF7098FFD8CC54aFCFe0338b4BC",
  "FLYWHEEL_ADDRESS": "0x00000f14ad09382841db481403d1775adee1179f",
  "BRIDGE_CAMPAIGN_ADDRESS": "0xb61A842E4361C53C3f3c376DF3758b330BD6201c"
}

Solana Devnet

{
  "BridgeProgram": "7c6mteAcTXaQ1MFBCrnuzoZVTTAEfZwa6wgy4bqX3KXC",
  "BaseRelayerProgram": "56MBBEYAtQAdjT4e1NzHD8XaoyRSTvfgbSVVcEcHj51H",
  "GasFeeReceiver": "AFs1LCbodhvwpgX3u3URLsud6R1XMSaMiQ5LtXw4GKYT"
}

Resources