Engine
Architecture
The on-chain shape of Shain: one program, two PDA types, one treasury ATA. No relayer, no mixer, no upgrade authority on mainnet.
At a glance
| Component | Kind | Count | Owner |
|---|---|---|---|
| Program | Anchor crate | 1 | Deployed on Solana |
ShainConfig | PDA (singleton) | 1 | Program |
ShainSession | PDA (per holder) | N | Program |
| Treasury ATA | SPL token account | 1 | Treasury PDA |
Request flow
A holder session opens, is used, and expires in three round trips:
holder ──[start_session]──▶ program
┃
┗━─[token transfer fee]─▶ treasury ATA
┗━ write ShainSession PDA
dapp ──[gated_action]──▶ program
┗━ assert session.active
┗━ actions_count += 1
anyone ──[close_session]──▶ program
┗━ assert !session.active
┗━ close = user (rent refund)Program boundaries
The program is a single Rust crate at programs/shain/ in the monorepo. It exposes four instructions and defines two account types. The on-chain module graph is flat:
lib.rs ← #[program] dispatch + declare_id!
constants.rs ← PDA seeds, defaults, limits
error.rs ← ShainError enum
state.rs ← ShainConfig + ShainSession accounts
instructions.rs ← re-export barrel
instructions/
initialize.rs
start_session.rs
gated_action.rs
close_session.rsDeliberate constraints:
- No admin_update instruction. Config parameters are set once at
initialize. - No pause bit. If the program is buggy, the only safe path is redeployment with a new program ID — no kill switch can be abused.
- No upgrade authority on mainnet. The deployment retires the upgrade authority before the mainnet announcement.
PDA summary
| Account | Seeds | Space |
|---|---|---|
ShainConfig | [b"shain_config"] | 8 + 154 B |
Treasury authority | [b"shain_treasury"] | pass-through (no data) |
ShainSession | [b"shain_session", user.as_ref()] | 8 + 65 B |
Seed bytes are constants in programs/shain/src/constants.rs, re-exported by the SDK at SHAIN_SEEDS. Never recompute them from a string — use the constants.
CPI surface
Shain performs two CPIs itself:
- SPL Token
transferinsidestart_session, to movesession_feefrom the holder's ATA to the treasury ATA. - Associated Token Program
createinsideinitialize, to mint the treasury ATA.
Downstream dapps CPI into Shain via gated_action. Shain does not CPI back into them; the integration is one-way.
Determinism
- PDA addresses are deterministic from seeds. Given the program ID and a holder pubkey, everyone computes the same session PDA.
- Session expiry is based on
Clock::unix_timestamp. Clock drift between validator and integrator is negligible for a 24-hour window, but integrators should fetchgetSlot/getBlockTimefrom the same RPC they use forsendTransaction. - Instruction discriminators are Anchor-derived. Do not hand- assemble them; use the IDL or the SDK.