Sol.College

Full lesson

afterbell

Protection for tokenized stocks until the next open, on the chain where you hold them, hedged on Hyperliquid.

What it does

From the project README:

[](https://github.com/Osiyomeoh/afterbell/actions/workflows/ci.yml) [](https://github.com/Osiyomeoh/afterbell/actions/workflows/audit.yml)
Original
Osiyomeoh/afterbell
SuperStack fork
Blockchains/afterbell @ d1b583e
Stars
0
Last push (original)
2026-10-12
Languages
TypeScript, Rust, Solidity, CSS, JavaScript
Detected stack
@solana/kitAnchorNext.jsweb3.js
Categories
DeFi
Licence
MIT

Architecture

318 source files. By extension: .ts ×226, .tsx ×50, .rs ×24, .sol ×18.

Top-level directorySource files
app144
solana53
evm33
hedger30
quoter14
sdk12
research11
pricing9

On-chain programs: solana/programs/afterbell-vault

Dapp: devnet deploy pending

⏳ Devnet deploy pending — goes live when the devnet wallet is topped up. This fork's own dapp is not deployed yet, so there is no program address or transaction to show. It is queued in dapp-queue.txt; once devnet SOL is available it is deployed, exercised with real devnet transactions, and this section switches to the live dapp at /dapps/afterbell/. Until then, the lesson and step-by-step guide below are complete, and you can run it locally with the commands below.

Key code, explained

Excerpts chosen automatically where the code uses the most Solana concepts, pinned to the fork's commit.

Osiyomeoh/afterbell · solana/programs/afterbell-vault/src/instructions/sizer.rs L49–71 @ d1b583e · MIT · original
    s.max_shares = b.max_shares;    s.min_start_bps = b.min_start_bps;    s.max_depth_bps = b.max_depth_bps;    s.bump = ctx.bumps.sizer;    emit!(SizerSet {        order,        sizer: b.sizer,        max_shares: b.max_shares,        min_start_bps: b.min_start_bps,        max_depth_bps: b.max_depth_bps    });    Ok(())} #[derive(Accounts)]pub struct RemoveSizer<'info> {    #[account(mut)]    pub holder: Signer<'info>,    #[account(has_one = holder)]    pub order: Box<Account<'info, Order>>,    #[account(mut, close = holder, seeds = [SIZER_SEED, order.key().as_ref()], bump = sizer.bump)]    pub sizer: Box<Account<'info, Sizer>>,}
  1. Line 52 s.bump = ctx.bumps.sizer;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  2. Line 53 emit!(SizerSet {: Emits an Anchor event that clients can parse from the logs.
  3. Line 66 pub holder: Signer<'info>,: Requires this account to have signed the transaction.
  4. Line 67 #[account(has_one = holder)]: Checks that a stored pubkey field matches the passed account.
  5. Line 69 #[account(mut, close = holder, seeds = [SIZER_SEED, order.key().as_ref()], bump = sizer.bu: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  6. Line 69 #[account(mut, close = holder, seeds = [SIZER_SEED, order.key().as_ref()], bump = sizer.bu: Closes the account at the end of the instruction and refunds its lamports.
Osiyomeoh/afterbell · solana/programs/afterbell-vault/src/instructions/admin.rs L229–252 @ d1b583e · MIT · original
 #[derive(Accounts)]#[instruction(feed_id: [u8; 32])]pub struct AllowFeed<'info> {    #[account(mut)]    pub authority: Signer<'info>,    #[account(has_one = authority)]    pub vault: Box<Account<'info, Vault>>,    #[account(        init_if_needed,        payer = authority,        space = 8 + FeedConfig::INIT_SPACE,        seeds = [FEED_SEED, vault.key().as_ref(), feed_id.as_ref()],        bump    )]    pub feed_config: Box<Account<'info, FeedConfig>>,    pub system_program: Program<'info, System>,} pub fn handle_allow_feed(ctx: Context<AllowFeed>, feed_id: [u8; 32], symbol: String, allowed: bool) -> Result<()> {    require!(feed_id != [0u8; 32], VaultError::FeedNotAllowed);    require!(!symbol.is_empty() && symbol.len() <= MAX_SYMBOL_LEN, VaultError::TooLong);    let f = &mut ctx.accounts.feed_config;    f.vault = ctx.accounts.vault.key();
  1. Line 234 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 235 #[account(has_one = authority)]: Checks that a stored pubkey field matches the passed account.
  3. Line 238 init_if_needed,: Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
  4. Line 241 seeds = [FEED_SEED, vault.key().as_ref(), feed_id.as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  5. Line 242 bump: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  6. Line 249 require!(feed_id != [0u8; 32], VaultError::FeedNotAllowed);: An explicit validation that aborts the instruction with an error.
Osiyomeoh/afterbell · solana/programs/afterbell-vault/src/instructions/pool.rs L109–132 @ d1b583e · MIT · original
// ------------------------------------------------------------- request_redeem #[derive(Accounts)]pub struct RequestRedeem<'info> {    #[account(mut)]    pub owner: Signer<'info>,    #[account(mut, has_one = share_mint, has_one = share_escrow)]    pub vault: Box<Account<'info, Vault>>,    #[account(        init_if_needed,        payer = owner,        space = 8 + Request::INIT_SPACE,        seeds = [REQUEST_SEED, vault.key().as_ref(), owner.key().as_ref()],        bump    )]    pub request: Box<Account<'info, Request>>,    /// The rate of the round that filled an earlier redemption request, if one is waiting.    pub rate: Option<Box<Account<'info, RoundRate>>>,    pub share_mint: Box<InterfaceAccount<'info, ShareMint>>,    #[account(mut, token::mint = share_mint, token::authority = owner, token::token_program = share_token_program)]    pub owner_shares: Box<InterfaceAccount<'info, ShareAccount>>,    #[account(mut)]    pub share_escrow: Box<InterfaceAccount<'info, ShareAccount>>,    pub share_token_program: Program<'info, Token2022>,
  1. Line 114 pub owner: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 115 #[account(mut, has_one = share_mint, has_one = share_escrow)]: Checks that a stored pubkey field matches the passed account.
  3. Line 118 init_if_needed,: Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
  4. Line 121 seeds = [REQUEST_SEED, vault.key().as_ref(), owner.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  5. Line 122 bump: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  6. Line 132 pub share_token_program: Program<'info, Token2022>,: Targets the Token-2022 program instead of the original Token Program.
Osiyomeoh/afterbell · solana/programs/afterbell-vault/src/instructions/settle.rs L49–72 @ d1b583e · MIT · original
    series.state = SeriesState::Settled;    emit!(SeriesSettled { series: series.key(), price: series.price, publish_time: msg.publish_time });    Ok(())} // ----------------------------------------------------------------- void_series #[derive(Accounts)]pub struct VoidSeries<'info> {    pub vault: Box<Account<'info, Vault>>,    #[account(mut, has_one = vault)]    pub series: Box<Account<'info, Series>>,} /// Permissionless: a series nobody settled within the void delay voids, and/// every premium in it is refunded in full, the keeper's tip included.pub fn handle_void_series(ctx: Context<VoidSeries>) -> Result<()> {    let now = Clock::get()?.unix_timestamp;    let series = &mut ctx.accounts.series;    require!(series.state == SeriesState::Open, VaultError::SeriesNotOpen);    let deadline =        series.open_ts.checked_add(ctx.accounts.vault.params.void_delay_secs as i64).ok_or(VaultError::MathOverflow)?;    require!(now > deadline, VaultError::VoidTooEarly);    series.state = SeriesState::Voided;
  1. Line 50 emit!(SeriesSettled { series: series.key(), price: series.price, publish_time: msg.publish: Emits an Anchor event that clients can parse from the logs.
  2. Line 59 #[account(mut, has_one = vault)]: Checks that a stored pubkey field matches the passed account.
  3. Line 66 let now = Clock::get()?.unix_timestamp;: Reads on-chain time from the Clock sysvar.
  4. Line 68 require!(series.state == SeriesState::Open, VaultError::SeriesNotOpen);: An explicit validation that aborts the instruction with an error.
  5. Line 70 series.open_ts.checked_add(ctx.accounts.vault.params.void_delay_secs as i64).ok_or(VaultEr: Uses checked arithmetic so overflow fails instead of wrapping.

How to run, build and test

These commands come from the repo's own manifests (package.json scripts, Anchor.toml, Cargo, go.mod, Makefile). Check the README for prerequisites and environment variables.

DirectoryCommandRuns
.npm run testvitest run
sdknpm run buildtsup && tsc -p tsconfig.build.json && node fix-types.mjs
appnpm run devnext dev
appnpm run startnext start
appnpm run buildnext build
solanaanchor buildcompile programs and generate IDLs
solanaanchor testlocal validator + test suite
solanaanchor run testcargo test

What you could build from it

Used in lessons