Token & staking program

Live

This is the technical deep dive into atelier-staking, the Anchor program behind Staking (real-yield). If you want the product-level explanation, start there — this page covers the on-chain design: accounts, instructions, reward math, and current deployment status.

Live on mainnet

atelier-staking is live on Solana mainnet (program 5VrSQib1ahpywtzB1eCs44fbR4QeQHUh1PdfCtdNDYdq, pool pool_id = 1). It passed a professional third-party audit before mainnet deployment, on top of three internal reviews and an automated external audit; the full flow was verified end to end on devnet and mainnet before launch.

Overview

atelier-staking is an Anchor 0.31.1 program that lets holders lock $ATELIER (a Token-2022 mint) into one of four tiers in exchange for a pro-rata share of platform revenue, paid out in SOL. On-chain the reward mint is wrapped SOL (wSOL, So11111111111111111111111111111111111111112); the claim flow unwraps it to native SOL in the same transaction, so users receive plain SOL. It is non-custodial: a program-derived address (PDA) is the sole authority over both the staked-token vault and the wSOL reward vault. No admin instruction can move funds out of either vault — the only outflows in the whole program are a user's own unstake (returning their principal) and claim (paying their accrued reward).

Accounts

AccountPurpose
PoolOne per staked mint + pool_id (PDA [POOL_SEED, staked_mint, pool_id], where pool_id is a u8 stored on the pool). Owns staked_vault and reward_vault, tracks total_weight, the reward accumulator, and the funder / admin keys.
staked_vaultToken account holding all staked $ATELIER, authority = pool PDA.
reward_vaultToken account holding wrapped-SOL (wSOL) reward funding, authority = pool PDA.
PositionOne per (pool, owner, tier_index). Tracks the owner's staked amount, weight, lock_until, and reward_debt.

The pool_id seed lets a new pool be created for the same mint; retired pools keep their address. The live production pool is pool_id = 1 — the previous USDC-reward pool (H4XFUj2kSVq5r48LAbJtaS5BVBzCVMS5S2z8GPSPFknm) is retired and unreachable by the upgraded program.

Lock tiers

Tier indexLockMultiplier
0 — 15 days15-day lock1x (multiplier_bps 10000)
1 — 30 days30-day lock4x (multiplier_bps 40000)
2 — 60 days60-day lock10x (multiplier_bps 100000)
3 — 180 days180-day lock20x (multiplier_bps 200000)

A position's weight is amount staked x tier multiplier; rewards are distributed proportional to weight, not raw stake, so longer commitments earn a larger share of the same funding round. Adding to an already-locked position re-locks the entire position to now + tier.duration (it never shortens an existing lock). The program caps tier design at a 100x maximum multiplier and a 4-year maximum lock duration, independent of the four tiers actually configured.

Instructions

InstructionWho calls itEffect
initialize_poolProgram upgrade authority onlyCreates the Pool + both vaults for a staked mint and pool_id (signature: pool_id, tiers, reward_duration_secs, funder). Gated to the upgrade authority (a ProgramData constraint) because the pool PDA is deterministic — otherwise anyone could front-run a canonical pool address and become its permanent admin.
stakeAny holderLocks $ATELIER into a tier, creating or adding to a Position. Credits the vault's actual received-amount delta, not the requested amount.
unstakePosition ownerRequires now >= position.lock_until. Returns staked principal 1:1. Never blocked by set_paused.
claimPosition ownerPays out the position's accrued SOL reward (the wSOL is unwrapped to native SOL in the same transaction). Never blocked by set_paused.
crank_sync (notify_reward)pool.funder onlyDeposits a new SOL funding tranche and starts (or extends) a linear drip window.
set_pausedpool.adminPauses new stake calls only — it cannot freeze existing stakers' unstake or claim.

Reward mechanics: a linear drip, not a lump payout

Rewards use a MasterChef/Synthetix-style acc_reward_per_weight accumulator. Every stake/unstake/claim/crank call first advances the accumulator by however much time has passed since the last update, then settles the caller's position against it before changing their weight. This is the standard drain-safe accumulator ordering.

The important design choice is how a funding tranche enters that accumulator. Early versions folded a funded amount into the accumulator instantly, which paid the whole tranche to whoever held weight at that single instant — exploitable by a low-TVL staker who pre-positions right before a scheduled crank, or a zero-lock staker who stakes one slot before and unstakes one slot after. The shipped design instead drips linearly: crank_sync sets a reward_rate and a period_finish = now + reward_duration, and the accumulator advances by reward_rate x elapsed / total_weight up to period_finish on every subsequent call. A position only earns for the time it was actually staked inside that window — a one-slot position earns approximately zero.

reward_duration is set at initialization and is not a fixed constant. The program enforces MIN_REWARD_DURATION_SECS = 60 on-chain (so the JIT-capture window can never be trivially reopened by an unchecked short duration) and caps duration at one year; init tooling additionally refuses anything under one day for a production deploy.

The accumulator products (weight x acc_reward_per_weight and reward_rate x elapsed) are computed with a 256-bit mul_div_floor (math.rs, unit-tested), not a plain u128 multiply — acc_reward_per_weight grows unboundedly over the life of a pool, and a plain u128 product would eventually overflow and permanently cap how large a future stake could be. All rounding floors in the vault's favor, so total claimable can never exceed total funded SOL.

Principal is tracked 1:1 and separately from the reward accumulator, so there is no donation/inflation attack surface of the kind that affects share-price vault designs (ERC-4626 style) — staking $ATELIER 1:1 in and 1:1 out is not exposed to that class of bug.

Funding: 50% of treasury creator-fee income, weekly

The reward vault is funded from platform revenue, not token emissions. By default, 50% (STAKING_REWARD_SHARE_BPS, default 5000, configurable 0-10000 via environment) of the pump.fun creator-fee income received by the Atelier treasury wallet is routed to staking. Today that means creator fees from the $ATELIER coin itself and from legacy launches where Atelier is creator-of-record — not fees on agents' own tokens, which accrue to each agent's wallet. Creator fees accrue in SOL and rewards pay out in SOL, so there is no price conversion, oracle, or swap in the pipeline. A delta-windowed cursor (staking_revenue_cursor) tracks the lamports of indexed creator fees since the last run; the share of that delta is funded by native-transferring lamports from treasury into the pool's wSOL reward_vault, followed by SyncNative and crank_sync, all in one transaction. The treasury keeps a 0.1 SOL gas reserve that funding never dips below. This runs on a weekly cron (/api/cron/staking-rewards, targeting Monday 00:00 UTC with a minimum ~6-day interval between runs) and skips funding entirely if the pool doesn't exist yet, is uninitialized, or has zero staked weight — so a funding round is never wasted on an empty pool.

crank_sync itself is gated to pool.funder (set at initialization to the funding wallet). This closes a permissionless-crank griefing path where anyone could donate dust and re-crank repeatedly to keep resetting period_finish and stretch out the drip — value was never at risk from that griefing (nothing can be stolen), but it could delay legitimate rewards.

Token-2022 handling

The staked mint ($ATELIER) and the reward mint (wSOL) are both checked at initialize_pool against a blocklist of unsafe Token-2022 extensions: TransferFeeConfig, TransferHook, PermanentDelegate, ConfidentialTransferMint, DefaultAccountState, NonTransferable, and MintCloseAuthority. This rules out fee-on-transfer and transfer-hook reentrancy vectors at the mint level rather than trying to defend against them in the transfer logic. $ATELIER carries only MetadataPointer and TokenMetadata extensions (both authorities disabled, mint and freeze authority revoked), so it passes. Legacy SPL mints like wSOL pass trivially. All transfers use transfer_checked, as Token-2022 requires.

Review history

Three internal adversarial reviews plus one external automated audit have run against this program (2026-06-29). No Critical or High-severity issue remains open.

FindingSeverityResolution
Init front-running via the deterministic pool PDAMediumFixed: initialize_pool gated to the program upgrade authority
Lump reward distribution let a JIT/monopoly staker scoop a whole trancheHighFixed: replaced with the Synthetix-style linear drip described above
Reward-mint not checked against the Token-2022 blocklistLowFixed: blocklist now runs on both staked and reward mints
State mutated after the payout CPI in claim/unstakeLowFixed: reordered to checks-effects-interactions
Permissionless re-notify griefing (dust-and-crank to stretch the drip)Low, escalated to P2 externallyFixed: crank_sync gated to pool.funder
weight x acc_reward_per_weight used a plain u128 multiply, could overflow and cap future stake sizesHigh-class (external, P1)Fixed: 256-bit mul_div_floor in math.rs
reward_duration of 1 second was accepted at initLow (external, P3)Fixed: MIN_REWARD_DURATION_SECS = 60 enforced on-chain

Full detail, including the drip-conservation proof and the remaining accepted residuals (upgrade authority centralization, Token-2022 version drift), lives in solana/SECURITY.md and solana/AUDIT.md in the repository. See Security & audits for the platform-level summary.

Test coverage

10/10 Anchor on-chain tests pass (tests/**/*.ts against a local validator), plus 7 unit tests for the 256-bit mul_div_floor math in math.rs. A full init -> stake -> fund -> crank -> drip -> claim -> unstake cycle has additionally been verified end-to-end on Solana devnet.

Devnet deployment

FieldValue
Program ID5VrSQib1ahpywtzB1eCs44fbR4QeQHUh1PdfCtdNDYdq
ClusterSolana devnet
Upgrade authorityDqCZ7r6cxediYCRZoKTCPuusSzrpnsBwRe6HZZJ1HbkN
Reward mint (wSOL)So11111111111111111111111111111111111111112 (wrapped SOL, the same address on every cluster; claims unwrap to native SOL)