Skip to content

mgild/solzempic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Solzempic Logo

Solzempic

A lightweight, zero-overhead framework for building Solana programs with Pinocchio.

Solzempic provides the structure and safety of a framework without the bloat. It implements the Action pattern (Build, Validate, Execute) and provides type-safe account wrappers while maintaining full control over compute unit usage.

Why Solzempic?

The Problem with Existing Frameworks

Anchor is excellent for getting started, but comes with significant overhead:

  • Magic discriminators and automatic deserialization add ~2000+ CUs per instruction
  • IDL generation bloats binary size
  • Implicit borsh serialization prevents zero-copy optimizations
  • Hard to reason about exactly what code is generated

Vanilla Pinocchio gives maximum control, but requires writing boilerplate:

  • Account validation logic repeated across instructions
  • No structured pattern for instruction processing
  • Easy to forget security checks

Solzempic's Approach

Solzempic provides just enough structure to eliminate boilerplate while maintaining zero overhead:

+------------------+---------------+------------------+
|     Anchor       |   Solzempic   | Vanilla Pinocchio|
+------------------+---------------+------------------+
| High abstraction | Right balance | No abstraction   |
| Hidden costs     | Explicit      | Explicit         |
| Magic macros     | Thin macros   | No macros        |
| ~5000 CU/instr   | ~100 CU/instr | ~100 CU/instr    |
+------------------+---------------+------------------+

Key Features

  • Zero-overhead abstractions: All wrappers compile to the same code you'd write by hand
  • Action pattern: Structured Build -> Validate -> Execute flow for every instruction
  • Type-safe account wrappers: AccountRef<T>, AccountRefMut<T> with ownership validation
  • Program-specific Framework trait: Configure your program ID once, use everywhere
  • Validated program accounts: SystemProgram, TokenProgram, Signer etc. with compile-time guarantees
  • Derive macros: #[instruction] and #[SolzempicEntrypoint] for ergonomic dispatch
  • no_std compatible: Works in constrained Solana runtime environment

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    SOLZEMPIC FRAMEWORK                       │
└─────────────────────────────────────────────────────────────┘
                             │
┌────────────────────────────┼────────────────────────────┐
│                            │                            │
▼                            ▼                            ▼
┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│  Framework   │      │    Action    │      │   Account    │
│    Trait     │      │   Pattern    │      │   Wrappers   │
└──────────────┘      └──────────────┘      └──────────────┘
      │                      │                      │
      │              ┌───────┴───────┐              │
      ▼              ▼               ▼              ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ define_      │ │    build()   │ │  validate()  │ │  AccountRef  │
│ framework!   │ │              │ │              │ │ AccountRefMut│
│              │ │ Accounts +   │ │ Invariants   │ │ ShardRef-    │
│ Creates:     │ │ params from  │ │ PDA checks   │ │ Context      │
│ - Solzempic  │ │ raw bytes    │ │ Ownership    │ └──────────────┘
│ - AccountRef │ └──────────────┘ └──────────────┘        │
│ - AccountRef │        │               │                 │
│   Mut        │        └───────┬───────┘                 │
└──────────────┘                │                         │
      │                         ▼                         │
      │                  ┌──────────────┐                 │
      │                  │  execute()   │◄────────────────┘
      │                  │              │
      └─────────────────►│ State changes│
                         │ CPI calls    │
                         │ Token xfers  │
                         └──────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                         PROGRAM WRAPPERS                            │
├─────────────────┬─────────────────┬─────────────────┬──────────────┤
│  SystemProgram  │   TokenProgram  │    Signer       │   Sysvars    │
│  AtaProgram     │   Mint          │    Payer        │   Clock      │
│  AltProgram     │   TokenAccount  │                 │   Rent       │
│  Lut            │                 │                 │   SlotHashes │
└─────────────────┴─────────────────┴─────────────────┴──────────────┘

Installation

Add to your Cargo.toml:

[dependencies]
solzempic = { version = "0.1" }
pinocchio = { version = "0.7" }
bytemuck = { version = "1.14", features = ["derive"] }

Quick Start

1. Define Your Program Entry Point

In your program's lib.rs, use the #[SolzempicEntrypoint] attribute:

#![no_std]

use solzempic::SolzempicEntrypoint;

#[SolzempicEntrypoint("YourProgramId111111111111111111111111111")]
pub enum MyInstruction {
    Initialize = 0,
    Increment = 1,
}

This single attribute generates:

  • ID: Pubkey constant and id() -> &'static Pubkey function
  • AccountRef<'a, T>, AccountRefMut<'a, T>, ShardRefContext<'a, T>, ShardRefMutContext<'a, T> type aliases
  • #[repr(u8)] on the enum
  • TryFrom<u8> and dispatch methods
  • The program entrypoint

2. Define Account Types

#[solzempic::account(discriminator = 1)]
pub struct Counter {
    pub discriminator: [u8; 8],
    pub owner: Pubkey,
    pub count: u64,
}

The #[account(discriminator = N)] macro automatically:

  • Adds #[repr(C)], Pod, Zeroable derives
  • Implements Loadable and Account traits
  • Generates LEN, DISCRIMINATOR, and discriminator() method

3. Implement an Instruction

Use the #[instruction] attribute on an impl block:

use solzempic::{instruction, Signer, ValidatedAccount};
use pinocchio::{AccountView, program_error::ProgramError, ProgramResult};
use solana_address::Address;

#[repr(C)]
#[derive(Clone, Copy)]
pub struct IncrementParams {
    pub amount: u64,
}

pub struct Increment<'a> {
    pub counter: AccountRefMut<'a, Counter>,
    pub owner: Signer<'a>,
}

#[instruction(IncrementParams)]
impl<'a> Increment<'a> {
    fn build(accounts: &'a [AccountView], _params: &IncrementParams) -> Result<Self, ProgramError> {
        if accounts.len() < 2 {
            return Err(ProgramError::NotEnoughAccountKeys);
        }
        Ok(Self {
            counter: AccountRefMut::load(&accounts[0])?,
            owner: Signer::wrap(&accounts[1])?,
        })
    }

    fn validate(&self, _program_id: &Address, _params: &IncrementParams) -> ProgramResult {
        // Verify owner matches counter's owner
        if self.owner.key() != &self.counter.get().owner {
            return Err(ProgramError::IllegalOwner);
        }
        Ok(())
    }

    fn execute(&self, _program_id: &Address, params: &IncrementParams) -> ProgramResult {
        self.counter.get_mut().count += params.amount;
        Ok(())
    }
}

4. Process Instructions

The #[SolzempicEntrypoint] macro generates everything needed. Your process_instruction is simply:

fn process_instruction(
    program_id: &Address,
    accounts: &[AccountView],
    data: &[u8],
) -> ProgramResult {
    MyInstruction::process(program_id, accounts, data)
}

Core Concepts

The Action Pattern

Every instruction follows the same three-phase pattern:

┌─────────────────────────────────────────────────────────────────────────┐
│                         ACTION LIFECYCLE                                 │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
        ▼                           ▼                           ▼
┌───────────────────┐     ┌───────────────────┐     ┌───────────────────┐
│      BUILD        │     │     VALIDATE      │     │     EXECUTE       │
├───────────────────┤     ├───────────────────┤     ├───────────────────┤
│ • Deserialize     │     │ • Check invariants│     │ • Modify state    │
│   parameters      │     │ • Verify PDAs     │     │ • Transfer tokens │
│ • Load accounts   │ ──► │ • Check ownership │ ──► │ • Create accounts │
│ • Wrap programs   │     │ • Validate ranges │     │ • Emit events     │
│ • Early validation│     │ • Business rules  │     │ • CPI calls       │
└───────────────────┘     └───────────────────┘     └───────────────────┘
        │                           │                           │
        │         FAIL FAST         │      PURE CHECKS          │     SIDE EFFECTS
        │    (bad accounts = error) │   (no state changes)      │   (point of no return)
        ▼                           ▼                           ▼

Phase 1: Build

  • Extract parameters from instruction data (zero-copy via parse_params)
  • Load and validate account types (AccountRef::load, AccountRefMut::load)
  • Wrap program accounts (Signer::wrap, TokenProgram::wrap)
  • Fail fast on structural errors

Phase 2: Validate

  • Check business logic invariants
  • Verify PDA derivations if needed
  • Validate numerical ranges and relationships
  • No state mutations allowed

Phase 3: Execute

  • Perform all state changes
  • Execute token transfers
  • Make CPI calls
  • This is the "point of no return"

Account Wrappers

AccountRef<T> - Read-Only Access

pub struct AccountRef<'a, T: Loadable, F: Framework> {
    pub info: &'a AccountInfo,
    data: &'a [u8],
    // ...
}

impl<'a, T: Loadable, F: Framework> AccountRef<'a, T, F> {
    /// Load with full validation (ownership + discriminator)
    pub fn load(info: &'a AccountInfo) -> Result<Self, ProgramError>;

    /// Load without ownership check (for cross-program reads)
    pub fn load_unchecked(info: &'a AccountInfo) -> Result<Self, ProgramError>;

    /// Get typed reference to account data
    pub fn get(&self) -> &T;

    /// Check if account is a PDA with given seeds
    pub fn is_pda(&self, seeds: &[&[u8]]) -> (bool, u8);
}

AccountRefMut<T> - Read-Write Access

impl<'a, T: Loadable, F: Framework> AccountRefMut<'a, T, F> {
    /// Load with validation (ownership + discriminator + is_writable)
    pub fn load(info: &'a AccountInfo) -> Result<Self, ProgramError>;

    /// Get typed reference
    pub fn get(&self) -> &T;

    /// Get mutable typed reference
    pub fn get_mut(&mut self) -> &mut T;

    /// Reload after CPI (updates internal data pointer)
    pub fn reload(&mut self);
}

impl<'a, T: Initializable, F: Framework> AccountRefMut<'a, T, F> {
    /// Initialize a new account (writes discriminator, zeros rest)
    pub fn init(info: &'a AccountInfo) -> Result<Self, ProgramError>;

    /// Initialize if uninitialized, otherwise load
    pub fn init_if_needed(info: &'a AccountInfo) -> Result<Self, ProgramError>;

    /// Initialize a PDA account (create via CPI + initialize)
    pub fn init_pda(
        info: &'a AccountInfo,
        payer: &AccountInfo,
        system_program: &AccountInfo,
        seeds: &[&[u8]],
        space: usize,
    ) -> Result<Self, ProgramError>;
}

ShardRefContext<T> - Read-Only Triplet Navigation

For sharded data structures that need read-only access to prev/current/next:

pub struct ShardRefContext<'a, T: Loadable, F: Framework> {
    pub prev: AccountRef<'a, T, F>,
    pub current: AccountRef<'a, T, F>,
    pub next: AccountRef<'a, T, F>,
}

impl<'a, T: Loadable, F: Framework> ShardRefContext<'a, T, F> {
    pub fn new(prev: &'a AccountInfo, current: &'a AccountInfo, next: &'a AccountInfo) -> Result<Self, ProgramError>;
    pub fn current(&self) -> &T;
    pub fn prev(&self) -> &T;
    pub fn next(&self) -> &T;
}

ShardRefMutContext<T> - Mutable Triplet Navigation

For sharded data structures that need mutable access to prev/current/next:

pub struct ShardRefMutContext<'a, T: Loadable, F: Framework> {
    pub prev: AccountRefMut<'a, T, F>,
    pub current: AccountRefMut<'a, T, F>,
    pub next: AccountRefMut<'a, T, F>,
}

impl<'a, T: Loadable, F: Framework> ShardRefMutContext<'a, T, F> {
    pub fn new(prev: &'a AccountInfo, current: &'a AccountInfo, next: &'a AccountInfo) -> Result<Self, ProgramError>;
    pub fn from_loaded(prev: AccountRefMut, current: AccountRefMut, next: AccountRefMut) -> Self;
    pub fn current_mut(&mut self) -> &mut T;
    pub fn prev_mut(&mut self) -> &mut T;
    pub fn next_mut(&mut self) -> &mut T;
    pub fn all_mut(&mut self) -> (&mut T, &mut T, &mut T);
}

Common use cases for shard contexts:

  • Orderbooks: Orders may need to move between price-range shards
  • Linked lists: Insertions/deletions update prev/next pointers
  • Rebalancing: Moving data between under/over-utilized shards

PDA (Program Derived Address) Support

Solzempic provides comprehensive PDA support through the account wrappers:

Validating PDAs

Both AccountRef and AccountRefMut implement the is_pda() method via the AsAccountRef trait:

let seeds = &[b"user", owner.key().as_ref()];
let (is_valid, bump) = account.is_pda(seeds);

if !is_valid {
    return Err(ProgramError::InvalidSeeds);
}

// Store bump for later use in CPI signing
let full_seeds = &[b"user", owner.key().as_ref(), &[bump]];

Performance note: PDA derivation costs ~2000 CUs. If you validate frequently, consider storing the bump in your account data and using a simple key comparison instead.

Creating PDA Accounts

Use AccountRefMut::init_pda() to create and initialize a PDA in one operation:

// Derive PDA and get bump
let (_, bump) = Pubkey::find_program_address(
    &[b"market", base_mint.as_ref(), quote_mint.as_ref()],
    &program_id,
);

// Include bump in seeds
let seeds: &[&[u8]] = &[
    b"market",
    base_mint.as_ref(),
    quote_mint.as_ref(),
    &[bump],
];

// Create PDA and initialize with discriminator
let mut market: AccountRefMut<Market> = AccountRefMut::init_pda(
    market_account,
    payer.info(),
    system_program.info(),
    seeds,
    Market::LEN,
)?;

// Set initial values
market.get_mut().admin = *admin.key();

For lower-level control, use the create_pda_account() function:

use solzempic::create_pda_account;

// Create without initialization
create_pda_account(payer, new_account, &program_id, space, seeds)?;

The AsAccountRef Trait

The AsAccountRef trait provides a common interface for PDA validation on both read-only and writable accounts:

pub trait AsAccountRef<'a, T: Loadable, F: Framework> {
    fn info(&self) -> &'a AccountView;
    fn address(&self) -> &Address;
    fn get(&self) -> &T;
    fn is_pda(&self, seeds: &[&[u8]]) -> (bool, u8);
}

This allows generic code to work with either AccountRef or AccountRefMut:

fn validate_pda<'a, T, F, A>(account: &A, seeds: &[&[u8]]) -> ProgramResult
where
    T: Loadable,
    F: Framework,
    A: AsAccountRef<'a, T, F>,
{
    let (is_valid, _bump) = account.is_pda(seeds);
    if !is_valid {
        return Err(ProgramError::InvalidSeeds);
    }
    Ok(())
}

Validated Program Wrappers

All program and sysvar accounts validate their identity on construction:

// Programs
let system = SystemProgram::wrap(&accounts[0])?;      // Validates key == 11111...
let token = TokenProgram::wrap(&accounts[1])?;        // Validates SPL Token or Token-2022
let ata = AtaProgram::wrap(&accounts[2])?;            // Validates ATA program

// Signers
let signer = Signer::wrap(&accounts[3])?;             // Validates is_signer flag
let payer = Payer::wrap(&accounts[4])?;               // Alias for Signer

// Sysvars
let clock = ClockSysvar::wrap(&accounts[5])?;         // Validates Clock sysvar ID
let rent = RentSysvar::wrap(&accounts[6])?;           // Validates Rent sysvar ID

// Token accounts
let mint = Mint::wrap(&accounts[7])?;                 // Validates token program ownership
let token = TokenAccountRefMut::load(&accounts[8])?;   // Validates token account
let token_account = TokenAccountRefMut::load(&accounts[9])?;

The Framework Trait

The Framework trait allows account wrappers to know your program's ID without passing it everywhere:

pub trait Framework {
    const PROGRAM_ID: Pubkey;
}

// define_framework! generates:
pub struct Solzempic;
impl Framework for Solzempic {
    const PROGRAM_ID: Pubkey = YOUR_PROGRAM_ID;
}

// Which allows:
AccountRefMut::<MyAccount>::load(&account)?  // Automatically checks owner == YOUR_PROGRAM_ID

Derive Macros

#[SolzempicEntrypoint("program_id")]

The main entrypoint attribute that generates everything needed for your program:

#[SolzempicEntrypoint("Your11111111111111111111111111111111111111")]
pub enum MyInstruction {
    Initialize = 0,
    Transfer = 1,
    Close = 2,
}

// Generates:
// - pub const ID: Address = ...
// - pub fn id() -> &'static Address
// - pub type AccountRef<'a, T> = ...
// - pub type AccountRefMut<'a, T> = ...
// - pub type ShardRefContext<'a, T> = ...
// - pub type ShardRefMutContext<'a, T> = ...
// - #[repr(u8)] on the enum
// - TryFrom<u8> for MyInstruction
// - MyInstruction::process() dispatch method
// - The program entrypoint

#[instruction(ParamsType)]

Implements the Instruction trait on an impl block:

pub struct Transfer<'a> {
    pub from: AccountRefMut<'a, TokenAccount>,
    pub to: AccountRefMut<'a, TokenAccount>,
    pub authority: Signer<'a>,
}

#[instruction(TransferParams)]
impl<'a> Transfer<'a> {
    fn build(accounts: &'a [AccountView], params: &TransferParams) -> Result<Self, ProgramError> { ... }
    fn validate(&self, program_id: &Address, params: &TransferParams) -> ProgramResult { ... }
    fn execute(&self, program_id: &Address, params: &TransferParams) -> ProgramResult { ... }
}

// Generates InstructionParams and Instruction trait implementations

Comparison to Alternatives

vs Anchor

Feature Anchor Solzempic
CU overhead ~2000-5000 per instruction ~100 (just your logic)
Binary size Large (IDL, borsh) Minimal
Account validation Automatic, opaque Explicit, transparent
Serialization Borsh (copies data) Zero-copy bytemuck
Learning curve Low (magic) Medium (explicit)
Debugging Hard (generated code) Easy (your code)
Flexibility Constrained Full control

vs Vanilla Pinocchio

Feature Vanilla Pinocchio Solzempic
Boilerplate High (repeat validation) Low (wrappers)
Structure None (DIY) Action pattern
Safety Manual Enforced by types
Program ID handling Pass everywhere Framework trait
Learning curve High Medium

Performance Characteristics

Solzempic adds no runtime overhead beyond what you'd write by hand:

  • Account loading: Single borrow, no copies
  • Parameter parsing: Zero-copy pointer cast
  • Discriminator checks: Single byte comparison
  • Program validation: Pubkey comparison (32-byte memcmp)

All wrapper methods are #[inline] and compile away in release builds.

Typical instruction overhead:

  • Parse params: ~10 CUs
  • Load AccountRefMut: ~50 CUs (borrow + discriminator check)
  • Wrap Signer: ~20 CUs (is_signer check)
  • Your business logic: varies

API Reference

Macros

Macro Purpose
#[SolzempicEntrypoint("...")] Main entrypoint - generates ID, type aliases, dispatch, and entrypoint
#[instruction(Params)] Implements Instruction trait on an impl block
define_framework!(ID) Alternative: manually define framework type aliases
define_account_types! { ... } Define account discriminator enum

Account Wrappers

Type Purpose
AccountRef<'a, T> Read-only typed account with ownership validation
AccountRefMut<'a, T> Writable typed account with ownership + is_writable checks
ShardRefContext<'a, T> Read-only prev/current/next triplet for sharded data
ShardRefMutContext<'a, T> Mutable prev/current/next triplet for sharded data

Program Wrappers

Type Validates
SystemProgram Key == System Program
TokenProgram Key == SPL Token or Token-2022
AtaProgram Key == Associated Token Program
AltProgram Key == Address Lookup Table Program
Lut Address Lookup Table account

Signer Wrappers

Type Validates
Signer is_signer flag is true
Payer Alias for Signer (semantic clarity)

Token Wrappers

Type Purpose
Mint SPL Token mint account
TokenAccountRefMut Writable token account with utility methods
TokenAccountData Token account data struct

Sysvar Wrappers

Type Sysvar
ClockSysvar Clock (slot, timestamp, epoch)
RentSysvar Rent parameters
SlotHashesSysvar Recent slot hashes
InstructionsSysvar Current transaction instructions
RecentBlockhashesSysvar Recent blockhashes

Traits

Trait Purpose
Instruction Three-phase pattern: build()validate()execute()
InstructionParams Associates a params type with an instruction
Framework Program-specific configuration (program ID)
Loadable POD types with discriminator byte
Initializable Marker trait for types that can be initialized
ValidatedAccount Common interface for validated wrappers
AsAccountRef Common interface for account wrappers (PDA validation, data access)

Utility Functions

Function Purpose
create_pda_account() Create and initialize a PDA via CPI
transfer_lamports() Transfer SOL between accounts
rent_exempt_minimum() Calculate rent-exempt minimum for size
parse_params::<T>() Zero-copy parameter parsing

Constants

Constant Value
SYSTEM_PROGRAM_ID System Program
TOKEN_PROGRAM_ID SPL Token Program
TOKEN_2022_PROGRAM_ID Token-2022 Program
ASSOCIATED_TOKEN_PROGRAM_ID ATA Program
ADDRESS_LOOKUP_TABLE_PROGRAM_ID ALT Program
CLOCK_SYSVAR_ID Clock sysvar
RENT_SYSVAR_ID Rent sysvar
SLOT_HASHES_SYSVAR_ID SlotHashes sysvar
INSTRUCTIONS_SYSVAR_ID Instructions sysvar
RECENT_BLOCKHASHES_SYSVAR_ID RecentBlockhashes sysvar
LAMPORTS_PER_BYTE Rent cost per byte
MAX_ACCOUNT_SIZE Maximum account size (10MB)

Error Handling

Solzempic uses Pinocchio's ProgramError throughout:

// Common errors returned by wrappers:
ProgramError::IllegalOwner           // Account not owned by program
ProgramError::InvalidAccountData     // Wrong discriminator or not writable
ProgramError::AccountAlreadyInitialized  // init() on initialized account
ProgramError::IncorrectProgramId     // Wrong program/sysvar ID
ProgramError::MissingRequiredSignature   // Signer check failed
ProgramError::NotEnoughAccountKeys   // Too few accounts passed
ProgramError::InvalidInstructionData // Params too short

Define your own errors by implementing Into<ProgramError>:

#[repr(u32)]
pub enum MyError {
    InvalidPrice = 1000,
    OrderNotFound = 1001,
}

impl From<MyError> for ProgramError {
    fn from(e: MyError) -> Self {
        ProgramError::Custom(e as u32)
    }
}

Best Practices

  1. Fail fast in build(): Validate account structure early
  2. Keep validate() pure: No state changes, only checks
  3. Document account order: Use comments to specify expected accounts
  4. Use #[inline(always)]: For hot paths in execute()
  5. Prefer AccountRefMut::init_pda(): For PDA creation with initialization
  6. Call reload() after CPI: If you modify accounts via CPI

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for new functionality
  4. Ensure cargo clippy passes
  5. Submit a pull request

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.

Acknowledgments

Built on top of Pinocchio, the minimal Solana program framework.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages