We built a cross-chain platform for an influencer project that hit $3M market cap. Solana NFTs that burned for Ethereum membership. Here's the technical breakdown and lessons from shipping cross-chain in production.
The Project: Solana NFT to Ethereum Token
The concept was simple. Buy an NFT on Solana (cheap, fast). Burn it to claim an ERC-20 membership token on Ethereum (established ecosystem).
Why cross-chain:
- Solana: Low mint costs ($0.01 vs $10+ on Ethereum)
- Ethereum: Established DeFi ecosystem for the token
- User choice: Collect NFTs or convert to liquid tokens
Results:
| Metric | Value |
|---|---|
| Solana NFTs minted | 8,500 |
| NFTs burned for Ethereum tokens | 3,200 |
| Peak token market cap | $3M |
| Development time | 3 months |
Solana vs Ethereum: Technical Differences
Before building cross-chain, understand how different these chains really are.
Programming model:
| Aspect | Solana | Ethereum |
|---|---|---|
| Language | Rust | Solidity |
| Account model | Account-based with programs | Contract-based |
| State storage | Separate from logic | Combined in contract |
| Gas/fees | Compute units | Gas |
| Finality | ~400ms | ~12 min (safe) |
Solana's account model:
// Solana: State is in accounts, programs are stateless
#[account]
pub struct NftData {
pub owner: Pubkey,
pub burned: bool,
pub burn_signature: [u8; 64],
}
// Program operates on accounts passed to it
pub fn burn_nft(ctx: Context<BurnNft>) -> Result<()> {
let nft = &mut ctx.accounts.nft_data;
nft.burned = true;
nft.burn_signature = generate_signature();
Ok(())
}
Ethereum's contract model:
// Ethereum: State and logic in same contract
contract MembershipToken is ERC20 {
mapping(bytes32 => bool) public claimed;
function claimFromSolana(
bytes32 burnProof,
bytes memory signature
) external {
require(!claimed[burnProof], "Already claimed");
require(verifySolanaSignature(burnProof, signature), "Invalid");
claimed[burnProof] = true;
_mint(msg.sender, MEMBERSHIP_AMOUNT);
}
}
Architecture Options
Three main approaches to cross-chain communication.
Option 1: Centralized relay (what we used)
Solana Program → Off-chain Indexer → Ethereum Contract
Pros: Simple, fast, cheap Cons: Centralized, trust required
Option 2: LayerZero / Wormhole
Solana Program → Bridge Protocol → Ethereum Contract
Pros: Decentralized, trustless Cons: Complex, expensive, dependency risk
Option 3: Custom oracle network
Solana Program → Multiple Validators → Ethereum Contract
Pros: Semi-decentralized, customizable Cons: Complex to build, ongoing costs
Why we chose centralized relay:
For a $3M project, the complexity and cost of decentralized bridges wasn't justified. Users trusted the project team anyway. We could ship in weeks instead of months.
Solana Program Implementation
The Solana side handles NFT minting and burning.
Program structure:
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Mint};
declare_id!("YOUR_PROGRAM_ID");
#[program]
pub mod cross_chain_nft {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let state = &mut ctx.accounts.state;
state.authority = ctx.accounts.authority.key();
state.total_burned = 0;
Ok(())
}
pub fn burn_for_ethereum(
ctx: Context<BurnForEthereum>,
ethereum_address: [u8; 20]
) -> Result<()> {
// Burn the NFT
token::burn(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Burn {
mint: ctx.accounts.nft_mint.to_account_info(),
from: ctx.accounts.nft_account.to_account_info(),
authority: ctx.accounts.owner.to_account_info(),
},
),
1,
)?;
// Record burn for indexer
let burn_record = &mut ctx.accounts.burn_record;
burn_record.nft_mint = ctx.accounts.nft_mint.key();
burn_record.owner = ctx.accounts.owner.key();
burn_record.ethereum_address = ethereum_address;
burn_record.timestamp = Clock::get()?.unix_timestamp;
burn_record.claimed = false;
// Increment counter
let state = &mut ctx.accounts.state;
state.total_burned += 1;
emit!(BurnEvent {
nft_mint: ctx.accounts.nft_mint.key(),
ethereum_address,
burn_index: state.total_burned,
});
Ok(())
}
}
#[derive(Accounts)]
pub struct BurnForEthereum<'info> {
#[account(mut)]
pub owner: Signer<'info>,
#[account(mut)]
pub nft_mint: Account<'info, Mint>,
#[account(
mut,
constraint = nft_account.owner == owner.key(),
constraint = nft_account.amount == 1
)]
pub nft_account: Account<'info, TokenAccount>,
#[account(
init,
payer = owner,
space = 8 + BurnRecord::SIZE,
seeds = [b"burn", nft_mint.key().as_ref()],
bump
)]
pub burn_record: Account<'info, BurnRecord>,
#[account(mut)]
pub state: Account<'info, ProgramState>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct BurnRecord {
pub nft_mint: Pubkey,
pub owner: Pubkey,
pub ethereum_address: [u8; 20],
pub timestamp: i64,
pub claimed: bool,
}
impl BurnRecord {
pub const SIZE: usize = 32 + 32 + 20 + 8 + 1;
}
#[event]
pub struct BurnEvent {
pub nft_mint: Pubkey,
pub ethereum_address: [u8; 20],
pub burn_index: u64,
}
Ethereum Contract Implementation
The Ethereum side verifies burns and mints tokens.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract CrossChainMembership is ERC20, Ownable {
using ECDSA for bytes32;
address public relayer;
uint256 public constant TOKENS_PER_NFT = 1000 * 10**18;
mapping(bytes32 => bool) public claimed;
mapping(bytes32 => ClaimData) public claims;
struct ClaimData {
address claimer;
uint256 timestamp;
bytes32 solanaTxHash;
}
event Claimed(
address indexed claimer,
bytes32 indexed burnProof,
uint256 amount
);
constructor(address _relayer) ERC20("Membership", "MEMBER") Ownable(msg.sender) {
relayer = _relayer;
}
function claimFromSolana(
bytes32 burnProof,
bytes32 solanaTxHash,
bytes memory signature
) external {
require(!claimed[burnProof], "Already claimed");
// Verify relayer signature
bytes32 messageHash = keccak256(
abi.encodePacked(
msg.sender,
burnProof,
solanaTxHash,
block.chainid
)
);
bytes32 ethSignedHash = messageHash.toEthSignedMessageHash();
require(ethSignedHash.recover(signature) == relayer, "Invalid signature");
// Mark as claimed
claimed[burnProof] = true;
claims[burnProof] = ClaimData({
claimer: msg.sender,
timestamp: block.timestamp,
solanaTxHash: solanaTxHash
});
// Mint tokens
_mint(msg.sender, TOKENS_PER_NFT);
emit Claimed(msg.sender, burnProof, TOKENS_PER_NFT);
}
function setRelayer(address _relayer) external onlyOwner {
relayer = _relayer;
}
// Emergency functions
function pause() external onlyOwner {
// Implement pause logic
}
}
The Relay/Indexer
The bridge between chains. This is the centralized piece.
// Simplified indexer logic
import { Connection, PublicKey } from '@solana/web3.js';
import { ethers } from 'ethers';
class CrossChainRelay {
private solanaConnection: Connection;
private ethWallet: ethers.Wallet;
private contract: ethers.Contract;
async indexBurnEvents() {
// Subscribe to Solana program logs
this.solanaConnection.onLogs(
PROGRAM_ID,
async (logs) => {
const burnEvent = this.parseBurnEvent(logs);
if (burnEvent) {
await this.processBurn(burnEvent);
}
}
);
}
async processBurn(event: BurnEvent) {
// Generate proof
const burnProof = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
['bytes32', 'bytes20', 'uint64'],
[event.nftMint, event.ethereumAddress, event.burnIndex]
)
);
// Sign for Ethereum claim
const messageHash = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'bytes32', 'bytes32', 'uint256'],
[
ethers.getAddress('0x' + Buffer.from(event.ethereumAddress).toString('hex')),
burnProof,
event.txHash,
CHAIN_ID
]
)
);
const signature = await this.ethWallet.signMessage(
ethers.getBytes(messageHash)
);
// Store for user to claim
await this.storePendingClaim({
ethereumAddress: event.ethereumAddress,
burnProof,
solanaTxHash: event.txHash,
signature
});
}
}
Security Considerations
Cross-chain adds attack surface. Here's what we worried about.
1. Replay attacks
Same burn proof used on multiple chains or multiple times.
// Include chain ID in signature
bytes32 messageHash = keccak256(
abi.encodePacked(
msg.sender,
burnProof,
solanaTxHash,
block.chainid // Prevents cross-chain replay
)
);
2. Relayer compromise
If relayer key is stolen, attacker can mint unlimited tokens.
Mitigations:
- Multi-sig relayer
- Rate limiting on contract
- Maximum supply cap
3. Solana reorgs
Solana transactions can be dropped. Wait for finality.
// Wait for confirmation
const confirmation = await connection.confirmTransaction(
signature,
'finalized' // Not 'processed' or 'confirmed'
);
4. Front-running claims
Someone sees burn and claims before legitimate owner.
// Ethereum address specified in Solana burn
// Only that address can claim
require(
msg.sender == specifiedEthereumAddress,
"Wrong claimer"
);
Development Timeline
Cross-chain takes longer than you think.
| Phase | Duration | What We Built |
|---|---|---|
| Design | 2 weeks | Architecture, security model |
| Solana program | 3 weeks | Mint, burn, events |
| Ethereum contract | 2 weeks | Claim, verification |
| Indexer/relay | 3 weeks | Event processing, signatures |
| Frontend | 2 weeks | Wallet connection, UX |
| Testing | 2 weeks | Integration, edge cases |
| Audit prep | 1 week | Documentation, cleanup |
| Total | 15 weeks | Full cross-chain system |
Compare to single-chain:
| Single-chain | Duration |
|---|---|
| Smart contract | 2 weeks |
| Frontend | 1 week |
| Testing | 1 week |
| Total | 4 weeks |
Cross-chain added 11 weeks of development time.
When to Go Cross-Chain
Good reasons:
- Leveraging specific chain strengths (Solana speed, Ethereum liquidity)
- User base on multiple chains
- Arbitrage opportunities between chains
Bad reasons:
- "It sounds cool"
- Following trends
- Adding complexity for marketing
Our recommendation:
Start single-chain. Add cross-chain only when you have:
- Proven product-market fit
- Resources for 3x development time
- Budget for additional security audits
- Team with experience on both chains
Resources
Solana Development:
- Anchor - Solana framework
- Solana Cookbook - Examples and guides
- Solana Docs - Official documentation
Ethereum Development:
- Hardhat - Development environment
- OpenZeppelin - Contract libraries
- Ethers.js - JavaScript library
Cross-Chain Infrastructure:
Testing:
- Foundry - Ethereum testing
- Anchor Test - Solana testing
Development Services:
- AllThingsWeb3 - Smart contract development
- Token Cost Calculator - Estimate costs