In web2 world let us say we are building an todo app then we will host the code in an server and perform the CRUD operations reflections in Database
But in case of Solana we deploy the smart contract (backend) in an Solana account and when the new User comes we create a new account(auth) for that user from the main account(where the smart contract is deployed) and if the user create a new Todo we create an another new account and store the Todo in that account and all these accounts are linked to main account via PDA’s
But why like this ??
The answer recites the concept of Rent in Solana, which is discussed here in this section. Like when the user create their accounts or Todos they will pay the Solana that needed to be paid to maintain their account rather the contract paying it

Program Derived Address (PDA)

There are something called as Program derived address make the address of the account deterministic in blockchain, means generateAssociatedTokenAddress(("Unique Identifier" + user Address), Token Mint Address) = Associated Token Address (ATA) having the account (This formula is wrong discussed correct one down). The unique identifier is used to identify different Associated Token Address like one Todo can be stored in one ATA, the auth details might stored in one ATA thus we use Unique identifier
example: By taking same Todo App as Example, If the PDA’s doesn’t exist we need to store the addresses of where the Todos of an user are present, but as PDA’s exist we can deterministically find the Todos corresponding to an account deterministically Note:

  • ATA Don't have a private key they only have a public key, the data manipulation is done by the main account from which the ATA has derived. Thus we use a bump while generating ATA account such that it won’t have an corresponding private key
  • Associated Token Program is made by solana engineers which is responsible for mapping the user’s wallet address to the associated token accounts they hold
  • Bump seed we give an random seed of max of 255 which make sure that our generated ATA address doesn’t have an corresponding private key (means it should be off the curve of Ed25519), if it has in first iteration we will decrement to 254 and check if the generated key doesn’t have the corresponding private key and so on
  • Learn more about Elliptical key cryptography here cloudflare

Now for example to get the address of an ATA having username we need to pass Token Program Id, Associated Token Program Id, Mint Address, Bump seed, {"unique Identifier" + user Address} in an function which returns us Associated Token Account 1 Address (Assuming If we have given unique identifier as “username”(not exactly)).

Owners in Solana

Solana is nothing but bunch of accounts (HashMap - key-value like structure) on blockchain
Each account can store up to 10MB data
There are three types of accounts on Solana

  1. Normal Accounts Store crypto
  2. Program Account will store data(code) with crypto
  3. Data Account The account which stores the data from Program Account

Every account in solana has an owner, only that owner has the authority to modify the data or deduct lamports.
But anyone can increase lamports even it’s not owner
Native Programs Run time programs which runs while running solana validator but not this is not deployed

Programs on Solana

There are different kinds of program on the Solana blockchain

  1. Native Program Like System Program, BPF Loader Created by Solana Engineers and this comes bundled along with Solana Runtime
  2. Dev Created Program Like Programs created by normal developers - common type
  3. Programs Developed by Solana Engineers These are not Native but created by Solana Engineers, these are separately deployed on the Solana Blockchain

System Program

By default all the accounts are owned by system Program, The system program perform various tasks like Account Creation, Space Allocation and Account authority Transfer
Note: If your account don’t have any crypto then the account won’t exist on Block chain
Thus when the first transaction takes place the System Program will create an Existence (Account in case of Solana) on the blockchain
When you’re sending money from account A to B it inertly calls system program which debit from A and credit to B

BPF Loader Program

It Converts the Rust code in a format such that i get deployed on Blockchain. BPF Loader is the owner of all the Programs on the Blockchain except the Native Programs (System Program). It is Responsible for deploying, upgrading and executing custom programs

Q Then the System Program have access to our accounts and BPF Loader has access of our Programs. Then how it’s decentralized ?????
A It all depends on the minor, minors will be running some specific version (say 1.1) of solana run time which haves system program, now let us say new version (say 1.2) comes out having an shady code, the minors will not accept that runtime, they will go against it and run only 1.1 error free code, as if you think solana people and minors mix hand doing shady things it’s wrong as minors earn more if there are more transactions and there will be more transactions only if there will be trust in the network.
Now coming to BPF Loader it’s good that there will be an centralized authority to manage our data as, data is public in blockchain, and the ATA’s don’t have private key and not linked to money transactions, data will be encrypted, only the Main account and you know who you are

Compute Units

Compute Units (CU) are Solana’s way of measuring how much CPU work a transaction consumes
Think of CU as: “How much execution time the validator is allowed to spend (very roughly)”
Every on-chain action: Instruction execution, CPI calls, BPF ops, Account deserialization and Signature checks (indirectly) …consumes compute units

  • Every instruction costs CU
  • More logic = more CU
  • Loops, CPIs, hashing, serialization → expensive

CU is about execution cost, not memory or storage

Each Solana block has a hard cap on total compute
Why?

  • Prevents validators from being overloaded
  • Keeps block times predictable
  • Ensures fairness

So even if:

  • Individual transactions are cheap
  • Too many are submitted

The block will only include transactions until: Compute budget is exhausted and the Remaining transactions will wait for the next block

By default:

  • Every transaction gets a small fixed CU limit
  • Enough for simple transfers and basic instructions

If the transaction does heavy CPI or verification logic or worse DeFi math. It may fail with ComputationalBudgetExceeded error

For costly operations we can use Compute Budget Instructions which is a transaction can explicitly request A higher compute unit limit which is upto ~1.4 million CU. we must ask before execution, not dynamically

On Solana, Requesting more CU does not automatically raise fees. Fees are mostly Signature-based and Priority-fee based (optional). So High compute ≠ high fee (by default) But… Priority fees are possible
We can Attach a priority fee, Pay extra lamports per compute unit and this tells validators: “Include me first if block space is tight”

During congestion:

  • Validators prioritize higher-fee-per-CU transactions
  • Not just higher total fee

This creates:

  • A local fee market
  • Without global gas price spikes

Q How runtime actually enforces this ?
A

  • At execution time:
    1. Transaction starts with CU budget
    2. Each instruction burns CU
    3. CPI burns from the same budget
    4. If CU hits zero → immediate failure
    5. Entire transaction rolls back
  • CU is Deterministic, Metered at instruction boundaries and Enforced by the BPF runtime

IDL(Interface Design Language)

On Solana, an IDL is “A machine-readable description of a program’s public interface” - Like a README of the project, nothing more
It describes:

  • What instructions the program exposes
  • What accounts each instruction expects
  • What data types are used
  • What errors can occur

IDL is NOT used by the runtime. It’s is purely a developer & tooling abstraction
The Solana runtime does not care about IDLs

Q Why IDL exists at all ?
A

  • Solana programs:
    • Take raw bytes as input
    • Receive accounts as an indexed array
    • Have no built-in ABI like Ethereum
  • Without IDL:
    • Clients must manually serialize instruction data
    • Manually order accounts
    • Manually decode responses
    • Easy to make fatal mistakes
  • IDL exists to, Bridge low-level Solana programs with high-level client code

Q What an IDL typically contains ?
A A real IDL describes:

  • Instructions: Name, Arguments (types), Required accounts and Signer / writable flags
  • Accounts: Account layouts, Field types and Ownership expectations
  • Types: Structs, Enums and Nested data
  • Errors: Error codes and Human-readable messages

All written in JSON

Q what do we mean by “Public IDLs can be uploaded on-chain” ?
A This is about discoverability, not execution

  • Some programs:
    • Store their IDL in a PDA on-chain
    • So clients can fetch it trustlessly
  • Benefits:
    • Wallets can auto-build transactions
    • Explorers can decode instructions
    • SDKs can auto-generate bindings
  • Uploading IDL on-chain is optional and not enforced

Creating own token on solana

For context, Tokens are discussed here 1. Intro
we can create our own token in solana using solana-cli

spl-token create-token --decimal 9 

Note: we can or cannot specify the decimal flag it by default take decimal nine. But what is this decimal?
decimal is the amount of units that the token can breakdown into like decimal 9 represent token can breakdown into 10^9 lamports
Note: —decimal 0 means it’s an NFT as NFT’s can’t breakdown into parts

  • To check the supply of the token created use command
spl-token supply <mint address of the token>
  • To create an Associated token account use
spl-token create-account <mint address of the token>
  • To get the token in associated token account use
spl-token mint <mint address of the token you want> <amount of token you want> 
# we wont specify the where (our account address) this should reach as it will take automatically from using `solana address` command from terminal
  • You can check the tokens by going to explorer.solana.com or by importing your local keys in phantom in devnet
  • Now you can sent this token to others also via web wallet
  • But wait others haven’t created the Associated Token Account na then how can they get the new token the fact is the ATA will be created for them whom you’re sending by you with the cost of some new tokens deduction from your account in the form of gas fee (And this is only one time fee while creating ATA and might also be charged later as a gas fee for transaction but it will be less as we don’t need to create ATA)

NFT(Non-fungable Token)

Metaplex: Metaplex on Solana is an open-source protocol and toolset for creating, managing, and trading Non-Fungible Tokens (NFTs) and other digital assets, powering most of the Solana NFT ecosystem. The goal was to create an open NFT protocol and tools to support it

  • The Metaplex Metadata Standard
  • CandyMachine
  • Compressed NFTs

Metaplex Token Standard

  • NonFungible: A non-fungible token with a Master Edition
  • FungibleAsset: A token with metadata that can also have attributes, sometimes called Semi-Fungible
  • Fungible: A token with simple metadata.
  • Non Fungible Edition: A non-fungible token with an Edition account (printed from a Master edition)
  • Programmable Non Fungible: A special Non Fungible token that is frozen at all times to enforce custom authorization rules

The Token Standard is set automatically by the Token Metadata program

  • If the token has a Master Edition account, it is a NonFungible
  • If the token has an Edition account, it is a NonFungibleEdition
  • If the token has no (Master) Edition account ensuring its supply can be > 1 and uses zero decimals places, it is a FungibleAsset
  • If the token has no (Master) Edition account ensuring its supply can be > 1 and uses at least one decimal place, it is a Fungible

Most of the NFTs are Non-Fungable, it means they’ve only master addition account and no other account associated with it, this means the no other authority can mint the NFTs apart from master edition account and this guarantees there will be only one NFT and no duplicate of it

Q Can’t master edition account can mint more NFTs as it own it ? then how there can be only one NFT ?
A The Master edition account will be owned by a PDA and this PDA is owned my metaplex contract, same logic as system program which owns the account

Q What if we want to update some metadata of the NFT ?
A In Metaplex, the authority that can update an asset’s metadata (like name, symbol, URI) is called the Update Authority, which is a designated wallet key set during metadata creation, requiring its signature for any changes, and can be different from the Mint Authority for the token itself

  • If the Is Mutable attribute is set to false, the metadata cannot be changed, even by the update authority
  • When creating the metadata (using instructions like CreateMetadataAccountV3), the update authority is specified; this could be the token’s Mint Authority or another address
  • In collections, the authority of the collection’s tree often acts as the update authority for its NFTs, or a specific collection authority can be designated
  • To update metadata, the designated update authority must sign the transaction

Q What if hacker stole NFT from us ?
A We can use update authority to increase the royality fees of the NFT to 95%, that make un economical for the hacker to own that NFT

Depending on the type of Token you are creating there are different JSON standard that should be used. see metaplex docs for reference

Q For a user to use an NFT that he not own, should be signed by the original owner and what is the NFT is famous, does the owner sign for every user that is using the NFT ?
A If the NFT is famous he use platform like Candy Machine, where we can host NFT and let the users use it by paying us money. And here we add candy machine as an owner to the NFT but it will receive zero royalty, this mens candy machine is one of the owner of the NFT that can authorize to sign the NFT but receives zero royalty

Anchor Vault

use anchor_lang::{
    prelude::*,
    system_program::{transfer, Transfer},
};
 
declare_id!("7mnQzyZeT39CG8t4megsqywN7z44ct58MBzznqL5YQ5z");
 
#[program]
pub mod anchor_vault_q4_25 {
    use super::*;
 
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.initialize(&ctx.bumps)
    }
 
    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        ctx.accounts.deposit(amount)
    }
 
    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        ctx.accounts.withdraw(amount)
    }
 
    pub fn close(ctx: Context<Close>) -> Result<()> {
        ctx.accounts.close()
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        init,
        payer = user,
        seeds = [b"state", user.key().as_ref()], 
        bump,
        space = VaultState::DISCRIMINATOR.len() + VaultState::INIT_SPACE,
    )]
    pub vault_state: Account<'info, VaultState>,
    #[account(
        mut,
        seeds = [b"vault", vault_state.key().as_ref()],
        bump,
    )]
    pub vault: SystemAccount<'info>,
    pub system_program: Program<'info, System>,
}
 
impl<'info> Initialize<'info> {
    pub fn initialize(&mut self, bumps: &InitializeBumps) -> Result<()> {
        // Get the amount of lamports needed to make the vault rent exempt
        let rent_exempt = Rent::get()?.minimum_balance(self.vault.to_account_info().data_len());
 
        // Transfer the rent-exempt amount from the user to the vault
        let cpi_program = self.system_program.to_account_info();
        let cpi_accounts = Transfer {
            from: self.user.to_account_info(),
            to: self.vault.to_account_info(),
        };
 
        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
 
        transfer(cpi_ctx, rent_exempt)?;
 
        self.vault_state.vault_bump = bumps.vault;
        self.vault_state.state_bump = bumps.vault_state;
 
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        mut,
        seeds = [b"vault", vault_state.key().as_ref()], 
        bump = vault_state.vault_bump,
    )]
    pub vault: SystemAccount<'info>,
    #[account(
        seeds = [b"state", user.key().as_ref()],
        bump = vault_state.state_bump,
    )]
    pub vault_state: Account<'info, VaultState>,
    pub system_program: Program<'info, System>,
}
 
impl<'info> Deposit<'info> {
    pub fn deposit(&mut self, amount: u64) -> Result<()> {
        let cpi_program = self.system_program.to_account_info();
 
        let cpi_accounts = Transfer {
            from: self.user.to_account_info(),
            to: self.vault.to_account_info(),
        };
 
        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
 
        transfer(cpi_ctx, amount)?;
 
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        mut,
        seeds = [b"vault", vault_state.key().as_ref()],
        bump = vault_state.vault_bump,
    )]
    pub vault: SystemAccount<'info>,
    #[account(
        seeds = [b"state", user.key().as_ref()],
        bump = vault_state.state_bump,
    )]
    pub vault_state: Account<'info, VaultState>,
    pub system_program: Program<'info, System>,
}
 
impl<'info> Withdraw<'info> {
    pub fn withdraw(&mut self, amount: u64) -> Result<()> {
        let cpi_program = self.system_program.to_account_info();
 
        let cpi_accounts = Transfer {
            from: self.vault.to_account_info(),
            to: self.user.to_account_info(),
        };
 
        let seeds = &[
            b"vault",
            self.vault_state.to_account_info().key.as_ref(),
            &[self.vault_state.vault_bump],
        ];
        let signer_seeds = &[&seeds[..]];
 
        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
 
        transfer(cpi_ctx, amount)?;
 
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Close<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        mut,
        seeds = [b"vault", vault_state.key().as_ref()],
        bump = vault_state.vault_bump,
    )]
    pub vault: SystemAccount<'info>,
    #[account(
        mut,
        seeds = [b"state", user.key().as_ref()],
        bump = vault_state.state_bump,
        close = user,
    )]
    pub vault_state: Account<'info, VaultState>,
    pub system_program: Program<'info, System>,
}
 
impl<'info> Close<'info> {
    pub fn close(&mut self) -> Result<()> {
        let cpi_program = self.system_program.to_account_info();
 
        let cpi_accounts = Transfer {
            from: self.vault.to_account_info(),
            to: self.user.to_account_info(),
        };
 
        let seeds = &[
            b"vault",
            self.vault_state.to_account_info().key.as_ref(),
            &[self.vault_state.vault_bump],
        ];
        let signer_seeds = &[&seeds[..]];
 
        let balance = self.vault.lamports();
 
        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
 
        transfer(cpi_ctx, balance)?;
 
        Ok(())
    }
}
 
#[derive(InitSpace)]
#[account]
pub struct VaultState {
    pub vault_bump: u8,
    pub state_bump: u8,
}
  1. Imports and Program Identity
    use anchor_lang::{
        prelude::*,
        system_program::{transfer, Transfer},
    };
    • This pulls in Anchor’s core abstractions and explicitly imports the System Program transfer instruction
    • Anchor intentionally requires explicit imports for CPI instructions to make cross-program calls auditable and type-safe
    • The Transfer struct defines the account layout required by the System Program’s transfer instruction, enforcing correctness at compile time rather than runtime
    declare_id!("7mnQzyZeT39CG8t4megsqywN7z44ct58MBzznqL5YQ5z");
    • This hard-binds the program binary to a single program ID
    • All PDAs derived inside this program are implicitly tied to this ID. Changing this value invalidates every PDA derived by the program, which is why program upgrades must preserve the same program ID unless a full migration is intended
  2. Program Entry Points
    #[program]
    pub mod anchor_vault_q4_25 {
    • This macro generates the Solana instruction dispatcher
    • Each public function inside becomes an on-chain instruction with a fixed ABI. Anchor generates:
      • instruction discriminators
      • account deserialization logic
      • constraint validation
      • automatic error bubbling
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.initialize(&ctx.bumps)
    }
    • The instruction body delegates logic to the account struct itself.
    • This pattern centralizes business logic next to account constraints, reducing the chance of logic drifting away from security assumptions enforced by #[derive(Accounts)]
    • The ctx.bumps value is injected by Anchor after PDA derivation. It is deterministic, verified on-chain, and avoids recomputation or mismatches
    • The same delegation pattern is used for deposit, withdraw, and close
  3. Initialize Account Context
    #[derive(Accounts)]
    pub struct Initialize<'info> {
    • This struct defines all accounts required to initialize the vault system
    • Anchor validates every constraint before instruction execution
    #[account(mut)]
    pub user: Signer<'info>,
    • Must sign the transaction
    • Pays rent
    • Acts as the authority implicitly through PDA derivation
    • Mutability is required because lamports will be debited
  4. VaultState PDA
    #[account(
        init,
        payer = user,
        seeds = [b"state", user.key().as_ref()],
        bump,
        space = VaultState::DISCRIMINATOR.len() + VaultState::INIT_SPACE,
    )]
    pub vault_state: Account<'info, VaultState>,
    • This creates a program-owned PDA that stores metadata
      • init ensures the account does not already exist
      • payer = user enforces who funds account creation
      • seeds tie this PDA uniquely to the user
      • bump ensures off-curve derivation
      • space includes:
        • 8-byte Anchor discriminator
        • Struct size calculated via InitSpace
    • This account becomes the source of truth for bump values and vault linkage
  5. Vault PDA (SOL Holder)
    #[account(
        mut,
        seeds = [b"vault", vault_state.key().as_ref()],
        bump,
    )]
    pub vault: SystemAccount<'info>,
    • This is a PDA owned by the System Program, not by this program
    • Implications:
      • Cannot store custom data
      • Holds lamports only
      • Must use CPI + signer seeds for transfers
      • Security is enforced purely via deterministic address derivation
    • Using vault_state.key() as a seed chains ownership: user → vault_state → vault. This prevents vault reuse or collisions
    pub system_program: Program<'info, System>,
    • Explicit dependency injection of the System Program
    • Anchor prevents spoofing by validating the program ID matches the canonical System Program
  6. Initialize Logic
    pub fn initialize(&mut self, bumps: &InitializeBumps) -> Result<()> {
    • Anchor auto-generates InitializeBumps containing: vault, vault_state. These are guaranteed to match the PDAs validated earlier
  7. Rent Calculation
    let rent_exempt =
        Rent::get()?.minimum_balance(self.vault.to_account_info().data_len());
    • Even though the vault holds no data, rent-exemption is still required to prevent reclamation. This ensures the vault PDA remains permanently allocated unless explicitly drained
  8. CPI Transfer (User → Vault)
    let cpi_accounts = Transfer {
        from: self.user.to_account_info(),
        to: self.vault.to_account_info(),
    };
    • This constructs the exact account layout required by the System Program
    • Anchor enforces ordering, mutability, and ownership correctness
    transfer(cpi_ctx, rent_exempt)?;
    • Execution occurs inside the System Program, not this program. This separation prevents arbitrary lamport mutation
  9. Persisting Bumps
    self.vault_state.vault_bump = bumps.vault;
    self.vault_state.state_bump = bumps.vault_state;
    • Storing bumps on-chain ensures:
      • future instructions do not rely on recomputation
      • signer seeds remain stable
      • upgrades remain deterministic
    • This is a defensive design against seed drift and future refactors
  10. Deposit Context
    #[derive(Accounts)]
    pub struct Deposit<'info> {
    This instruction only moves SOL into the vault
    #[account(mut)]
    pub user: Signer<'info>,
    The signer must own the source funds
    #[account(
        mut,
        seeds = [b"vault", vault_state.key().as_ref()],
        bump = vault_state.vault_bump,
    )]
    pub vault: SystemAccount<'info>,
    • Explicit bump verification ensures:
      • the passed vault matches the stored PDA
      • no alternative vault can be substituted
    #[account(
        seeds = [b"state", user.key().as_ref()],
        bump = vault_state.state_bump,
    )]
    pub vault_state: Account<'info, VaultState>,
    • This ties deposits strictly to the user’s own vault
    • Cross-user deposits are impossible without modifying seeds
  11. Deposit Logic
    transfer(cpi_ctx, amount)?;
    • No signer seeds are needed because:
      • the user signs
      • the user is the source account
    • The program never assumes authority it does not possess.
  12. Withdraw Context
    • Withdraw mirrors deposit structurally but reverses authority
    from: self.vault.to_account_info(),
    to: self.user.to_account_info(),
    Here, the vault must sign, but it cannot
  13. Signer Seeds
    let seeds = &[
        b"vault",
        self.vault_state.to_account_info().key.as_ref(),
        &[self.vault_state.vault_bump],
    ];
    These seeds recreate the vault PDA exactly. The runtime verifies that:
    • the derived PDA matches vault
    • the bump matches
    • the calling program owns the derivation
    CpiContext::new_with_signer(...)
    This authorizes the program to act as the PDA for this CPI only
  14. Close Context
    #[account(
        mut,
        seeds = [b"state", user.key().as_ref()],
        bump = vault_state.state_bump,
        close = user,
    )]
    pub vault_state: Account<'info, VaultState>,
    • This triggers automatic lamport transfer, account deallocation and storage cleanup
    • Anchor ensures this happens after instruction execution succeeds
  15. Vault Drain
    let balance = self.vault.lamports();
    transfer(cpi_ctx, balance)?;
    System accounts are not auto-closable.
    Draining lamports manually ensures:
    • no stranded SOL
    • vault PDA becomes economically inert
    • no rent leak
  16. VaultState Layout
    #[derive(InitSpace)]
    #[account]
    pub struct VaultState {
        pub vault_bump: u8,
        pub state_bump: u8,
    }
    • Minimal state:
      • keeps PDA derivation deterministic
      • avoids recomputation
      • avoids passing bumps via instruction data
      • supports future extensions without breaking layout
    • The InitSpace macro computes exact byte size at compile time, preventing under-allocation bugs