Skip to Content
EVMMigrate from Solana

How to Migrate from Solana to Sei EVM

Sei offers a unique value proposition for Solana developers: the parallelized execution model you’re familiar with, combined with full EVM compatibility and the extensive Ethereum tooling ecosystem. This guide helps Rust/Anchor developers translate their mental models and codebases to Solidity on Sei.

Why Solana Developers Choose Sei

  • Familiar parallelization – Sei uses optimistic parallel execution similar to Solana’s Sealevel
  • 400ms block times – Comparable to Solana’s speed, with instant finality
  • ~100 MGas/s throughput – High performance without sacrificing EVM compatibility
  • EVM ecosystem access – Leverage Ethereum’s mature tooling, audited contracts, and developer resources
  • No dependency declarations – Unlike Solana, Sei handles parallelization automatically

Understanding the Paradigm Shift

Before diving into code, it’s essential to understand the fundamental architectural differences between Solana and EVM-based chains like Sei.

Execution Model Comparison

AspectSolanaSei EVM
LanguageRust (with Anchor framework)Solidity
Account ModelPrograms + Accounts (separated code and data)Contracts (code and storage unified)
State StorageFlat account data with owner programsContract storage slots (key-value)
ParallelizationExplicit (declare accounts upfront)Optimistic (automatic conflict detection)
Block Time~400ms400ms
Finality~2.5-4.5 seconds (32 confirmations)Instant (single block)
Fee ModelCompute units + priority fees + rentGas × Gas Price (no rent)
Cross-Contract CallsCPI (Cross-Program Invocation)Internal/External function calls
Deterministic AddressesPDAs (Program Derived Addresses)CREATE2 / ImmutableCreate2Factory
Token StandardSPL TokenERC-20 / ERC-721 / ERC-1155
Dev ToolingAnchor, Solana CLI, solana-web3.jsHardhat, Foundry, ethers.js, viem

Core Concept Mapping

Programs → Smart Contracts

On Solana, you write programs that are stateless executables. Data lives in separate accounts that programs can read and modify. On Sei EVM, smart contracts combine code and state in a single entity.

// Solana: Program is stateless, data in accounts #[program] pub mod counter { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = 0; counter.authority = ctx.accounts.authority.key(); Ok(()) } pub fn increment(ctx: Context<Increment>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; Ok(()) } } #[account] pub struct Counter { pub count: u64, pub authority: Pubkey, } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = authority, space = 8 + 8 + 32)] pub counter: Account<'info, Counter>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, }

Key differences:

  • No account space allocation needed—storage grows dynamically
  • No explicit Signer validation—msg.sender is always authenticated
  • No system program imports—native operations are built into the EVM

PDAs → CREATE2 Deterministic Addresses

Solana’s Program Derived Addresses (PDAs) let you create deterministic addresses from seeds. On EVM, you achieve similar functionality with CREATE2.

// Solana: PDA derivation let (pda, bump) = Pubkey::find_program_address( &[ b"vault", user.key().as_ref(), ], &program_id, ); // In Anchor account validation #[account( seeds = [b"vault", user.key().as_ref()], bump, )] pub vault: Account<'info, Vault>,

CPI → Contract Calls

Solana’s Cross-Program Invocation (CPI) becomes simple function calls in Solidity:

// Solana: CPI to token program use anchor_spl::token::{self, Transfer}; let cpi_accounts = Transfer { from: ctx.accounts.from_token_account.to_account_info(), to: ctx.accounts.to_token_account.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); token::transfer(cpi_ctx, amount)?;

SPL Token → ERC-20

// Solana SPL Token - requires token accounts #[derive(Accounts)] pub struct TransferTokens<'info> { #[account(mut)] pub from: Account<'info, TokenAccount>, #[account(mut)] pub to: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, }

Key ERC-20 differences from SPL:

  • No Associated Token Accounts (ATAs)—balances are stored directly in the contract
  • Approvals use approve() / transferFrom() pattern
  • No mint/freeze authorities in basic implementation (add via Ownable)

Fee Model Translation

Understanding the fee differences is crucial for accurate cost estimation:

Solana ConceptSei EVM EquivalentNotes
Compute Units (CU)GasBoth measure computational work
Priority FeeGas PriceHigher price = faster inclusion
RentNoneSei has no rent; storage is permanent
Rent ExemptionN/ANo minimum balance requirements
Base FeeDynamic Base FeeSei doesn’t burn base fee
// Solana fee estimation const computeUnits = 200_000; const priorityFee = 1_000; // microlamports per CU const rentExempt = await connection.getMinimumBalanceForRentExemption(accountSize); // Sei EVM fee estimation const gasLimit = 200_000n; const gasPrice = await provider.getGasPrice(); // ~0.1 gwei on Sei const fee = gasLimit * gasPrice; // No rent to consider

No Rent on Sei!

Unlike Solana where accounts can be garbage collected if rent isn’t paid, Sei EVM storage is permanent. This simplifies your application logic—no need to track rent-exempt minimums or worry about account closure.

Parallelization: Automatic vs Explicit

One of the biggest advantages of Sei for Solana developers is that parallelization is automatic.

Solana: Explicit Account Declaration

On Solana, you must declare all accounts a transaction will touch upfront:

// Solana: Must declare all accounts for parallelization #[derive(Accounts)] pub struct Swap<'info> { #[account(mut)] pub user_token_a: Account<'info, TokenAccount>, #[account(mut)] pub user_token_b: Account<'info, TokenAccount>, #[account(mut)] pub pool_token_a: Account<'info, TokenAccount>, #[account(mut)] pub pool_token_b: Account<'info, TokenAccount>, #[account(mut)] pub pool_state: Account<'info, PoolState>, // ... more accounts }

Sei: Optimistic Parallelization

On Sei, you write normal Solidity—the runtime handles parallelization:

// Sei EVM: Just write normal code function swap( address tokenIn, address tokenOut, uint256 amountIn ) external returns (uint256 amountOut) { // Sei's parallelization engine automatically: // 1. Estimates which storage slots will be accessed // 2. Runs non-conflicting transactions in parallel // 3. Re-executes conflicts sequentially IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); amountOut = calculateOutput(amountIn); IERC20(tokenOut).transfer(msg.sender, amountOut); }

Optimizing for Parallelization

While Sei handles parallelization automatically, you can still optimize your contracts for better parallel performance. Avoid global counters updated on every transaction—use user-partitioned storage instead. See Optimizing for Parallelization for detailed patterns.

Step 1: Set Up Your Development Environment

Install Required Tools

# Install Node.js (if not already installed) # https://nodejs.org/ # Install Hardhat (recommended for Solana devs transitioning) npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox # Or install Foundry (Rust-based, may feel more familiar) curl -L https://foundry.paradigm.xyz | bash foundryup

Configure for Sei

hardhat.config.ts
import { HardhatUserConfig } from 'hardhat/config'; import '@nomicfoundation/hardhat-toolbox'; const config: HardhatUserConfig = { solidity: '0.8.22', networks: { seiMainnet: { url: 'https://evm-rpc.sei-apis.com', chainId: 1329, accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] }, seiTestnet: { url: 'https://evm-rpc-testnet.sei-apis.com', chainId: 1328, accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] } }, etherscan: { apiKey: { seiMainnet: 'dummy', // Seitrace doesn't require API key seiTestnet: 'dummy' }, customChains: [ { network: 'seiMainnet', chainId: 1329, urls: { apiURL: 'https://seitrace.com/pacific-1/api', browserURL: 'https://seitrace.com' } }, { network: 'seiTestnet', chainId: 1328, urls: { apiURL: 'https://seitrace.com/atlantic-2/api', browserURL: 'https://seitrace.com' } } ] } }; export default config;

Wallet Setup

Configure MetaMask or any EVM wallet with Sei:

const seiMainnet = { chainId: '0x531', // 1329 in hex chainName: 'Sei', nativeCurrency: { name: 'Sei', symbol: 'SEI', decimals: 18 }, rpcUrls: ['https://evm-rpc.sei-apis.com'], blockExplorerUrls: ['https://seitrace.com'] };

Step 2: Translate Your Solana Program

Common Pattern Translations

Initializing State

pub fn initialize(ctx: Context<Initialize>, initial_value: u64) -> Result<()> { let state = &mut ctx.accounts.state; state.value = initial_value; state.authority = ctx.accounts.authority.key(); state.bump = ctx.bumps.state; Ok(()) } #[account] pub struct State { pub value: u64, pub authority: Pubkey, pub bump: u8, }

Access Control

// Solana: Check signer matches authority pub fn restricted_action(ctx: Context<RestrictedAction>) -> Result<()> { require!( ctx.accounts.authority.key() == ctx.accounts.state.authority, ErrorCode::Unauthorized ); // ... action Ok(()) } #[derive(Accounts)] pub struct RestrictedAction<'info> { #[account(mut)] pub state: Account<'info, State>, pub authority: Signer<'info>, }

Error Handling

// Solana: Custom error enum #[error_code] pub enum ErrorCode { #[msg("Insufficient balance")] InsufficientBalance, #[msg("Invalid amount")] InvalidAmount, #[msg("Unauthorized")] Unauthorized, } // Usage require!(amount > 0, ErrorCode::InvalidAmount);

Events/Logs

// Solana: Emit event macro use anchor_lang::prelude::*; #[event] pub struct TransferEvent { pub from: Pubkey, pub to: Pubkey, pub amount: u64, } // Emit emit!(TransferEvent { from: ctx.accounts.from.key(), to: ctx.accounts.to.key(), amount, });

Step 3: Frontend Migration

SDK Comparison

SolanaSei EVMNotes
@solana/web3.jsethers.js / viemCore blockchain interaction
@coral-xyz/anchortypechainType-safe contract interaction
@solana/wallet-adapterwagmi / RainbowKitWallet connection
Phantom, SolflareMetaMask, Rabby, CompassPopular wallets

Code Translation

import { Connection, PublicKey } from '@solana/web3.js'; import { Program, AnchorProvider } from '@coral-xyz/anchor'; // Connect const connection = new Connection('https://api.mainnet-beta.solana.com'); const provider = new AnchorProvider(connection, wallet, {}); const program = new Program(idl, programId, provider); // Read state const state = await program.account.state.fetch(stateAddress); // Send transaction const tx = await program.methods .increment() .accounts({ state: stateAddress, authority: wallet.publicKey }) .rpc();

Step 4: Testing Your Migrated Code

Test Framework Comparison

import * as anchor from '@coral-xyz/anchor'; import { Program } from '@coral-xyz/anchor'; import { Counter } from '../target/types/counter'; describe('counter', () => { const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace.Counter as Program<Counter>; it('Initializes', async () => { const counter = anchor.web3.Keypair.generate(); await program.methods.initialize().accounts({ counter: counter.publicKey }).signers([counter]).rpc(); const account = await program.account.counter.fetch(counter.publicKey); expect(account.count.toNumber()).to.equal(0); }); });

Step 5: Deploy and Verify

Deploy to Testnet

# Hardhat npx hardhat run scripts/deploy.ts --network seiTestnet # Foundry forge create --rpc-url https://evm-rpc-testnet.sei-apis.com \ --private-key $PRIVATE_KEY \ src/Counter.sol:Counter

Verify Contract

# Hardhat npx hardhat verify --network seiTestnet <CONTRACT_ADDRESS> # Foundry forge verify-contract \ --chain-id 1328 \ --verifier blockscout \ --verifier-url https://seitrace.com/atlantic-2/api \ <CONTRACT_ADDRESS> \ src/Counter.sol:Counter

Common Migration Pitfalls

1. Expecting Rent

// ❌ Wrong: No need to check rent exemption require(address(this).balance >= rentExempt, "Not rent exempt"); // ✅ Correct: Just use the contract normally // Storage persists without rent payments

2. Manual Account Validation

// ❌ Wrong: Over-engineering account checks (Solana habit) require(accountOwner == expectedOwner, "Invalid account owner"); // ✅ Correct: EVM handles this via contract addresses // msg.sender is already authenticated

3. Expecting Explicit Parallelization

// ❌ Wrong: Trying to declare "accounts" for parallelization function swap(address[] memory accounts) external { ... } // ✅ Correct: Write normal code, Sei handles parallelization function swap(address tokenIn, address tokenOut, uint256 amount) external { ... }

4. Using Lamports Mental Model

// ❌ Wrong: Solana-style lamports uint256 amount = 1_000_000_000; // 1 SOL in lamports // ✅ Correct: Use wei (18 decimals for SEI) uint256 amount = 1 ether; // 1 SEI = 1e18 wei uint256 amount = 1e18; // Same thing

Ecosystem Infrastructure

Available on Sei

ComponentSolana EquivalentSei Address/Info
Multicall3N/A0xcA11bde05977b3631167028862bE2a173976CA11
Permit2N/A0xB952578f3520EE8Ea45b7914994dcf4702cEe578
CREATE2 FactoryN/A0x0000000000FFe8B47B3e2130213B802212439497
OraclePyth, SwitchboardPyth, Redstone, Chainlink, Native Oracle
BridgeWormholeLayerZero, Wormhole, many more..

Helpful Resources

Learning Solidity

Sei-Specific

Need Help?

Join the Sei Tech Chat  on Telegram for developer support. The community is active and helpful for migration questions.

Last updated on