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
Normal Accounts ⇒ Store crypto
Program Account ⇒ will store data(code) with crypto
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
Native Program ⇒ Like System Program, BPF Loader ⇒ Created by Solana Engineers and this comes bundled along with Solana Runtime
Dev Created Program ⇒ Like Programs created by normal developers - common type
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”
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,}
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
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
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:
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
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
Anchor auto-generates InitializeBumps containing: vault, vault_state. These are guaranteed to match the PDAs validated earlier
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
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