Solana Native CRUD Program


Simple CRUD (Create, Read, Update, Delete) program written in native Rust. This program manage a PDA where users can create, update, and delete.
Program Overview
The program can :
Create a message account with associated text
Update existing messages
Delete message accounts
Each message account is tied to a specific user's public key and stores the message content.
Project Structure
src/
├── lib.rs # program entry point and ID declaration
├── processor.rs # instruction processing logic
├── state/ # account state
│ ├── mod.rs
│ └── message.rs # MessageAccount struct
└── instructions/ # instructions
├── mod.rs
├── create.rs # create message logic
├── update.rs # update message logic
└── delete.rs # delete message logic
Core Components
MessageAccount Structure
The MessageAccount
struct represents the data stored on-chain for each message:
pub struct MessageAccount {
pub user: Pubkey, // owner of the message
pub message: String, // the message text
pub bump: u8, // bump seed for PDA derivation
}
Program Derived Addresses (PDAs)
The program uses PDAs to create for message accounts. Each account is derived using:
Seed prefix: "message"
User's public key
Bump seed (for canonical derivation)
This ensures each user can have exactly one message account with a predictable address. Read More: 1 2
Instruction Types
The program handles three instruction types defined in the Instructions
enum:
Create(MessageAccount) - Creates a new message account
Update(MessageAccount) - Updates an existing message
Delete - Removes a message account
Instruction Processing
Create Operation
The create instruction handles the initial creation of a message account with the following logic:
pub fn create(
_program_id: &Pubkey,
accounts: &[AccountInfo],
message: MessageAccount,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let message_account = next_account_info(accounts_iter)?;
let payer = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
// calculate the rent required to store account on chain
let account_span = (to_vec(&message)?).len();
let lamports_required = (Rent::get()?).minimum_balance(account_span);
// find the bump seed for the PDA
let (_, bump) = Pubkey::find_program_address(
&[MessageAccount::SEED_PREFIX.as_bytes(), payer.key.as_ref()],
&crate::ID,
);
// create the account using CPI to System Program
invoke_signed(
&instruction::create_account(
payer.key,
message_account.key,
lamports_required,
account_span as u64,
&crate::ID,
),
&[payer.clone(), message_account.clone(), system_program.clone()],
&[&[
MessageAccount::SEED_PREFIX.as_bytes(),
payer.key.as_ref(),
&[bump],
]],
)?;
// serialize the message data into the account
message.serialize(&mut &mut message_account.data.borrow_mut()[..])?;
Ok(())
}
Steps:
Account Extraction: Gets the three required accounts - the message account to be created, the payer, and the system program
Size Calculation: Uses
to_vec()
to serialize the message data and determine the exact space neededRent Calculation: Determines minimum lamports needed for rent exemption
PDA Derivation: Finds the bump seed for the Program Derived Address
Account Creation: Uses
invoke_signed
to call the System Program's create_account instruction with the derived seedsData Storage: Serializes the MessageAccount struct directly into the account's data
Things to note down:
- Uses
invoke_signed
because we're creating an account with a PDA (requires signature derivation)
Update Operation
The update instruction modifies existing message content and handles account resizing:
pub fn update(_program_id: &Pubkey, accounts: &[AccountInfo], message: String) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let message_account = next_account_info(accounts_iter)?;
let payer = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
// deserialize existing account data
let mut message_data = MessageAccount::try_from_slice(&message_account.data.borrow())?;
message_data.message = message;
// calculate new account size requirements
let account_span = (to_vec(&message_data)?).len();
let lamports_required = (Rent::get()?).minimum_balance(account_span);
// add more lamports if the new message is larger
let diff = lamports_required - message_account.lamports();
let _ = &invoke(
&instruction::transfer(payer.key, message_account.key, diff),
&[payer.clone(), message_account.clone(), system_program.clone()],
);
// resize the account to fit new data
message_account.resize(account_span)?;
// serialize updated data back to account
message_data.serialize(&mut &mut message_account.data.borrow_mut()[..])?;
Ok(())
}
Steps:
Data Retrieval: Deserializes the existing MessageAccount from the account data
Message Update: Replaces the old message with the new one
Size Recalculation: Determines if the account needs to grow or shrink
Lamport Transfer: If the new message is larger, transfers additional lamports for rent
Account Resize: Changes the account's data size to match the new requirements
Data Storage: Serializes the updated MessageAccount back to the account
Things to note down:
Uses regular
invoke
(notinvoke_signed
) because we're not creating new accountsOnly transfers additional lamports when needed (if new message is larger)
Delete Operation
The delete instruction removes the message account and returns lamports to the payer:
pub fn delete(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let message_account = next_account_info(accounts_iter)?;
let payer = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_span = 0usize;
let lamports_required = (Rent::get()?).minimum_balance(account_span);
// calculate lamports to return to payer
let diff = message_account.lamports() - lamports_required;
// direct lamport manipulation for efficiency instead of transfer
**message_account.lamports.borrow_mut() -= diff;
**payer.lamports.borrow_mut() += diff;
// resize account to zero bytes
message_account.resize(account_span)?;
// transfer ownership back to System Program
message_account.assign(system_program.key);
Ok(())
}
Steps:
Zero Size Calculation: Sets target account size to 0 bytes
Lamport Calculation: Determines how many lamports to return to the payer
Direct Transfer: Manually adjusts lamport balances for both accounts
Account Resize: Shrinks the account data to zero bytes
Ownership Transfer: Assigns the account back to the System Program
Why direct lamport manipulation?
The code comment explains this design choice:
Performance: Cheaper in compute units than CPI transfers
Efficiency: No Cross-Program Invocation overhead
Authority: Since the account is owned by our program, we can directly modify its lamports
Cost: Avoids invoking the System Program for transfers
Security Implementation
Several security checks and validations to ensure safe operation:
Account Validation Checks
Program ID Verification
if program_id.ne(&crate::ID) {
return Err(ProgramError::IncorrectProgramId);
}
Every instruction validates that it's being called with the correct program ID to prevent unauthorized access.
PDA Validation The program enforces bump seeds for all PDAs to prevent account confusion attacks. Each message account is derived using:
let (_, bump) = Pubkey::find_program_address(
&[MessageAccount::SEED_PREFIX.as_bytes(), payer.key.as_ref()],
&crate::ID,
);
Account Ownership Verification Before any modification operation, the program verifies that accounts are owned by the correct program and have valid data structures through Borsh deserialization checks.
Development Notes
The Solana crates used by program:
solana-program
: Core Solana programming primitivesborsh
: Serialization frameworksolana-system-interface
: System Program interaction helpers
More Info
The Codebase + testcase are here
The collection solana programs + Good READMEs are here
If you like this blog give me a follow at twitter: arjun
Subscribe to my newsletter
Read articles from Arjun C directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Arjun C
Arjun C
i read and write often