# A Practical Guide to Solana for Ethereum Devs I've been trying to learn how to build on Solana but it's been a pain in the ass. I compiled everything I learned in these past few weeks to hopefully make things slightly easier for everyone after me. This is a to-the-point, no BS guide that aims to provide Ethereum developers with a practical understanding of how things work on Solana through direct comparisons and hands-on code examples. ## The Fundamental Concept: Everything in Solana is an Account Before diving into code, you need to understand the most important concept in Solana: **everything is an account**. This is radically different from Ethereum's model and understanding it is crucial to building on Solana. ![Solana Account Model](https://private-us-east-1.manuscdn.com/sessionFile/PQoAb6GTc52jDZQl7a8JOB/sandbox/g5wyPDTj4TW3BspM71wtDZ-images_1752301912782_na1fn_L2hvbWUvdWJ1bnR1L3NvbGFuYV9hY2NvdW50X21vZGVsX2NsZWFu.png?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9wcml2YXRlLXVzLWVhc3QtMS5tYW51c2Nkbi5jb20vc2Vzc2lvbkZpbGUvUFFvQWI2R1RjNTJqRFpRbDdhOEpPQi9zYW5kYm94L2c1d3lQRFRqNFRXM0JzcE03MXd0RFotaW1hZ2VzXzE3NTIzMDE5MTI3ODJfbmExZm5fTDJodmJXVXZkV0oxYm5SMUwzTnZiR0Z1WVY5aFkyTnZkVzUwWDIxdlpHVnNYMk5zWldGdS5wbmciLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3OTg3NjE2MDB9fX1dfQ__&Key-Pair-Id=K2HSFNDJXOU9YS&Signature=gisbjIRBvj883l-3bHoExvgqMDaSlRolFVxtc5zZXQ9-YCXeErqQzMajrq~GKengeRwqNkQ3toX0WXI9fc3F48zHAMpj1tip9BsK17cnGoXaJR4qV5Tvu51b3E9xadVYZxvRtHo3zqylGGEAs2LF997U6mhLEvH7lms~dhjlCABTq7Tly67mgCdepb-Tg3MTjxRMZ0QEi9mdQZxyvRfNFioBtFlnom9Yb3gS8nSZUFOykr2nf4Nj~QvQU20Gtlyf4Yz2TR6Cga5wbE0tasOh3A41HUF9jxbD7i9BY7Z4OaUuLm2MZMq3HuSEWtkAENqhJg5aFZxPHfkwJkm3piHEew__) In Ethereum, you have two distinct types of entities: - **Externally Owned Accounts (EOAs)**: Controlled by private keys, can send transactions - **Contract Accounts**: Contain code and storage, cannot initiate transactions Solana unifies everything under a single concept: **accounts**. Your wallet, programs (smart contracts), token balances, user data - everything is an account with the same basic structure but different properties. ### What Exactly is an Account? An account in Solana is simply a data structure that contains: ```typescript interface SolanaAccount { publicKey: PublicKey; // Unique 32-byte address lamports: number; // SOL balance (1 SOL = 1 billion lamports) data: Buffer; // Raw data storage (can be empty) owner: PublicKey; // Which program controls this account executable: boolean; // Can this account be executed as a program? rentEpoch: number; // Last time rent was collected } ``` The magic is in the `owner` and `executable` fields. These determine what type of "thing" the account represents: **Your Wallet (Ethereum EOA equivalent):** ```typescript { publicKey: "Alice123...", lamports: 5000000000, // 5 SOL data: Buffer.alloc(0), // Empty - no data stored owner: SystemProgram.programId, // Owned by System Program executable: false, // Cannot be executed rentEpoch: 350 } ``` **A Program (Ethereum Contract equivalent):** ```typescript { publicKey: "CounterProg...", lamports: 1000000, // Rent exemption amount data: Buffer.from([...]), // Compiled bytecode owner: BPF_LOADER_UPGRADEABLE_PROGRAM_ID, // Owned by BPF Loader Upgradeable executable: true, // Can be executed! rentEpoch: 350 } ``` **A Data Account (Ethereum Contract Storage equivalent):** ```typescript { publicKey: "Counter123...", lamports: 2000000, // Rent exemption amount data: Buffer.from([42]), // Contains count value owner: CounterProgram.programId, // Owned by counter program executable: false, // Cannot be executed rentEpoch: 350 } ``` ### How This Relates to Ethereum Let's map this to what you know from Ethereum: | Ethereum Concept | Solana Equivalent | Account Properties | |------------------|-------------------|-------------------| | Your EOA address | Your wallet account | `owner: SystemProgram`, `executable: false` | | Contract address | Program account | `owner: BPFLoaderUpgradeable`, `executable: true` | | Contract storage | Data accounts | `owner: YourProgram`, `executable: false` | | ETH balance | Lamports in wallet account | Stored in `lamports` field | | Contract state | Data in owned accounts | Stored in `data` field | ### The Owner Relationship: Who Controls What The `owner` field is crucial. Only the owner program can modify an account's data. This creates a hierarchy: - **System Program** owns user wallets and can transfer SOL between them - **BPF Loader Upgradeable** owns program accounts and loads executable code - **Your Program** owns data accounts it creates and can modify their data - **Token Program** owns all token accounts and manages token transfers Think of it like file permissions in an operating system. The System Program is like the OS kernel, your programs are like applications, and data accounts are like files that only specific applications can modify. ### Programs vs Smart Contracts: The Separation of Code and State In Ethereum, a smart contract combines code and state in one entity: ```solidity contract Counter { uint256 public count = 0; // State and code together function increment() public { count += 1; } } ``` Solana completely separates these concerns. Programs contain only logic and are stateless: ```rust use anchor_lang::prelude::*; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] pub mod counter { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = 0; Ok(()) } pub fn increment(ctx: Context<Increment>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count += 1; Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { #[account( init, payer = user, space = 8 + 8 // discriminator + u64 )] pub counter: Account<'info, Counter>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Increment<'info> { #[account(mut)] pub counter: Account<'info, Counter>, } #[account] pub struct Counter { pub count: u64, } ``` Notice how the program defines the logic, but the actual count data lives in a separate `Counter` account. The program is stateless - it can operate on any `Counter` account passed to it. ### Why This Architecture Matters This separation enables several powerful capabilities: 1. **Parallel Processing**: Since programs declare which accounts they'll access upfront, Solana can run multiple transactions in parallel if they don't conflict 2. **Composability**: Programs can easily interact with accounts owned by other programs, creating flexible composability patterns 3. **Efficiency**: Multiple users can share the same program code, reducing storage costs 4. **Flexibility**: You can have multiple instances of the same data structure (multiple counters) without deploying new code ### Account Lifecycle and Rent Unlike Ethereum where storage is permanent once paid for, Solana accounts must pay "rent" to stay alive. However, if an account has enough SOL to cover about 2 years of rent, it becomes "rent-exempt" and effectively permanent. When you create an account, Anchor automatically calculates the rent exemption amount: ```rust #[derive(Accounts)] pub struct CreateAccount<'info> { #[account( init, payer = payer, space = 8 + 1000 // Anchor calculates rent for 1008 bytes )] pub my_account: Account<'info, MyData>, #[account(mut)] pub payer: Signer<'info>, pub system_program: Program<'info, System>, } ``` The `payer` automatically funds the account with enough SOL to make it rent-exempt. Think of it like a security deposit that you get back if you close the account. ## Transaction Structure and Parallel Execution Now that you understand accounts, let's see how transactions work. In Ethereum, transactions can dynamically access any storage: ```solidity // Ethereum: Can access any storage during execution function transfer(address to, uint256 amount) public { balances[msg.sender] -= amount; // Dynamic storage access balances[to] += amount; } ``` Solana requires you to declare all accounts upfront: ```typescript // Solana: Must declare all accounts before execution await program.methods .increment() .accounts({ counter: counterAccount, // Must specify which counter }) .rpc(); ``` This upfront declaration enables Solana's Sealevel runtime to analyze transactions and run them in parallel. If two transactions don't share any writable accounts, they can execute simultaneously: ```typescript // These can run in parallel - different accounts const tx1 = program.methods.increment().accounts({ counter: counter1 }); const tx2 = program.methods.increment().accounts({ counter: counter2 }); // These must run sequentially - same account const tx3 = program.methods.increment().accounts({ counter: counter1 }); ``` ## Working with SPL Tokens: A Completely Different Mental Model This is where Solana differs most dramatically from Ethereum. Understanding SPL tokens requires completely rethinking how tokens work. ### Ethereum ERC-20: One Contract, Internal Mapping In Ethereum, each token is its own smart contract with an internal mapping: ```solidity contract MyToken { mapping(address => uint256) private _balances; // All balances in one place function transfer(address to, uint256 amount) public { _balances[msg.sender] -= amount; _balances[to] += amount; } function balanceOf(address account) public view returns (uint256) { return _balances[account]; } } ``` Every user's balance is stored inside the token contract. Your Ethereum address automatically has a balance (initially 0) in every ERC-20 contract. ### Solana SPL: Shared Program, Individual Accounts Solana uses a completely different approach. There's **one shared SPL Token Program** that manages all tokens. Each token has: 1. **One Mint Account** - Defines the token properties (like the ERC-20 contract) 2. **Many Token Accounts** - Individual accounts for each user-token combination Here's the mental model: ``` Ethereum ERC-20: MyToken Contract (0x123...) ├── balances[Alice] = 100 ├── balances[Bob] = 50 └── balances[Carol] = 25 Solana SPL Token: SPL Token Program (shared) ├── MyToken Mint Account (defines token properties) ├── Alice's MyToken Account (balance: 100, mint: MyToken) ├── Bob's MyToken Account (balance: 50, mint: MyToken) └── Carol's MyToken Account (balance: 25, mint: MyToken) ``` **Key Difference**: In Ethereum, your address automatically has a balance in every token. In Solana, you need a separate token account for each token you want to hold. ### Creating SPL Tokens **Step 1: Create the Mint Account (like deploying an ERC-20)** ```rust use anchor_spl::token::{Mint, Token}; #[derive(Accounts)] pub struct CreateMint<'info> { #[account( init, payer = payer, mint::decimals = 6, mint::authority = mint_authority, )] pub mint: Account<'info, Mint>, /// CHECK: This is not dangerous because we don't read or write from this account pub mint_authority: AccountInfo<'info>, #[account(mut)] pub payer: Signer<'info>, pub token_program: Program<'info, Token>, pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>, } ``` **Step 2: Create Token Accounts for Users** ```rust use anchor_spl::token::{TokenAccount, Token, Mint}; #[derive(Accounts)] pub struct CreateTokenAccount<'info> { #[account( init, payer = payer, token::mint = mint, token::authority = owner, )] pub token_account: Account<'info, TokenAccount>, pub mint: Account<'info, Mint>, /// CHECK: This is not dangerous because we don't read or write from this account pub owner: AccountInfo<'info>, #[account(mut)] pub payer: Signer<'info>, pub token_program: Program<'info, Token>, pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>, } ``` **Step 3: Transfer Tokens Between Accounts** ```rust use anchor_spl::token::{self, TokenAccount, Token, Transfer}; #[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>, } #[program] pub mod token_transfer { use super::*; pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> { token::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, )?; Ok(()) } } ``` ### Client-Side Token Operations ```typescript import { getAssociatedTokenAddress } from '@solana/spl-token'; // Find token account addresses (deterministic based on owner + mint) const aliceTokenAccount = await getAssociatedTokenAddress( mintPublicKey, alice.publicKey ); const bobTokenAccount = await getAssociatedTokenAddress( mintPublicKey, bob.publicKey ); // Transfer tokens await program.methods .transferTokens(new anchor.BN(100)) .accounts({ from: aliceTokenAccount, to: bobTokenAccount, authority: alice.publicKey, tokenProgram: TOKEN_PROGRAM_ID, }) .signers([alice]) .rpc(); ``` ## Program Derived Addresses: Solana's CREATE2 PDAs are deterministic addresses that programs can control without needing private keys. They're essential for creating program-controlled accounts: ```rust #[derive(Accounts)] pub struct InitializeVault<'info> { #[account( init, payer = authority, space = 8 + 32 + 8, seeds = [b"vault", authority.key().as_ref()], bump )] pub vault: Account<'info, Vault>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, } #[account] pub struct Vault { pub authority: Pubkey, pub amount: u64, } ``` The client can find this PDA deterministically: ```typescript const [vaultPDA, bump] = await PublicKey.findProgramAddress( [Buffer.from("vault"), wallet.publicKey.toBuffer()], program.programId ); // This address is deterministic and controlled by the program console.log("Vault PDA:", vaultPDA.toString()); ``` ## Cross-Program Invocations: Composability Programs can call other programs through Cross-Program Invocations (CPIs): ```rust use anchor_spl::token::{self, Token, TokenAccount, Transfer}; #[program] pub mod token_wrapper { use super::*; pub fn wrapped_transfer(ctx: Context<WrappedTransfer>, amount: u64) -> Result<()> { // Call the SPL Token program to transfer tokens token::transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ), amount, )?; msg!("Transferred {} tokens", amount); Ok(()) } } #[derive(Accounts)] pub struct WrappedTransfer<'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>, } ``` ## Building a Complete Example: Simple Counter Let's build a simple counter to demonstrate these concepts: ```rust use anchor_lang::prelude::*; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] pub mod simple_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(()) } pub fn decrement(ctx: Context<Decrement>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count -= 1; Ok(()) } pub fn reset(ctx: Context<Reset>) -> Result<()> { let counter = &mut ctx.accounts.counter; counter.count = 0; Ok(()) } } #[derive(Accounts)] pub struct Initialize<'info> { #[account( init, payer = authority, space = 8 + 8 + 32, // discriminator + u64 + pubkey seeds = [b"counter", authority.key().as_ref()], bump )] pub counter: Account<'info, Counter>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Increment<'info> { #[account( mut, seeds = [b"counter", authority.key().as_ref()], bump, has_one = authority )] pub counter: Account<'info, Counter>, pub authority: Signer<'info>, } #[derive(Accounts)] pub struct Decrement<'info> { #[account( mut, seeds = [b"counter", authority.key().as_ref()], bump, has_one = authority )] pub counter: Account<'info, Counter>, pub authority: Signer<'info>, } #[derive(Accounts)] pub struct Reset<'info> { #[account( mut, seeds = [b"counter", authority.key().as_ref()], bump, has_one = authority )] pub counter: Account<'info, Counter>, pub authority: Signer<'info>, } #[account] pub struct Counter { pub count: u64, pub authority: Pubkey, } ``` Client-side usage: ```typescript import * as anchor from "@project-serum/anchor"; import { Program } from "@project-serum/anchor"; import { SimpleCounter } from "../target/types/simple_counter"; const program = anchor.workspace.SimpleCounter as Program<SimpleCounter>; const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); // Find counter PDA const [counterPDA] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from("counter"), provider.wallet.publicKey.toBuffer()], program.programId ); // Initialize counter await program.methods .initialize() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); // Increment counter await program.methods .increment() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, }) .rpc(); // Read counter value const counterAccount = await program.account.counter.fetch(counterPDA); console.log("Counter value:", counterAccount.count.toString()); // Decrement counter await program.methods .decrement() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, }) .rpc(); // Reset counter await program.methods .reset() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, }) .rpc(); ``` ## Development Tools and Testing ### Setting up Anchor ```bash # Install Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Install Solana CLI sh -c "$(curl -sSfL https://release.solana.com/v1.16.0/install)" # Install Anchor cargo install --git https://github.com/coral-xyz/anchor avm --locked --force avm install latest avm use latest # Create new project anchor init my-solana-project cd my-solana-project # Build and test anchor build anchor test ``` ### Testing Patterns ```typescript import * as anchor from "@project-serum/anchor"; import { expect } from "chai"; describe("simple-counter", () => { const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace.SimpleCounter; let counterPDA: anchor.web3.PublicKey; before(async () => { [counterPDA] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from("counter"), provider.wallet.publicKey.toBuffer()], program.programId ); }); it("Initializes the counter", async () => { await program.methods .initialize() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); const counterAccount = await program.account.counter.fetch(counterPDA); expect(counterAccount.count.toNumber()).to.equal(0); expect(counterAccount.authority.toString()).to.equal( provider.wallet.publicKey.toString() ); }); it("Increments the counter", async () => { await program.methods .increment() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, }) .rpc(); const counterAccount = await program.account.counter.fetch(counterPDA); expect(counterAccount.count.toNumber()).to.equal(1); }); it("Decrements the counter", async () => { await program.methods .decrement() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, }) .rpc(); const counterAccount = await program.account.counter.fetch(counterPDA); expect(counterAccount.count.toNumber()).to.equal(0); }); it("Resets the counter", async () => { // First increment to have something to reset await program.methods .increment() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, }) .rpc(); await program.methods .reset() .accounts({ counter: counterPDA, authority: provider.wallet.publicKey, }) .rpc(); const counterAccount = await program.account.counter.fetch(counterPDA); expect(counterAccount.count.toNumber()).to.equal(0); }); }); ``` ## Quick Reference: Key Differences Summary | Aspect | Ethereum | Solana | |--------|----------|---------| | **Development Framework** | Hardhat, Foundry | Anchor | | **Language** | Solidity, Vyper | Rust (via Anchor) | | **Virtual Machine** | EVM | SVM | | **Account Types** | EOAs + Contracts | Unified account model | | **State Storage** | Contract storage slots | Separate account data | | **Execution** | Sequential EVM | Parallel Sealevel | | **Gas Model** | Gas per operation | Rent + transaction fees | | **Composability** | Contract calls | Cross-Program Invocations | | **Token Standard** | ERC-20/721/1155 | SPL Token/NFT | | **Block Times** | ~12 second blocks | ~400ms slots | The fundamental shift from Ethereum to Solana requires rethinking how you structure applications around the unified account model. Once you understand that everything is an account with different properties, the rest of Solana's architecture becomes much clearer. Your wallet works just like an Ethereum EOA for signing transactions, but the underlying system treats it as just another account in a unified model. Start with simple programs like this counter, then gradually work up to more complex applications. The patterns shown here form the foundation for most Solana applications.