PumpTek
Size-based sell cut funding an 8-hour buyback
A trade hook and a fee hook that work together.
What it does
Bordrless’s reading of the source, written 14 Oct, 23:47 UTC. Read by AI from the code, not an audit.
Big sells pay up to 7.8% of their SOL into the creator fee. The pot spends it buying the token and burning it over 8 hours, paying 20% to a fixed wallet.
On a sell, the hook takes a SOL cut that grows with the sell's share of the pool's tokens. Selling 1% pays about 0.8%, and 10% or more pays 7.8%. Buys and transfers pay nothing. The cut joins the 2% creator fee and reaches the companion's pot. Once the pot holds 0.2 SOL, the script spreads that SOL over 480 one-minute steps, about 8 hours. Each step buys the token (at most 1% of the pool's SOL) and burns it. 20% of every SOL the pot receives is owed to one fixed wallet, paid at most 10% of the pot a step and 25% a day.
The most it takes
Up to 7.8% of a sell's SOL, more for bigger sells; nothing on buys
Every rule
When it applies, what it takes, and where that goes.
| On | When | What happens | At most | To |
|---|---|---|---|---|
| Buys | Buys | free, no hook cut | 0% | — |
| Sells | Sells | About 0.8% of the SOL selling 1% of the pool's tokens, up to 7.8% at 10% or more, to the creator fee | 7.8% | The creator |
| Transfers | Transfers | free | 0% | — |
| Each step | Every 60 seconds, once the pot holds 0.2 SOL | Buys and burns about 1/480 of the pot's SOL, at most 1% of the SOL in the pool per buy | 0.21% | Burned |
| Each step | Every 60 seconds, while wallet payouts are allowed | Pays 20% of all SOL the pot receives to one fixed wallet, at most 10% of the pot a step | 10% | A wallet |
What to watch
Things a launcher and a buyer should know before they use it.
- medium20% of all SOL entering the pot goes to one fixed wallet. If wallet payouts are off, that 20% sits in the pot unused.
- mediumThe hook checks many launch and pool details on every trade. If any one doesn't match, every trade of the coin fails.
- mediumLarge sells pay up to 7.8% of their SOL, about 9.75% with Bordrless's share. Splitting a sale into small sells pays far less.
- lowEach buyback pays the price at that moment, so a trader can push the price up just before a step.
- lowBuilt for one exact setup: a 2% creator fee, a 0.3% pool fee and these caps. Any other setup makes every step fail.
Security review
Big sells pay up to 7.8% of their SOL into the creator fee. The pot spends it buying the token and burning it over 8 hours, paying 20% to a fixed wallet.
- mediumThe script: 20% of every pot credit is owed to a hard-coded wallet
The ledger in royalty.rs books totalcredits/5 as royaltydebt. payoutabi.rs answers a payout to the AUTHOR constant (payoutabi.rs:7). This is declared in keeper.json, but the launcher should see that a fifth of the pot never reaches buybacks. State it in the description and the listing.
- mediumThe script: royalty debt is stranded while wallet payouts are disabled
When royaltyenabled is false, baseplan keeps growing royaltydebt. That debt is backed in the pot and never available to buybacks, so 20% of inflows waits indefinitely. Consider releasing it to the buyback after a deadline, or refusing to start without payouts enabled.
- mediumThe hook: quotecut fails the trade on any profile mismatch
In market.rs, authenticatedsnapshot errors propagate rather than answering 0. These include the exact 8 accounts, launch.reserved12..14 matching bumps, launch status 0/1, and a buy's lpfeebps equal to the pool's fee (possibly not true during the sniper window). One mismatch fails every buy or sell. Return 0 on non-authentication mismatches.
- mediumThe script: the buyback minimum is the live quote, not a reference price
buyandburn uses quotebuy on the current reserves as min out (market.rs), so anyone can front-run a permissionless step and back-run it. This is limited to 1% of the pool's SOL and about 1/480 of the pot per step, and the sell cut raises the attacker's cost. A TWAP or reference price would close it.
- lowThe hook: the sell cut is dodged by splitting sells
In penalty.rs the budget scales with received/(pool effective base + received), so n small sells pay roughly 1/n the rate of one large sell. This is inherent to a size-based cut. Disclose it.
- lowThe script: hard-pinned launch profile
validate in market.rs requires creator fee 200, LP 30, quote cut caps (1, 780) and a 2500 share. On any other config every step fails and the pot's SOL waits.
Checks
Approved 14 Oct, 22:25 UTC by its checks
- The template’s rulesPassed
No calls into other programs, no SOL moved, no unsafe code, no reads from the builder.
- Built on Bordrless’s builderPassed
211,912 bytes, hash d6dab14bc4a15b5d58de8de1875e6bb2cf151910d60e1b8eb1e3bbb91bd00051.
- Trading testPassed
Bought, sold and sent by test wallets over a simulated year; the largest cut it took was 0%; its SOL cut took at most 0% of a buy and 0.99% of a sell (caps 0.01% and 7.8%).
- What it does, read by AINotes
It found: The script: 20% of every pot credit is owed to a hard-coded wallet; The script: royalty debt is stranded while wallet payouts are disabled; The hook: quotecut fails the trade on any profile mismatch.
- Fee hook: The template’s rulesPassed
No SOL moved by itself, no unsafe code, no reads from the builder; it calls the DEX to trade its pot.
- Fee hook: Built on Bordrless’s builderPassed
256,960 bytes, hash 56b1cc326ea59ce1f62ac0dcd0fde9f9e752d5b40f6a19568b9002257285c33c.
What the trading test covered
- Simulated a launch at unix time 1791936000 with a 100 bps creator fee: the creator and 6 fresh wallets per round bought, sent to each other, partly sold and sold out at +31s, +1h, +1d, +7d, +30d, +365d, then 50 more trades and a last round.
- SOL cut: the config was made with create_quote_cut_config, caps 1 bps on buys and 780 on sells; the launchpad asked the hook on 199 trades and took SOL on 132: at most 0 bps of a buy's SOL and 99 of a sell's, 49301624 lamports in all.
- Not caught: a rule triggered by a specific amount or by a condition the simulation never reproduces (a price, a supply, a balance or a number of trades it does not reach).
- Not caught: a rule aimed at specific hard-coded wallets (the simulation's creator and traders are its own keys, not the real ones).
- Not caught: anything after +365 days, and behaviour after graduation (the simulation trades on the launch pool and never fills the curve).
On chain
Track record
Turn trading activity into lasting buy pressure. A share of buy/sell fees and exit penalties funds PumpTek. Its dynamic sell penalty rises with sale size relative to pool liquidity, capped at 10% extra. No added buy tax. After claim costs, 80% of allocated fees funds scheduled buys and running costs; 20% pays the hook creator in SOL. Every token bought is burned. Starts near 0.254 SOL, targeting 8 hours. Fresh fees can extend or strengthen buying, helping active launches sustain momentum and reduce supply. No claims needed. Timing depends on service and liquidity; small balances may remain.
Source: the trade hook
The source Bordrless checked and built. Read it before you launch with it.
src/lib.rs256 lines ›
//! Authenticated burn receipts and a read-only, size-dependent quote penalty on sells.
//! Buys return zero. Added seller loss, including the extra protocol share, is capped
//! at 10% of the same trade's normal-fee net proceeds. Ordinary fees remain separate.
//! Local fixture ID below is not a deployed address; Workshop assigns the real program ID.
//! ReceiptState serialized ABI, discriminator included: version@8 u8, bump@9 u8,
//! mint@10 Pubkey, total_burned@42 u128, companion_origin_burned@58 u128,
//! latest_observed_supply@74 u64; all integers little-endian; length 82 bytes.
//! Counters cover successful AFTER_BURN callbacks since prepare, in raw token units.
//! Companion-origin identifies the protocol creator PDA as owner and nondelegate authority,
//! not a particular script, fee provenance, or whether the burn occurred inside a swap.
use anchor_lang::prelude::*;
use bordrless_hook::{
hook_accounts_address, hook_signer, token_flags, write_registry, AccountSource, ExtraAccount,
HookAccountList, Phase, PoolHookArgs, Seed, TokenHookArgs, TokenOp,
};
pub mod market;
pub mod penalty;
declare_id!("6YMEjhBqVTMaSRWcmVkLrnHZ22FWEDJEpTeonAg8GKSy");
pub const TOKEN_HOOK_FLAGS: u16 = token_flags::AFTER_BURN;
pub const VERSION: u8 = 1;
pub const MAX_SELL_BPS: u16 = 780;
pub const MAX_ADDED_LOSS_BPS: u16 = 1_000;
/// The receipt remains the first callback extra. The literal mint is additionally
/// writable so the paired companion can burn acquired tokens without native burn fees.
pub fn registry_for(mint: Pubkey) -> HookAccountList {
HookAccountList::new(vec![
ExtraAccount {
writable: true,
source: AccountSource::Pda {
program: ID,
seeds: vec![Seed::Literal(b"state".to_vec()), Seed::Account(1)],
},
},
ExtraAccount {
writable: true,
source: AccountSource::Key(mint),
},
])
}
#[program]
pub mod burn_receipt_hook {
use super::*;
pub fn prepare(ctx: Context<Prepare>) -> Result<()> {
let mint = ctx.accounts.mint.key();
ctx.accounts.state.set_inner(ReceiptState {
version: VERSION,
bump: ctx.bumps.state,
mint,
total_burned: 0,
companion_origin_burned: 0,
latest_observed_supply: 0,
});
let registry = registry_for(mint);
let (_, bump) = hook_accounts_address(&ID, &mint);
write_registry(
&ctx.accounts.payer.to_account_info(),
&ctx.accounts.registry.to_account_info(),
&ctx.accounts.system_program.to_account_info(),
&ID,
&mint,
bump,
®istry,
)
}
pub fn after_burn(ctx: Context<Callback>, args: TokenHookArgs) -> Result<()> {
record_receipt(
&mut ctx.accounts.state,
&args,
ctx.accounts.hook_signer.key(),
ctx.accounts.hook_signer.is_signer,
[
ctx.accounts.mint.key(),
ctx.accounts.source.key(),
ctx.accounts.destination.key(),
ctx.accounts.authority.key(),
],
)
}
pub fn quote_cut(ctx: Context<QuoteCut>, args: PoolHookArgs) -> Result<u16> {
let market = market::authenticated_snapshot(&ctx.accounts.to_account_infos(), &args, &ID)?;
require!(ctx.accounts.state.version == VERSION, ReceiptError::Version);
if market.buy {
return Ok(0);
}
let observation = penalty::SellObservation {
received_base: market.amount_in,
post_base_reserve: market.base_reserve,
virtual_base: market.virtual_base,
callback_quote: market.amount_out,
};
let policy = penalty::Policy {
creator_bps: market.creator_bps,
protocol_share_bps: market.protocol_share_bps,
quote_cut_cap_bps: MAX_SELL_BPS.min(market.max_sell_bps),
max_added_loss_bps: MAX_ADDED_LOSS_BPS,
impact_divisor: 1,
};
// A valid authenticated trade with no positive fee budget gets no extra penalty.
// Authentication failures above still reject forged or mismatched callbacks.
Ok(penalty::sell_decision(observation, policy)
.map(|decision| decision.quote_cut_bps)
.unwrap_or(0))
}
}
/// Account ownership, discriminator and PDA checks are additionally enforced by Callback.
pub fn record_receipt(
state: &mut ReceiptState,
args: &TokenHookArgs,
signer: Pubkey,
is_signer: bool,
prefix: [Pubkey; 4],
) -> Result<()> {
require!(is_signer, ReceiptError::Authority);
require_keys_eq!(
signer,
hook_signer(&bordrless_game::TOKEN_PROGRAM_ID, &ID).0,
ReceiptError::Authority
);
require!(
args.op == TokenOp::Burn && args.phase == Phase::After,
ReceiptError::Operation
);
require!(state.version == VERSION, ReceiptError::Version);
require_keys_eq!(args.mint, state.mint, ReceiptError::Account);
require!(
prefix == [args.mint, args.source, args.destination, args.authority],
ReceiptError::Account
);
require_keys_eq!(args.destination, args.mint, ReceiptError::Account);
require!(
args.amount > 0
&& args.delta == 0
&& args.destination_balance == 0
&& args.destination_owner == Pubkey::default(),
ReceiptError::Operation
);
let creator = bordrless_game::companion_creator_address(&args.mint);
let holding = Pubkey::find_program_address(
&[b"holding", args.mint.as_ref(), creator.as_ref()],
&bordrless_game::TOKEN_PROGRAM_ID,
)
.0;
let companion_origin = args.source_owner == creator
&& args.authority == creator
&& !args.authority_is_delegate
&& args.source == holding;
let total = state
.total_burned
.checked_add(u128::from(args.amount))
.ok_or(ReceiptError::Overflow)?;
let companion_total = if companion_origin {
state
.companion_origin_burned
.checked_add(u128::from(args.amount))
.ok_or(ReceiptError::Overflow)?
} else {
state.companion_origin_burned
};
state.total_burned = total;
state.companion_origin_burned = companion_total;
state.latest_observed_supply = args.supply;
Ok(())
}
#[account]
#[derive(InitSpace, Debug, PartialEq, Eq)]
pub struct ReceiptState {
pub version: u8,
pub bump: u8,
pub mint: Pubkey,
pub total_burned: u128,
pub companion_origin_burned: u128,
pub latest_observed_supply: u64,
}
impl ReceiptState {
pub const LEN: usize = 8 + Self::INIT_SPACE;
}
#[derive(Accounts)]
pub struct Prepare<'info> {
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: prepare precedes mint creation; only the address is used.
pub mint: UncheckedAccount<'info>,
#[account(init, payer = payer, space = ReceiptState::LEN,
seeds = [b"state", mint.key().as_ref()], bump)]
pub state: Account<'info, ReceiptState>,
/// CHECK: canonical registry; the sanctioned writer creates it once.
#[account(mut, address = hook_accounts_address(&ID, &mint.key()).0)]
pub registry: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Callback<'info> {
/// CHECK: exact token-program PDA and signer status checked by record_receipt.
pub hook_signer: UncheckedAccount<'info>,
/// CHECK: authenticated prefix; token program owns the mint.
#[account(owner = bordrless_game::TOKEN_PROGRAM_ID)]
pub mint: UncheckedAccount<'info>,
/// CHECK: authenticated prefix; token program owns the source holding.
#[account(owner = bordrless_game::TOKEN_PROGRAM_ID)]
pub source: UncheckedAccount<'info>,
/// CHECK: burn destination must equal mint, checked by record_receipt.
pub destination: UncheckedAccount<'info>,
/// CHECK: authenticated argument identity; no authority signature is forwarded.
pub authority: UncheckedAccount<'info>,
#[account(mut, seeds = [b"state", mint.key().as_ref()], bump = state.bump,
constraint = state.mint == mint.key() @ ReceiptError::Account)]
pub state: Account<'info, ReceiptState>,
}
#[derive(Accounts)]
pub struct QuoteCut<'info> {
/// CHECK: exact launch quote-cut PDA and signer status checked before account reads.
pub hook_signer: UncheckedAccount<'info>,
/// CHECK: canonical launch account and contents authenticated by market.
pub launch: UncheckedAccount<'info>,
/// CHECK: canonical pool account and contents authenticated by market.
pub pool: UncheckedAccount<'info>,
/// CHECK: token-program mint authenticated by market.
pub base_mint: UncheckedAccount<'info>,
/// CHECK: canonical bridged SOL mint authenticated by market.
pub quote_mint: UncheckedAccount<'info>,
/// CHECK: authenticated callback actor identity, no signature is forwarded here.
pub actor: UncheckedAccount<'info>,
/// CHECK: canonical actor BASE holding, possibly uninitialized on a buy.
pub actor_holding: UncheckedAccount<'info>,
#[account(seeds = [b"state", base_mint.key().as_ref()], bump = state.bump,
constraint = state.mint == base_mint.key() @ ReceiptError::Account)]
pub state: Account<'info, ReceiptState>,
}
#[error_code]
pub enum ReceiptError {
#[msg("Invalid token callback authority")]
Authority,
#[msg("Expected a positive after-burn receipt")]
Operation,
#[msg("Receipt account mismatch")]
Account,
#[msg("Unsupported receipt state version")]
Version,
#[msg("Receipt counter overflow")]
Overflow,
}
hook.json5 lines ›
{
"version": 1,
"quoteCut": { "maxBuyBps": 1, "maxSellBps": 780 }
}
src/market.rs402 lines ›
//! Read-only quote_cut authentication and pinned native ABI readers.
//! Source pin: bordrless-programs 1aaf8f823c22be5bec9cc753fa1049f4cced1e7c.
//! Mirrors launch/src/state.rs Launch/LaunchRules, swap/src/state.rs Pool, and
//! token/src/state.rs Mint's fixed prefix. No native program crates, writes or CPIs.
//! Timing: swap/src/instructions/swap.rs:200-220,334-405; launch hooks.rs:162-248.
use anchor_lang::prelude::*;
use bordrless_game::{LAUNCH_PROGRAM_ID, TOKEN_PROGRAM_ID};
use bordrless_hook::{pool_flags, token_flags, Phase, PoolHookArgs, PoolOp};
pub const SWAP_PROGRAM_ID: Pubkey =
Pubkey::from_str_const("GyzKSnnEu2uN5bBRecE4XYY2enbfR2D2MtxnbJPGy7hk");
pub const QUOTE_MINT: Pubkey =
Pubkey::from_str_const("A49oVhX22ExMwTEtFC6Y8nhBdZ4LJDGhdXLDn4c2f59i");
const SYSTEM_ID: Pubkey = Pubkey::from_str_const("11111111111111111111111111111111");
const LAUNCH_DISCRIMINATOR: [u8; 8] = [144, 51, 51, 163, 206, 85, 213, 38];
const POOL_DISCRIMINATOR: [u8; 8] = [241, 154, 109, 4, 17, 177, 109, 188];
const MINT_DISCRIMINATOR: [u8; 8] = [80, 188, 245, 20, 95, 138, 57, 156];
const LAUNCH_FLAGS: u16 = pool_flags::BEFORE_INITIALIZE
| pool_flags::BEFORE_SWAP
| pool_flags::AFTER_SWAP
| pool_flags::BEFORE_SWAP_RETURNS_DELTA
| pool_flags::AFTER_SWAP_RETURNS_DELTA
| pool_flags::BEFORE_SWAP_OVERRIDES_FEE;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MarketSnapshot {
pub creator_bps: u16,
pub protocol_share_bps: u16,
pub lp_fee_bps: u16,
pub max_buy_bps: u16,
pub max_sell_bps: u16,
pub buy: bool,
pub curve: bool,
pub amount_in: u64,
pub amount_out: u64,
pub base_reserve: u64,
pub quote_reserve: u64,
pub virtual_base: u64,
pub virtual_quote: u64,
/// The serialized pool count: after this sell, before this buy.
pub swap_count: u64,
pub supply: u64,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default)]
struct RulesAbi {
pub holder_fee_buy_bps: u16,
pub holder_fee_sell_bps: u16,
pub burn_buy_bps: u16,
pub burn_sell_bps: u16,
pub max_wallet_bps: u16,
pub creator_lock_secs: u32,
pub early_window_secs: u32,
pub early_lock_secs: u32,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
struct LaunchAbi {
pub version: u8,
pub bump: u8,
pub mint: Pubkey,
pub creator: Pubkey,
pub pool: Pubkey,
pub quote_mint: Pubkey,
pub status: u8,
pub creator_fee_bps: u16,
pub lp_fee_bps: u16,
pub sniper_window_secs: i64,
pub sniper_start_bps: u16,
pub virtual_quote: u64,
pub virtual_base: u64,
pub graduation_quote: u64,
pub curve_tokens: u64,
pub reserve_tokens: u64,
pub reserve_holding: Pubkey,
pub quote_holding: Pubkey,
pub lp_holding: Pubkey,
pub created_at: i64,
pub graduated_at: i64,
pub creator_fees_accrued: u64,
pub creator_fees_claimed: u64,
pub graduation_topup: u64,
pub graduation_burned: u64,
pub rules: RulesAbi,
pub modules: u8,
pub kit_config: Pubkey,
pub holder_vault: Pubkey,
pub kit_caller_bump: u8,
pub creator_unlock_at: i64,
pub early_window_end: i64,
pub early_unlock_at: i64,
pub creator_bought: bool,
pub holder_fees_accrued: u64,
pub burned_on_trades: u64,
pub config: Pubkey,
pub custom_hook: Option<Pubkey>,
pub custom_hook_flags: u16,
pub author_share_bps: u16,
pub author_fees_paid: u64,
pub reserved: [u8; 22],
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
struct PoolAbi {
pub version: u8,
pub bump: u8,
pub lp_mint_bump: u8,
pub base_mint: Pubkey,
pub quote_mint: Pubkey,
pub lp_mint: Pubkey,
pub base_vault: Pubkey,
pub quote_vault: Pubkey,
pub hook_program: Option<Pubkey>,
pub hook_flags: u16,
pub lp_fee_bps: u16,
pub protocol_fee_bps: u16,
pub base_reserve: u64,
pub quote_reserve: u64,
pub virtual_base: u64,
pub virtual_quote: u64,
pub lp_supply: u64,
pub protocol_fees_quote: u64,
pub curve: bool,
pub creator: Pubkey,
pub created_at: i64,
pub last_swap_at: i64,
pub swap_count: u64,
pub base_volume: u128,
pub quote_volume: u128,
pub hook_signer_bump: u8,
pub fee_model: u8,
pub protocol_share_bps: u16,
pub reserved: [u8; 60],
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Default)]
struct MintPrefix {
pub version: u8,
pub decimals: u8,
pub supply: u64,
pub max_supply: u64,
pub mint_authority: Option<Pubkey>,
pub freeze_authority: Option<Pubkey>,
pub hook_authority: Option<Pubkey>,
pub metadata_authority: Option<Pubkey>,
pub hook_program: Option<Pubkey>,
pub hook_flags: u16,
}
/// The launchpad supplies exactly this prefix, with its own quote-cut PDA signing.
/// State contents/discriminator belong to the caller's Anchor Accounts constraints.
/// Authentication precedes data reads; no signer or mutable borrow is forwarded.
pub fn authenticated_snapshot(
accounts: &[AccountInfo<'_>],
args: &PoolHookArgs,
hook_id: &Pubkey,
) -> Result<MarketSnapshot> {
require!(accounts.len() == 8, MarketError::Accounts);
let signer = &accounts[0];
let (signer_key, signer_bump) =
Pubkey::find_program_address(&[b"quote-cut", hook_id.as_ref()], &LAUNCH_PROGRAM_ID);
require!(
signer.is_signer && *signer.key == signer_key,
MarketError::Authority
);
require!(
args.op == PoolOp::Swap
&& ((args.direction == 1 && args.phase == Phase::Before)
|| (args.direction == 0 && args.phase == Phase::After))
&& args.amount_in > 0
&& args.lp_amount == 0,
MarketError::Operation
);
let buy = args.direction == 1;
require!(
if buy {
args.amount_out == 0
} else {
args.amount_out > 0
},
MarketError::Operation
);
let [launch_info, pool_info, base_info, quote_info, actor, holding_info, state_info] = [
&accounts[1],
&accounts[2],
&accounts[3],
&accounts[4],
&accounts[5],
&accounts[6],
&accounts[7],
];
require!(
*pool_info.key == args.pool
&& *base_info.key == args.base_mint
&& *quote_info.key == args.quote_mint
&& *actor.key == args.actor
&& args.base_mint != args.quote_mint
&& args.quote_mint == QUOTE_MINT,
MarketError::Accounts
);
let (launch_key, launch_bump) =
Pubkey::find_program_address(&[b"launch", base_info.key.as_ref()], &LAUNCH_PROGRAM_ID);
let (state_key, state_bump) =
Pubkey::find_program_address(&[b"state", base_info.key.as_ref()], hook_id);
require!(
*launch_info.key == launch_key
&& *state_info.key == state_key
&& *state_info.owner == *hook_id
&& !state_info.executable,
MarketError::Accounts
);
let launch: LaunchAbi = read(launch_info, &LAUNCH_PROGRAM_ID, &LAUNCH_DISCRIMINATOR)?;
let pool: PoolAbi = read(pool_info, &SWAP_PROGRAM_ID, &POOL_DISCRIMINATOR)?;
let base: MintPrefix = read(base_info, &TOKEN_PROGRAM_ID, &MINT_DISCRIMINATOR)?;
let quote: MintPrefix = read(quote_info, &TOKEN_PROGRAM_ID, &MINT_DISCRIMINATOR)?;
let (pool_key, pool_bump) = Pubkey::find_program_address(
&[
b"pool",
base_info.key.as_ref(),
quote_info.key.as_ref(),
&launch.lp_fee_bps.to_le_bytes(),
LAUNCH_PROGRAM_ID.as_ref(),
],
&SWAP_PROGRAM_ID,
);
require!(
launch.version == 1
&& launch.bump == launch_bump
&& launch.mint == args.base_mint
&& launch.quote_mint == QUOTE_MINT
&& launch.pool == *pool_info.key
&& *pool_info.key == pool_key
&& pool.version == 1
&& pool.bump == pool_bump
&& pool.base_mint == args.base_mint
&& pool.quote_mint == QUOTE_MINT
&& pool.creator == launch_key
&& pool.lp_fee_bps == launch.lp_fee_bps,
MarketError::Accounts
);
// Custom hooks and kit modules are mutually exclusive in the native configuration.
// Read actual fee rates, permitting the builder's creator100/no-burn fixture.
// The companion, not this generic reader, enforces the exact production profile.
require!(
launch.custom_hook == Some(*hook_id)
&& launch.custom_hook_flags == token_flags::AFTER_BURN
&& launch.modules == 0
&& launch.rules.holder_fee_buy_bps == 0
&& launch.rules.holder_fee_sell_bps == 0
&& launch.rules.max_wallet_bps == 0
&& launch.rules.creator_lock_secs == 0
&& launch.rules.early_window_secs == 0
&& launch.rules.early_lock_secs == 0
&& launch.creator_fee_bps <= 10_000
&& launch.rules.burn_buy_bps <= 10_000
&& launch.rules.burn_sell_bps <= 10_000
&& pool.fee_model == 1
&& pool.protocol_fee_bps == 0
&& pool.protocol_share_bps <= 10_000
&& pool.lp_fee_bps <= 9_000
&& pool.hook_program == Some(LAUNCH_PROGRAM_ID)
&& pool.hook_flags == LAUNCH_FLAGS,
MarketError::Profile
);
require!(
(launch.status == 0 && pool.curve)
|| (launch.status == 1
&& !pool.curve
&& pool.virtual_base == 0
&& pool.virtual_quote == 0),
MarketError::Profile
);
let max_buy_bps = u16::from_le_bytes([launch.reserved[0], launch.reserved[1]]);
let max_sell_bps = u16::from_le_bytes([launch.reserved[2], launch.reserved[3]]);
require!(
(1..=1000).contains(&max_buy_bps)
&& max_sell_bps <= 1000
&& launch.reserved[12] == state_bump
&& launch.reserved[13] == signer_bump,
MarketError::Profile
);
require!(
base.version == 1
&& base.hook_program == Some(*hook_id)
&& base.hook_flags == token_flags::AFTER_BURN
&& base.hook_authority.is_none()
&& base.mint_authority.is_none()
&& base.freeze_authority.is_none()
&& quote.version == 1
&& quote.decimals == 9
&& quote.hook_program.is_none()
&& quote.hook_flags == 0
&& quote.hook_authority.is_none()
&& quote.freeze_authority.is_none(),
MarketError::Profile
);
let holding_key = |mint: &Pubkey, owner: &Pubkey| {
Pubkey::find_program_address(
&[b"holding", mint.as_ref(), owner.as_ref()],
&TOKEN_PROGRAM_ID,
)
};
let (expected_holding, holding_bump) = holding_key(base_info.key, actor.key);
require!(
*holding_info.key == expected_holding
&& pool.base_vault == holding_key(base_info.key, pool_info.key).0
&& pool.quote_vault == holding_key(quote_info.key, pool_info.key).0,
MarketError::Accounts
);
// The quote-cut route permits an absent actor holding (e.g. a recipient buy,
// or a delegate with no personal holding). This account is never used to price.
if holding_info.data_is_empty() {
require!(
*holding_info.owner == SYSTEM_ID && !holding_info.executable,
MarketError::Accounts
);
} else {
let holding = bordrless_game::read_holding(holding_info).ok_or(MarketError::Data)?;
require!(
holding.version == 1
&& holding.bump == holding_bump
&& holding.mint == args.base_mint
&& holding.owner == args.actor,
MarketError::Accounts
);
}
// After-swap arguments keep the OLD count, but the pool has been serialized
// with updated reserves and count. Before-buy arguments match the old pool.
let expected_count = if buy {
Some(args.swap_count)
} else {
args.swap_count.checked_add(1)
};
require!(
expected_count == Some(pool.swap_count)
&& args.base_reserve == pool.base_reserve
&& args.quote_reserve == pool.quote_reserve
&& args.virtual_base == pool.virtual_base
&& args.virtual_quote == pool.virtual_quote
&& args.created_at == pool.created_at
&& args.protocol_fee_bps == pool.protocol_fee_bps
&& args.lp_fee_bps <= 9000
&& (!buy || args.lp_fee_bps == pool.lp_fee_bps),
MarketError::Timing
);
Ok(MarketSnapshot {
creator_bps: launch.creator_fee_bps,
protocol_share_bps: pool.protocol_share_bps,
lp_fee_bps: args.lp_fee_bps,
max_buy_bps,
max_sell_bps,
buy,
curve: pool.curve,
amount_in: args.amount_in,
amount_out: args.amount_out,
base_reserve: pool.base_reserve,
quote_reserve: pool.quote_reserve,
virtual_base: pool.virtual_base,
virtual_quote: pool.virtual_quote,
swap_count: pool.swap_count,
supply: base.supply,
})
}
fn read<T: AnchorDeserialize>(
info: &AccountInfo,
owner: &Pubkey,
discriminator: &[u8; 8],
) -> Result<T> {
require!(
*info.owner == *owner && !info.executable,
MarketError::Accounts
);
let data = info.try_borrow_data()?;
require!(
data.get(..8) == Some(discriminator.as_slice()),
MarketError::Data
);
// Native accounts allocate maximum Borsh space, including unused Option bytes.
// Trailing allocation is intentionally permitted; truncated fields are rejected.
T::deserialize(&mut &data[8..]).map_err(|_| error!(MarketError::Data))
}
#[error_code]
pub enum MarketError {
#[msg("Invalid launchpad quote-cut signer")]
Authority,
#[msg("Quote-cut account or PDA mismatch")]
Accounts,
#[msg("Invalid native account discriminator or data")]
Data,
#[msg("Expected buy-before or sell-after quote-cut callback")]
Operation,
#[msg("Unsupported native launch fee or token profile")]
Profile,
#[msg("Quote-cut arguments do not match the native pool snapshot")]
Timing,
}
#[cfg(test)]
mod tests;
src/penalty.rs147 lines ›
//! Pure integer sell-penalty calculation; account authentication belongs to the callback.
//! Inputs must come from authenticated sell-After PoolHookArgs and the authenticated native profile.
//! Applies only to SHARE pools with no holder fee or BASE-transfer cut; native BASE burn is allowed.
const BPS: u128 = 10_000;
#[derive(Clone, Copy, Debug)]
pub struct SellObservation {
/// PoolHookArgs.amount_in: what actually reached the vault after native burn.
pub received_base: u64,
/// PoolHookArgs.base_reserve: already updated when sell quote_cut runs.
pub post_base_reserve: u64,
pub virtual_base: u64,
/// PoolHookArgs.amount_out: after initial LP/input-cut share, before output cuts/share.
pub callback_quote: u64,
}
#[derive(Clone, Copy, Debug)]
pub struct Policy {
/// Native creator rate, authenticated by the callback; holder fee must be zero.
pub creator_bps: u16,
/// Native SHARE rate, authenticated from the pool; not PoolHookArgs.protocol_fee_bps.
pub protocol_share_bps: u16,
/// Configured quote-cut cap. Native maximum is 1,000; never a promise about seller loss.
pub quote_cut_cap_bps: u16,
/// Maximum additional loss as a fraction of same-trade baseline NET proceeds; <= 1,000.
pub max_added_loss_bps: u16,
/// 1 targets min(i, cap). Values greater than one reduce the impact response.
pub impact_divisor: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Error {
InvalidPolicy,
InconsistentPostReserve,
ZeroDepth,
BaselineNotPositive,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Decision {
pub quote_cut_bps: u16,
pub pre_real_base: u64,
pub pre_effective_base: u128,
pub baseline_net_quote: u64,
pub added_loss_budget: u64,
pub creator_cut: u64,
pub quote_cut: u64,
pub protocol_share_with_cut: u64,
pub net_quote: u64,
pub added_seller_loss: u64,
}
fn ceil_fee(amount: u64, bps: u16) -> u64 {
((u128::from(amount) * u128::from(bps) + BPS - 1) / BPS) as u64
}
fn settlement(a: u64, creator_cut: u64, share: u16, rate: u16) -> Option<(u64, u64, u64)> {
let cut = (u128::from(a) * u128::from(rate) / BPS) as u64;
let total = creator_cut.checked_add(cut)?;
// A candidate whose quote cut would be dropped is not considered an effective penalty.
if total >= a {
return None;
}
let protocol = ceil_fee(total, share);
let net = a.checked_sub(total)?.checked_sub(protocol)?;
(net > 0).then_some((cut, protocol, net))
}
/// Returns the greatest permitted integer rate (basis points), with an exact seller-loss guard.
/// i = received / (pre_effective_base + received) is an ideal effective-quote withdrawal fraction.
/// It is NOT an exact observed price decline; integer curve rounding and fees still apply.
pub fn sell_decision(o: SellObservation, p: Policy) -> Result<Decision, Error> {
if p.creator_bps > 10_000
|| p.protocol_share_bps > 10_000
|| p.quote_cut_cap_bps > 1_000
|| p.max_added_loss_bps > 1_000
|| p.impact_divisor == 0
{
return Err(Error::InvalidPolicy);
}
let pre_real = o
.post_base_reserve
.checked_sub(o.received_base)
.ok_or(Error::InconsistentPostReserve)?;
let pre_effective = u128::from(pre_real) + u128::from(o.virtual_base);
let post_effective = pre_effective + u128::from(o.received_base);
if post_effective == 0 {
return Err(Error::ZeroDepth);
}
let raw_creator = ceil_fee(o.callback_quote, p.creator_bps);
// Matches native creator_and_holder_fees with holder_bps=0: drop if not strictly below A.
let creator = if raw_creator < o.callback_quote {
raw_creator
} else {
0
};
let (_, base_share, baseline) = settlement(o.callback_quote, creator, p.protocol_share_bps, 0)
.ok_or(Error::BaselineNotPositive)?;
let impact_budget = u128::from(baseline) * u128::from(o.received_base)
/ (post_effective * u128::from(p.impact_divisor));
let cap_budget = u128::from(baseline) * u128::from(p.max_added_loss_bps) / BPS;
let budget = impact_budget.min(cap_budget) as u64;
let mut rate = 0;
if budget > 0 {
let mut lo = 0u16;
let mut hi = p.quote_cut_cap_bps;
while lo < hi {
let mid = lo + (hi - lo + 1) / 2;
let fits = settlement(o.callback_quote, creator, p.protocol_share_bps, mid)
.is_some_and(|(_, _, net)| baseline - net <= budget);
if fits {
lo = mid;
} else {
hi = mid - 1;
}
}
rate = lo;
}
let (cut, share, net) = settlement(o.callback_quote, creator, p.protocol_share_bps, rate)
.ok_or(Error::BaselineNotPositive)?;
let added = baseline - net;
debug_assert!(added <= budget);
debug_assert_eq!(added, cut + share - base_share);
Ok(Decision {
quote_cut_bps: rate,
pre_real_base: pre_real,
pre_effective_base: pre_effective,
baseline_net_quote: baseline,
added_loss_budget: budget,
creator_cut: creator,
quote_cut: cut,
protocol_share_with_cut: share,
net_quote: net,
added_seller_loss: added,
})
}
/// Buy callback must authenticate first in its callback, then answer zero without sale logic.
pub fn quote_rate(buy: bool, o: SellObservation, p: Policy) -> Result<u16, Error> {
if buy {
Ok(0)
} else {
sell_decision(o, p).map(|d| d.quote_cut_bps)
}
}
Source: the fee hook
The source Bordrless checked and built. Read it before you launch with it.
src/lib.rs427 lines ›
//! RC10 fixed Workshop pair: unavailable royalty payment does not disable funded buybacks.
//! Local test ID only; no listing, deployment or hosted-service guarantee.
#![allow(unexpected_cfgs)]
use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::set_return_data;
pub mod companion_abi {
//! Pinned local ABI mirror: upstream Bordrless programs commit 1aaf8f823c22be5bec9cc753fa1049f4cced1e7c.
//! Golden equivalence tests live outside the submitted program packages.
use anchor_lang::prelude::*;
use anchor_lang::Discriminator;
pub const ID: Pubkey = Pubkey::from_str_const("6ZUM1gWBH9hBBNoJoaVAGwSftyZ6CUda6vUZTW9MsJuo");
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct Split {
pub buyback_bps: u16,
pub holders_bps: u16,
pub beneficiary_bps: u16,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct Companion {
pub version: u8,
pub bump: u8,
pub creator_bump: u8,
pub mint: Pubkey,
pub beneficiary: Pubkey,
pub split: Split,
pub bounty_bps: u16,
pub max_buyback: u64,
pub buyback_interval: i64,
pub vest_secs: i64,
pub launched: bool,
pub launched_at: i64,
pub dev_tokens: u64,
pub dev_released: u64,
pub pending_buyback: u64,
pub pending_holders: u64,
pub pending_beneficiary: u64,
pub last_buyback_at: i64,
pub claimed_total: u64,
pub spent_total: u64,
pub burned_total: u64,
pub shared_total: u64,
pub paid_beneficiary_total: u64,
pub bounties_total: u64,
pub reference_price: u128,
pub reference_at: i64,
pub game_hook: Pubkey,
pub pot_bps: u16,
pub pending_pot: u64,
pub round_secs: u32,
pub stranded_burned_at: i64,
pub game_kind: GameKind,
pub pot_locked: u64,
pub mode: u8,
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug)]
pub enum GameKind {
Lottery,
Jackpot,
Streak,
Strategy,
}
impl Companion {
pub fn is_script(&self) -> bool {
self.mode == 2
}
}
impl anchor_lang::Discriminator for Companion {
const DISCRIMINATOR: &'static [u8] = &[189, 21, 247, 69, 245, 100, 10, 15];
}
impl AccountDeserialize for Companion {
fn try_deserialize(buf: &mut &[u8]) -> Result<Self> {
if buf.len() < 8 || &buf[..8] != Self::DISCRIMINATOR {
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into());
}
Self::try_deserialize_unchecked(buf)
}
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into());
}
Self::deserialize(&mut &buf[8..])
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
}
}
impl AccountSerialize for Companion {
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
writer
.write_all(Self::DISCRIMINATOR)
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?;
self.serialize(writer)
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize.into())
}
}
impl Owner for Companion {
fn owner() -> Pubkey {
ID
}
}
}
pub mod ledger;
pub mod market;
pub mod payout_abi;
pub mod pool_abi;
pub mod receipt;
pub mod royalty;
use companion_abi::Companion;
declare_id!("Eucwumt1JuZU8zKNY8jxymFgmwdzWT7FX6VZg7fhGUKa");
pub const STATE_LEN: usize = 169;
pub const HOSTED_STATE_LEN: usize = 1024;
pub const EMPTY_ANSWER: [u8; 5] = [1, 0, 0, 0, 0];
/// Gross quote spent per purchase is at most 1% of authenticated real quote reserve.
pub const BUY_DEPTH_DIVISOR: u64 = 100;
#[program]
pub mod twap_script {
use super::*;
pub fn companion_init(
ctx: Context<CompanionInit>,
mint: Pubkey,
init_data: Vec<u8>,
) -> Result<()> {
require!(init_data.is_empty(), TwapError::Parameters);
require!(
ctx.accounts.state.to_account_info().data_len() == HOSTED_STATE_LEN,
TwapError::State
);
let s = &mut ctx.accounts.state;
s.version = 3;
s.mint = mint;
s.allowance = 0;
s.remaining = 0;
s.grant = 0;
s.dust = 0;
s.last_at = 0;
s.credited_total = 0;
s.spent_total = 0;
s.service_total = 0;
s.burned_total = 0;
s.paid_steps = 0;
s.observations = 0;
s.drains = 0;
s.unassigned_buyback = 0;
s.royalty_debt = 0;
s.royalty_paid_total = 0;
s.royalty_service_reserve = 0;
Ok(())
}
pub fn companion_step<'info>(
ctx: Context<'info, CompanionStep<'info>>,
mint: Pubkey,
) -> Result<()> {
let now = Clock::get()?.unix_timestamp;
let creator = ctx.accounts.creator.key();
let launch = &ctx.accounts.launch;
let c = &ctx.accounts.companion;
require!(
(c.version == 1 || c.version == 2) && c.launched && c.is_script(),
TwapError::Profile
);
require!(ctx.accounts.state.version == 3, TwapError::State);
require_keys_eq!(launch.creator, creator, TwapError::Account);
require_keys_eq!(launch.mint, mint, TwapError::Account);
require_keys_eq!(launch.pool, ctx.accounts.pool.key(), TwapError::Account);
let infos = market::available(ctx.accounts, ctx.remaining_accounts);
let pool = market::validate(launch, &ctx.accounts.pool.to_account_info(), &infos)?;
// These accounts are mandatory even for a zero-cost wait. Otherwise an
// arbitrary caller could omit them and consume the native due clock.
let (terms, wallets_enabled) = payout_abi::terms(ctx.remaining_accounts, &mint)?;
let rent = Rent::get()?;
let recipient_minimum =
payout_abi::recipient_minimum(ctx.remaining_accounts, launch, &rent)?;
let royalty_enabled = wallets_enabled && recipient_minimum.is_some();
let min_payment = recipient_minimum.unwrap_or(0);
// Native sniper rates can differ from the preset. Wait without consuming
// state/cash; this does not assert hosted free polling or eventual service.
let sniper_end = launch
.created_at
.checked_add(launch.sniper_window_secs)
.ok_or(TwapError::Arithmetic)?;
if now < sniper_end {
set_return_data(&EMPTY_ANSWER);
return Ok(());
}
let old = ctx.accounts.state.book();
// Real reserve excludes virtual quote and segregated protocol fees.
// This is a trade-size bound, not a price floor or a universal MEV defense.
let max_buy = pool.quote_reserve / BUY_DEPTH_DIVISOR;
let mut plan = old
.plan(
c.pending_pot,
now,
max_buy,
&terms,
true,
true,
min_payment,
royalty_enabled,
)
.map_err(|e| {
msg!("TWAP royalty ledger rejected: {:?}", e);
error!(TwapError::Ledger)
})?;
let mut expected = None;
if plan.buy > 0 {
expected = market::quote_buy(&pool, plan.buy)?;
if expected.is_none() {
// Discard the prospective buy plan. OLD debt keeps its closed
// tail. Once that debt is zero, a previously started ledger may
// admit a separately funded later batch without moving grants.
plan = old
.plan(
c.pending_pot,
now,
max_buy,
&terms,
true,
false,
min_payment,
royalty_enabled,
)
.map_err(|e| {
msg!("TWAP royalty-only fallback rejected: {:?}", e);
error!(TwapError::Ledger)
})?;
let credit_allowed = match plan.action {
royalty::Action::RoyaltyIntake => {
old.book.total_credits > 0
&& old.book.royalty_debt == 0
&& plan.new_credit > 0
&& plan.new_royalty > 0
&& plan.new_credit >= plan.new_royalty
&& plan.new_credit - plan.new_royalty >= royalty::MIN_NEW_BUYBACK_INTAKE
&& plan.royalty > 0
&& plan.bounty == royalty::WALLET_BOUNTY
}
royalty::Action::Idle | royalty::Action::RoyaltyTail => {
plan.new_credit == 0 && plan.new_royalty == 0
}
_ => false,
};
require!(
plan.buy == 0
&& credit_allowed
&& plan.next.book.ledger.allowance == old.book.ledger.allowance
&& plan.next.book.ledger.remaining == old.book.ledger.remaining
&& plan.next.book.ledger.grant == old.book.ledger.grant
&& plan.next.book.ledger.dust == old.book.ledger.dust,
TwapError::Ledger
);
}
}
if plan.bounty == 0 {
set_return_data(&EMPTY_ANSWER);
return Ok(());
}
require!(royalty_enabled || plan.royalty == 0, TwapError::Ledger);
// A structurally valid empty system wallet is allowed when this actual
// payout makes it rent funded. Never require an unrelated prefunding.
payout_abi::validate_payment(ctx.remaining_accounts, &rent, plan.royalty)?;
let mut burned = 0;
if plan.buy > 0 {
let expected = expected.ok_or(TwapError::Settlement)?;
burned = market::buy_and_burn(launch, &pool, creator, plan.buy, expected, &infos)?;
}
let s = &mut ctx.accounts.state;
s.allowance = plan.next.book.ledger.allowance;
s.remaining = plan.next.book.ledger.remaining;
s.grant = plan.next.book.ledger.grant;
s.dust = plan.next.book.ledger.dust;
s.last_at = plan.next.book.ledger.last_at;
s.credited_total = plan.next.book.total_credits;
s.spent_total = plan.next.book.buyback_spent;
s.service_total = plan.next.book.service_spent;
s.unassigned_buyback = plan.next.book.unassigned_buyback;
s.royalty_debt = plan.next.book.royalty_debt;
s.royalty_paid_total = plan.next.book.royalty_paid;
s.royalty_service_reserve = plan.next.reserve;
s.burned_total = s
.burned_total
.checked_add(burned)
.ok_or(TwapError::Arithmetic)?;
s.paid_steps = s.paid_steps.checked_add(1).ok_or(TwapError::Arithmetic)?;
if matches!(plan.action, royalty::Action::Base(ledger::Kind::Observe)) {
s.observations = s.observations.checked_add(1).ok_or(TwapError::Arithmetic)?;
}
if matches!(plan.action, royalty::Action::Base(ledger::Kind::Drain)) {
s.drains = s.drains.checked_add(1).ok_or(TwapError::Arithmetic)?;
}
// Parent performs the actual native wallet payout after checking custody.
// Its failure rolls back this entire buy/state transition. Authenticated
// cap saturation can defer royalties; malformed account lists cannot.
// Clear any CPI return data and make this program own the final answer.
if plan.royalty > 0 {
set_return_data(&payout_abi::answer(plan.royalty));
} else {
set_return_data(&[]);
}
Ok(())
}
}
#[account]
#[derive(InitSpace)]
pub struct TwapState {
pub version: u8,
pub mint: Pubkey,
pub allowance: u64,
pub remaining: u64,
pub grant: u64,
pub dust: u64,
pub last_at: i64,
/// Actual positive pending_pot credits accepted, not an assertion of fee provenance.
pub credited_total: u64,
pub spent_total: u64,
pub service_total: u64,
pub burned_total: u64,
pub paid_steps: u64,
pub observations: u64,
pub drains: u64,
/// Already-credited buyback cash outside the scheduled grants.
pub unassigned_buyback: u64,
/// Physically backed unpaid author allocation; never available to buybacks.
pub royalty_debt: u64,
pub royalty_paid_total: u64,
pub royalty_service_reserve: u64,
}
impl TwapState {
pub const LEN: usize = STATE_LEN;
pub fn book(&self) -> royalty::TailBook {
royalty::TailBook {
book: royalty::Book {
ledger: self.ledger(),
unassigned_buyback: self.unassigned_buyback,
royalty_debt: self.royalty_debt,
total_credits: self.credited_total,
royalty_paid: self.royalty_paid_total,
buyback_spent: self.spent_total,
service_spent: self.service_total,
},
reserve: self.royalty_service_reserve,
}
}
pub fn ledger(&self) -> ledger::Ledger {
ledger::Ledger {
allowance: self.allowance,
remaining: self.remaining,
grant: self.grant,
dust: self.dust,
last_at: self.last_at,
}
}
}
#[derive(Accounts)]
#[instruction(mint:Pubkey)]
pub struct CompanionInit<'info> {
#[account(seeds=[b"creator",mint.as_ref()],bump,seeds::program=companion_abi::ID)]
pub creator: Signer<'info>,
/// CHECK: address only; mint does not exist at official script initialization.
#[account(address=mint)]
pub mint_account: UncheckedAccount<'info>,
#[account(zero,seeds=[b"script-state",mint.as_ref()],bump,seeds::program=companion_abi::ID)]
pub state: Box<Account<'info, TwapState>>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(mint:Pubkey)]
pub struct CompanionStep<'info> {
// The native outer ScriptStep binds its selected script program/approval;
// this signer PDA plus our program-owned state authenticates that route.
// Companion itself need not contain or forward a script_program field.
#[account(mut,seeds=[b"creator",mint.as_ref()],bump,seeds::program=companion_abi::ID)]
pub creator: Signer<'info>,
/// CHECK: canonical mint; validated from token-owned account in market module.
#[account(address=mint)]
pub mint_account: UncheckedAccount<'info>,
#[account(address=bordrless_launch::client::launch_address(&mint))]
pub launch: Box<Account<'info, bordrless_launch::state::Launch>>,
#[account(seeds=[b"companion",mint.as_ref()],bump=companion.bump,
seeds::program=companion_abi::ID,constraint=companion.mint==mint)]
pub companion: Box<Account<'info, Companion>>,
/// CHECK: canonical address and owner/layout authenticated in handler.
pub pool: UncheckedAccount<'info>,
#[account(mut,seeds=[b"script-state",mint.as_ref()],bump,
seeds::program=companion_abi::ID,constraint=state.mint==mint)]
pub state: Box<Account<'info, TwapState>>,
}
#[error_code]
pub enum TwapError {
#[msg("Expected empty immutable preset parameters")]
Parameters,
#[msg("Wrong account, owner, address or authority")]
Account,
#[msg("Unsupported market profile")]
Profile,
#[msg("Invalid script state")]
State,
#[msg("Cash ledger rejected state, time or backing")]
Ledger,
#[msg("Checked arithmetic failed")]
Arithmetic,
#[msg("Missing CPI account or privilege")]
MissingAccount,
#[msg("Measured native settlement differs from expected full-input trade")]
Settlement,
#[msg("Missing or incompatible native wallet payout terms")]
PayoutTerms,
#[msg("Invalid royalty wallet or actual payment cannot fund rent")]
PayoutRecipient,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_hosted_layout_fits() {
assert_eq!(8 + TwapState::INIT_SPACE, STATE_LEN);
assert!(STATE_LEN < HOSTED_STATE_LEN);
}
}
keeper.json11 lines ›
{
"version": 1,
"holdings": null,
"swapAccounts": true,
"wallets": [
"HZ32BY2FGHJrFDxjJRPNncW8BsWNxqxMqQPhFTypwBJ6"
],
"stepSecs": 60,
"stateLen": 1024
}
src/ledger.rs238 lines ›
//! RC3 canonical cash ledger: terminal Drain may use already-credited allowance and dust.
//!
//! A plan only describes a transition. The caller must execute its spend and
//! bounty transactionally before committing `next`. Unobserved cash is admitted
//! only by a funded observation or a due tick; it never funds the current grant.
#![forbid(unsafe_code)]
pub const CAP: u64 = 480;
pub const INTERVAL_SECONDS: i64 = 60;
pub const CALL_COST: u64 = 20_000;
pub const MIN_BUY: u64 = 2_000_000;
pub const MIN_GRANT: u64 = 400_000;
pub const MIN_FUTURE_BUDGET: u64 = 201_600_000;
pub const MIN_ADMISSION: u64 = 201_620_000;
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
pub struct Ledger {
pub allowance: u64,
pub remaining: u64,
pub grant: u64,
pub dust: u64,
pub last_at: i64,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Kind {
Idle,
Observe,
Tick,
Drain,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Plan {
pub next: Ledger,
pub spend: u64,
pub bounty: u64,
pub credited: u64,
pub kind: Kind,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Error {
InvalidState,
InvalidTime,
TimeReversal,
NotDue,
ArithmeticOverflow,
InsufficientBacking,
InvariantViolation,
}
impl Ledger {
/// Cash already assigned to allowance, future successful calls, and dust.
pub fn backing(&self) -> Result<u64, Error> {
let per_slot = add(self.grant, CALL_COST)?;
add(
add(self.allowance, mul(self.remaining, per_slot)?)?,
self.dust,
)
}
/// Propose a transition without changing this ledger, including on error.
/// `max_buy` is a caller-supplied spending ceiling, in quote lamports.
pub fn plan(
&self,
actual_pending_pot: u64,
now: i64,
max_buy: u64,
) -> Result<Plan, Error> {
self.validate()?;
if now < 0 || self.last_at < 0 {
return Err(Error::InvalidTime);
}
if now < self.last_at {
return Err(Error::TimeReversal);
}
let old_backing = self.backing()?;
let delta = actual_pending_pot
.checked_sub(old_backing)
.ok_or(Error::InsufficientBacking)?;
if self.remaining == 0 {
// Existing allowance is owned by prior grants, not restart capital.
let available = actual_pending_pot
.checked_sub(self.allowance)
.ok_or(Error::InsufficientBacking)?;
if available < MIN_ADMISSION {
// No future slots remain: OLD allowance and credited dust can
// fund this purchase. Fresh delta stays unassigned. Consume
// allowance first, preserving any untouched dust classification.
let terminal_cash = old_backing;
if terminal_cash >= add(MIN_BUY, CALL_COST)? && max_buy >= MIN_BUY {
let due_at = self
.last_at
.checked_add(INTERVAL_SECONDS)
.ok_or(Error::ArithmeticOverflow)?;
if now < due_at {
return Err(Error::NotDue);
}
let after_cost = sub(terminal_cash, CALL_COST)?;
let spend = after_cost.min(max_buy);
let remainder = sub(after_cost, spend)?;
let dust = self.dust.min(remainder);
let next = Self {
allowance: sub(remainder, dust)?,
dust,
last_at: now,
..*self
};
return Self::funded_plan(next, old_backing, spend, 0, Kind::Drain);
}
return Ok(Plan {
next: *self,
spend: 0,
bounty: 0,
credited: 0,
kind: Kind::Idle,
});
}
// A previously funded ledger retains its successful-call clock.
// An underfunded idle probe above neither charges nor advances it.
if self.grant > 0 {
let due_at = self
.last_at
.checked_add(INTERVAL_SECONDS)
.ok_or(Error::ArithmeticOverflow)?;
if now < due_at {
return Err(Error::NotDue);
}
}
let future = sub(available, CALL_COST)?;
let per_slot = future / CAP;
let grant = sub(per_slot, CALL_COST)?;
let next = Self {
allowance: self.allowance,
remaining: CAP,
grant,
dust: sub(future, mul(CAP, per_slot)?)?,
last_at: now,
};
return Self::funded_plan(next, old_backing, 0, delta, Kind::Observe);
}
let due_at = self
.last_at
.checked_add(INTERVAL_SECONDS)
.ok_or(Error::ArithmeticOverflow)?;
if now < due_at {
return Err(Error::NotDue);
}
// Release exactly one OLD grant, even after a long elapsed-time gap.
let mut next = *self;
next.remaining = sub(next.remaining, 1)?;
next.allowance = add(next.allowance, self.grant)?;
next.last_at = now;
let spend = if next.allowance >= MIN_BUY && max_buy >= MIN_BUY {
let amount = next.allowance.min(max_buy);
next.allowance = sub(next.allowance, amount)?;
amount
} else {
0
};
// New cash changes future slots only. Allowance is deliberately absent.
let old_per_slot = add(self.grant, CALL_COST)?;
let future = add(add(mul(next.remaining, old_per_slot)?, self.dust)?, delta)?;
let affordable = future / old_per_slot;
if affordable <= CAP {
next.remaining = affordable;
next.dust = sub(future, mul(affordable, old_per_slot)?)?;
} else {
let per_slot = future / CAP;
next.remaining = CAP;
next.grant = sub(per_slot, CALL_COST)?;
next.dust = sub(future, mul(CAP, per_slot)?)?;
if next.grant < self.grant {
return Err(Error::InvariantViolation);
}
}
Self::funded_plan(next, old_backing, spend, delta, Kind::Tick)
}
fn validate(&self) -> Result<(), Error> {
if self.remaining > CAP
|| (self.remaining > 0 && self.grant < MIN_GRANT)
|| (self.grant == 0 && (self.remaining != 0 || self.dust != 0))
{
return Err(Error::InvalidState);
}
// All states produced here retain less than one slot's cost as dust.
if self.dust >= add(self.grant, CALL_COST)? {
return Err(Error::InvalidState);
}
Ok(())
}
fn funded_plan(
next: Self,
old_backing: u64,
spend: u64,
credited: u64,
kind: Kind,
) -> Result<Plan, Error> {
next.validate()?;
let expected = sub(sub(add(old_backing, credited)?, CALL_COST)?, spend)?;
if next.backing()? != expected {
return Err(Error::InvariantViolation);
}
Ok(Plan {
next,
spend,
bounty: CALL_COST,
credited,
kind,
})
}
}
fn add(a: u64, b: u64) -> Result<u64, Error> {
a.checked_add(b).ok_or(Error::ArithmeticOverflow)
}
fn sub(a: u64, b: u64) -> Result<u64, Error> {
a.checked_sub(b).ok_or(Error::ArithmeticOverflow)
}
fn mul(a: u64, b: u64) -> Result<u64, Error> {
a.checked_mul(b).ok_or(Error::ArithmeticOverflow)
}
src/market.rs280 lines ›
//! Fixed-pair native market adapter; exact integer quote and authenticated burn receipt.
use crate::{
pool_abi::{self, Pool},
receipt, TwapError,
};
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{instruction::Instruction, program::invoke};
use bordrless_launch::{
client as launch_client,
state::{Launch, LaunchRules},
};
use bordrless_token::{
client as token,
state::{Holding, Mint},
};
pub const QUOTE_MINT: Pubkey =
Pubkey::from_str_const("A49oVhX22ExMwTEtFC6Y8nhBdZ4LJDGhdXLDn4c2f59i");
// Launch keeps these protocol extensions in its existing 22 reserved bytes.
// Read the authenticated Launch directly so older builder crates remain compatible:
// [0..2] buy cap, [2..4] sell cap, [14..16] partner share, all little-endian u16.
// Match the native readers exactly, including None when both caps are zero.
fn launch_partner_share_bps(l: &Launch) -> u16 {
u16::from_le_bytes([l.reserved[14], l.reserved[15]])
}
fn launch_quote_cut(l: &Launch) -> Option<(Pubkey, u16, u16)> {
let buy = u16::from_le_bytes([l.reserved[0], l.reserved[1]]);
let sell = u16::from_le_bytes([l.reserved[2], l.reserved[3]]);
match l.custom_hook {
Some(hook) if buy > 0 || sell > 0 => Some((hook, buy, sell)),
_ => None,
}
}
pub fn available<'info>(
a: &impl ToAccountInfos<'info>,
r: &[AccountInfo<'info>],
) -> Vec<AccountInfo<'info>> {
let mut v = a.to_account_infos();
v.extend_from_slice(r);
v
}
fn find<'a, 'info>(v: &'a [AccountInfo<'info>], key: &Pubkey) -> Result<&'a AccountInfo<'info>> {
v.iter()
.find(|a| a.key == key)
.ok_or_else(|| error!(TwapError::MissingAccount))
}
fn mint(info: &AccountInfo) -> Result<Mint> {
require_keys_eq!(*info.owner, bordrless_token::ID, TwapError::Account);
Ok(Mint::try_deserialize(&mut &info.try_borrow_data()?[..])?)
}
fn held(v: &[AccountInfo], m: Pubkey, owner: Pubkey) -> Result<u64> {
let info = find(v, &token::holding_address(&m, &owner))?;
require_keys_eq!(*info.owner, bordrless_token::ID, TwapError::Account);
let h = Holding::try_deserialize(&mut &info.try_borrow_data()?[..])?;
require!(
h.version == 1
&& h.mint == m
&& h.owner == owner
&& !h.frozen
&& h.delegate.is_none()
&& h.delegated_amount == 0,
TwapError::Account
);
Ok(h.amount)
}
pub fn validate(l: &Launch, info: &AccountInfo, all: &[AccountInfo]) -> Result<Box<Pool>> {
require_keys_eq!(*info.owner, pool_abi::ID, TwapError::Account);
require_keys_eq!(
info.key(),
launch_client::pool_address(&l.mint, &l.quote_mint, l.lp_fee_bps),
TwapError::Account
);
let p = Box::new(Pool::try_deserialize(&mut &info.try_borrow_data()?[..])?);
let hook = l.custom_hook.ok_or(TwapError::Profile)?;
require!(find(all, &hook)?.executable, TwapError::Account);
let receipt_info = find(all, &receipt::address(&hook, &l.mint).0)?;
receipt::read(receipt_info, &hook, &l.mint)?;
require!(
l.version == 1
&& l.lp_fee_bps == 30
&& l.creator_fee_bps == 200
&& l.rules == LaunchRules::NONE
&& l.modules == 0
&& l.config != Pubkey::default()
&& l.custom_hook_flags == receipt::HOOK_FLAGS
&& l.author_share_bps == 0
&& launch_partner_share_bps(l) == 0
&& launch_quote_cut(l) == Some((hook, 1, 780)),
TwapError::Profile
);
require!(
l.quote_mint == QUOTE_MINT
&& p.version == 1
&& p.base_mint == l.mint
&& p.quote_mint == QUOTE_MINT
&& p.lp_fee_bps == 30
&& p.protocol_fee_bps == 0
&& p.fee_model == 1
&& p.protocol_share_bps == 2500
&& p.hook_program == Some(bordrless_launch::ID)
&& p.hook_flags == bordrless_launch::constants::LAUNCH_HOOK_FLAGS,
TwapError::Profile
);
require!(
(l.status == 0 && p.curve)
|| (l.status == 1 && !p.curve && p.virtual_base == 0 && p.virtual_quote == 0),
TwapError::Profile
);
let base = mint(find(all, &l.mint)?)?;
let quote = mint(find(all, "E_MINT)?)?;
require!(
base.version == 1
&& base.hook_program == Some(hook)
&& base.hook_flags == receipt::HOOK_FLAGS
&& base.hook_authority.is_none()
&& base.mint_authority.is_none()
&& base.freeze_authority.is_none(),
TwapError::Profile
);
require!(
quote.version == 1
&& quote.hook_program.is_none()
&& quote.hook_flags == 0
&& quote.hook_authority.is_none()
&& quote.freeze_authority.is_none(),
TwapError::Profile
);
require_keys_eq!(
p.base_vault,
token::holding_address(&l.mint, info.key),
TwapError::Account
);
require_keys_eq!(
p.quote_vault,
token::holding_address("E_MINT, info.key),
TwapError::Account
);
let b = held(all, l.mint, info.key());
let q = held(all, QUOTE_MINT, info.key());
require!(
b? >= p.base_reserve
&& q?
>= p.quote_reserve
.checked_add(p.protocol_fees_quote)
.ok_or(TwapError::Arithmetic)?,
TwapError::Settlement
);
// Creator's balance includes other native reserved splits. It is NEVER the budget.
held(all, l.mint, l.creator)?;
held(all, QUOTE_MINT, l.creator)?;
Ok(p)
}
fn fee(x: u64, bps: u64) -> Result<u64> {
let n = u128::from(x)
.checked_mul(u128::from(bps))
.ok_or(TwapError::Arithmetic)?;
u64::try_from(n.checked_add(9999).ok_or(TwapError::Arithmetic)? / 10000)
.map_err(|_| error!(TwapError::Arithmetic))
}
/// None means no positive full-input output within the real reserve; caller defers.
pub fn quote_buy(p: &Pool, gross: u64) -> Result<Option<u64>> {
let creator = fee(gross, 200)?;
let received = gross.checked_sub(creator).ok_or(TwapError::Arithmetic)?;
let lp = fee(received, 30)?;
let share = fee(creator, 2500)?;
let net = received
.checked_sub(lp)
.and_then(|x| x.checked_sub(share))
.ok_or(TwapError::Arithmetic)?;
if net == 0 {
return Ok(None);
}
let x = u128::from(p.quote_reserve)
.checked_add(u128::from(p.virtual_quote))
.ok_or(TwapError::Arithmetic)?;
let y = u128::from(p.base_reserve)
.checked_add(u128::from(p.virtual_base))
.ok_or(TwapError::Arithmetic)?;
let out = u128::from(net)
.checked_mul(y)
.ok_or(TwapError::Arithmetic)?
/ x.checked_add(u128::from(net))
.ok_or(TwapError::Arithmetic)?;
if out == 0 || out > u128::from(p.base_reserve) {
return Ok(None);
}
Ok(Some(
u64::try_from(out).map_err(|_| error!(TwapError::Arithmetic))?,
))
}
fn call<'info>(ix: &Instruction, all: &[AccountInfo<'info>]) -> Result<()> {
require!(
[pool_abi::ID, bordrless_token::ID].contains(&ix.program_id),
TwapError::Account
);
let program = find(all, &ix.program_id)?;
require!(program.executable, TwapError::Account);
let mut infos = ix
.accounts
.iter()
.map(|m| find(all, &m.pubkey).cloned())
.collect::<Result<Vec<_>>>()?;
infos.push(program.clone());
invoke(ix, &infos)?;
Ok(())
}
pub fn buy_and_burn<'info>(
l: &Launch,
_p: &Pool,
creator: Pubkey,
spend: u64,
expected: u64,
all: &[AccountInfo<'info>],
) -> Result<u64> {
require!(find(all, &l.mint)?.is_writable, TwapError::MissingAccount);
let before_base = held(all, l.mint, creator)?;
let before_quote = held(all, QUOTE_MINT, creator)?;
let before_supply = mint(find(all, &l.mint)?)?.supply;
let hook = l.custom_hook.ok_or(TwapError::Profile)?;
let receipt_key = receipt::address(&hook, &l.mint).0;
let receipt_info = find(all, &receipt_key)?;
let before_receipt = receipt::read(receipt_info, &hook, &l.mint)?;
// Match the paired hook registry exactly. The writable literal mint supplies
// required mint write permission without a native burn-fee setting.
let extras = vec![
AccountMeta::new(receipt_key, false),
AccountMeta::new(l.mint, false),
];
let ix = launch_client::swap_with_base_slice(
&launch_client::LaunchKeys::of(l),
creator,
creator,
1,
spend,
expected,
launch_client::custom_hook_slice(hook, extras.clone()),
);
call(&ix, all)?;
let acquired = held(all, l.mint, creator)?
.checked_sub(before_base)
.ok_or(TwapError::Settlement)?;
require!(
acquired == expected
&& acquired > 0
&& before_quote.checked_sub(held(all, QUOTE_MINT, creator)?) == Some(spend),
TwapError::Settlement
);
call(
&token::burn(
creator,
token::holding_address(&l.mint, &creator),
l.mint,
Some(hook),
extras,
acquired,
),
all,
)?;
require!(
held(all, l.mint, creator)? == before_base,
TwapError::Settlement
);
require!(
before_supply.checked_sub(mint(find(all, &l.mint)?)?.supply) == Some(acquired),
TwapError::Settlement
);
require!(
before_quote.checked_sub(held(all, QUOTE_MINT, creator)?) == Some(spend),
TwapError::Settlement
);
let after_supply = mint(find(all, &l.mint)?)?.supply;
let after_receipt = receipt::read(receipt_info, &hook, &l.mint)?;
receipt::check_record(&before_receipt, &after_receipt, acquired, after_supply)?;
Ok(acquired)
}
src/payout_abi.rs156 lines ›
//! Native wallet-payout ABI with mandatory accounts and explicit payment eligibility.
//! Malformed callers must fail so they cannot consume the parent's due clock.
use crate::{companion_abi, royalty, TwapError};
use anchor_lang::prelude::*;
use bordrless_launch::state::Launch;
pub const AUTHOR: Pubkey = Pubkey::from_str_const("HZ32BY2FGHJrFDxjJRPNncW8BsWNxqxMqQPhFTypwBJ6");
pub const TERMS_LEN: usize = 128;
pub const ROYALTY_TAG: u32 = 0x524f594c;
const BRIDGE: Pubkey = Pubkey::from_str_const("CtLkuFVitoXHTa86Hfp8KmfSDfqJaMYFWr6EGmQVsKb7");
#[account]
#[derive(InitSpace, Debug)]
pub struct PayoutTerms {
pub version: u8,
pub bump: u8,
pub mint: Pubkey,
pub step_bps: u16,
pub holding_bps: u16,
pub window_bps: u16,
pub window_secs: u32,
pub window_start: i64,
pub window_pot: u64,
pub window_paid: u64,
pub paid_total: u64,
pub payouts: u64,
pub last_paid_at: i64,
pub reserved: [u8; 28],
}
pub fn terms_address(mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[b"payout-terms", mint.as_ref()], &companion_abi::ID)
}
/// Exact release terms. The native parent remains the final payer.
pub fn terms(remaining: &[AccountInfo], mint: &Pubkey) -> Result<(royalty::Terms, bool)> {
let (key, bump) = terms_address(mint);
let info = remaining
.iter()
.find(|a| a.key == &key)
.ok_or(TwapError::MissingAccount)?;
require!(
*info.owner == companion_abi::ID
&& info.is_writable
&& !info.executable
&& info.data_len() == TERMS_LEN,
TwapError::PayoutTerms
);
let data = info.try_borrow_data()?;
let t =
PayoutTerms::try_deserialize(&mut &data[..]).map_err(|_| error!(TwapError::PayoutTerms))?;
require!(
t.version == 1
&& t.bump == bump
&& t.mint == *mint
&& t.reserved[0] <= 1
&& (t.step_bps, t.holding_bps, t.window_bps, t.window_secs)
== (1000, 10_000, 2500, 86_400),
TwapError::PayoutTerms
);
Ok((
royalty::Terms {
step_bps: t.step_bps,
holding_bps: t.holding_bps,
window_bps: t.window_bps,
window_secs: i64::from(t.window_secs),
window_start: t.window_start,
window_pot: t.window_pot,
window_paid: t.window_paid,
},
t.reserved[0] == 1,
))
}
/// Canonical presence is mandatory. An intrinsically payable account must also be
/// writable, so a caller cannot suppress royalties by downgrading it. Check intrinsic
/// incompatibility first because executable accounts may be readonly at runtime.
/// A valid new system wallet can be funded by its first payout.
pub fn recipient_minimum(
remaining: &[AccountInfo],
l: &Launch,
rent: &Rent,
) -> Result<Option<u64>> {
let to = remaining
.iter()
.find(|a| *a.key == AUTHOR)
.ok_or(TwapError::MissingAccount)?;
let pda =
|seed: &[u8]| Pubkey::find_program_address(&[seed, l.mint.as_ref()], &companion_abi::ID).0;
let excluded = [
bordrless_launch::client::launch_address(&l.mint),
l.pool,
l.creator,
pda(b"companion"),
crate::ID,
pda(b"script"),
pda(b"script-state"),
terms_address(&l.mint).0,
l.custom_hook.unwrap_or_default(),
l.mint,
Pubkey::find_program_address(&[b"payout-authority"], &companion_abi::ID).0,
Pubkey::find_program_address(&[b"__event_authority"], &companion_abi::ID).0,
Pubkey::find_program_address(&[b"sol-vault"], &BRIDGE).0,
Pubkey::default(),
];
require!(!excluded.contains(&AUTHOR), TwapError::PayoutRecipient);
if to.executable || *to.owner != anchor_lang::system_program::ID || !to.data_is_empty() {
return Ok(None);
}
require!(to.is_writable, TwapError::PayoutRecipient);
Ok(Some(rent.minimum_balance(0).saturating_sub(to.lamports())))
}
/// Check the actual selected amount, not a hypothetical maximum payment. The
/// parent applies this same post-payment rent condition after the script returns.
pub fn validate_payment(remaining: &[AccountInfo], rent: &Rent, amount: u64) -> Result<()> {
if amount == 0 {
return Ok(());
}
let to = remaining
.iter()
.find(|a| *a.key == AUTHOR)
.ok_or(TwapError::MissingAccount)?;
let funded = to
.lamports()
.checked_add(amount)
.is_some_and(|balance| balance >= rent.minimum_balance(to.data_len()));
require!(funded, TwapError::PayoutRecipient);
Ok(())
}
/// Exactly the native version-2 Borsh answer: one actual SOL recipient, no dummy entries.
pub fn answer(lamports: u64) -> Vec<u8> {
let mut bytes = Vec::with_capacity(49);
bytes.push(2);
bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.extend_from_slice(AUTHOR.as_ref());
bytes.extend_from_slice(&lamports.to_le_bytes());
bytes.extend_from_slice(&ROYALTY_TAG.to_le_bytes());
bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_layout_and_wallet_answer() {
assert_eq!(8 + PayoutTerms::INIT_SPACE, TERMS_LEN);
let a = answer(123);
assert_eq!(a.len(), 49);
assert_eq!(&a[..5], &[2, 1, 0, 0, 0]);
assert_eq!(&a[5..37], AUTHOR.as_ref());
assert_eq!(&a[37..45], &123u64.to_le_bytes());
}
}
src/pool_abi.rs69 lines ›
//! Pinned exact Pool ABI mirror; host parity tests compare native source.
use anchor_lang::prelude::*;
use anchor_lang::Discriminator;
pub const ID: Pubkey = Pubkey::from_str_const("GyzKSnnEu2uN5bBRecE4XYY2enbfR2D2MtxnbJPGy7hk");
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct Pool {
pub version: u8,
pub bump: u8,
pub lp_mint_bump: u8,
pub base_mint: Pubkey,
pub quote_mint: Pubkey,
pub lp_mint: Pubkey,
pub base_vault: Pubkey,
pub quote_vault: Pubkey,
pub hook_program: Option<Pubkey>,
pub hook_flags: u16,
pub lp_fee_bps: u16,
pub protocol_fee_bps: u16,
pub base_reserve: u64,
pub quote_reserve: u64,
pub virtual_base: u64,
pub virtual_quote: u64,
pub lp_supply: u64,
pub protocol_fees_quote: u64,
pub curve: bool,
pub creator: Pubkey,
pub created_at: i64,
pub last_swap_at: i64,
pub swap_count: u64,
pub base_volume: u128,
pub quote_volume: u128,
pub hook_signer_bump: u8,
pub fee_model: u8,
pub protocol_share_bps: u16,
pub reserved: [u8; 60],
}
impl anchor_lang::Discriminator for Pool {
const DISCRIMINATOR: &'static [u8] = &[241, 154, 109, 4, 17, 177, 109, 188];
}
impl AccountDeserialize for Pool {
fn try_deserialize(buf: &mut &[u8]) -> Result<Self> {
if buf.len() < 8 || &buf[..8] != Self::DISCRIMINATOR {
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into());
}
Self::try_deserialize_unchecked(buf)
}
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self> {
if buf.len() < 8 {
return Err(anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into());
}
Self::deserialize(&mut &buf[8..])
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
}
}
impl AccountSerialize for Pool {
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
writer
.write_all(Self::DISCRIMINATOR)
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?;
self.serialize(writer)
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize.into())
}
}
impl Owner for Pool {
fn owner() -> Pubkey {
ID
}
}
src/receipt.rs111 lines ›
//! The paired receipt ABI. Runtime identity comes from the authenticated Launch.
//! This verifies the actual hook/state used, not a compile-time assigned pair ID.
use crate::TwapError;
use anchor_lang::prelude::*;
pub const HOOK_FLAGS: u16 = 32; // AFTER_BURN only; no transfer callback or cuts.
pub const STATE_LEN: usize = 82;
#[account]
#[derive(InitSpace, Debug, PartialEq, Eq)]
pub struct ReceiptState {
pub version: u8,
pub bump: u8,
pub mint: Pubkey,
pub total_burned: u128,
pub companion_origin_burned: u128,
pub latest_observed_supply: u64,
}
pub fn address(hook: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[b"state", mint.as_ref()], hook)
}
pub fn read(info: &AccountInfo, hook: &Pubkey, mint: &Pubkey) -> Result<ReceiptState> {
let (expected, bump) = address(hook, mint);
require_keys_eq!(info.key(), expected, TwapError::Account);
require_keys_eq!(*info.owner, *hook, TwapError::Account);
require!(
info.is_writable && info.data_len() == STATE_LEN,
TwapError::MissingAccount
);
let state = ReceiptState::try_deserialize(&mut &info.try_borrow_data()?[..])?;
require!(
state.version == 1
&& state.bump == bump
&& state.mint == *mint
&& state.companion_origin_burned <= state.total_burned,
TwapError::Account
);
Ok(state)
}
pub fn check_record(
before: &ReceiptState,
after: &ReceiptState,
burned: u64,
supply: u64,
) -> Result<()> {
require!(
after.version == before.version
&& after.bump == before.bump
&& after.mint == before.mint
&& Some(after.total_burned) == before.total_burned.checked_add(u128::from(burned))
&& Some(after.companion_origin_burned)
== before
.companion_origin_burned
.checked_add(u128::from(burned))
&& after.latest_observed_supply == supply,
TwapError::Settlement
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn receipt_layout_is_exact() {
assert_eq!(8 + ReceiptState::INIT_SPACE, STATE_LEN);
let s = ReceiptState {
version: 1,
bump: 2,
mint: Pubkey::new_unique(),
total_burned: 3,
companion_origin_burned: 4,
latest_observed_supply: 5,
};
let mut bytes = Vec::new();
s.try_serialize(&mut bytes).unwrap();
assert_eq!(bytes.len(), STATE_LEN);
assert_eq!(&bytes[42..58], &3u128.to_le_bytes());
assert_eq!(&bytes[58..74], &4u128.to_le_bytes());
assert_eq!(&bytes[74..82], &5u64.to_le_bytes());
}
#[test]
fn observed_burn_must_match_both_counters_and_supply() {
let a = ReceiptState {
version: 1,
bump: 2,
mint: Pubkey::new_unique(),
total_burned: 100,
companion_origin_burned: 40,
latest_observed_supply: 1000,
};
let mut b = ReceiptState {
version: 1,
bump: 2,
mint: a.mint,
total_burned: 109,
companion_origin_burned: 49,
latest_observed_supply: 991,
};
assert!(check_record(&a, &b, 9, 991).is_ok());
b.companion_origin_burned = 40;
assert!(check_record(&a, &b, 9, 991).is_err());
b.companion_origin_burned = 49;
b.latest_observed_supply = 1000;
assert!(check_record(&a, &b, 9, 991).is_err());
}
}
src/royalty.rs473 lines ›
//! RC10: funded buybacks continue while authenticated wallet payment is ineligible.
//! Plans commit only with the native swap/payout/bounty transaction. Account
//! authentication and return-data delivery remain adapter responsibilities.
use crate::ledger::{self, Kind, Ledger, CALL_COST};
pub const WALLET_BOUNTY: u64 = 2500;
pub const DENOMINATOR: u64 = 10000;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Book {
pub ledger: Ledger,
pub unassigned_buyback: u64,
pub royalty_debt: u64,
pub total_credits: u64,
pub royalty_paid: u64,
pub buyback_spent: u64,
pub service_spent: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Terms {
pub step_bps: u16,
pub holding_bps: u16,
pub window_bps: u16,
pub window_secs: i64,
pub window_start: i64,
pub window_pot: u64,
pub window_paid: u64,
}
pub const RESERVE: u64 = 512 * WALLET_BOUNTY;
/// New80% cash must cover a full reserve and this call, even if old cash exists.
pub const MIN_NEW_BUYBACK_INTAKE: u64 = RESERVE + WALLET_BOUNTY;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TailBook {
pub book: Book,
pub reserve: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
Idle,
Base(Kind),
RoyaltyTail,
RoyaltyIntake,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Error {
Arithmetic,
Invalid,
MissingSupport,
Terms,
ShortReserve,
Ledger(ledger::Error),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Plan {
pub next: TailBook,
pub action: Action,
pub buy: u64,
pub royalty: u64,
pub bounty: u64,
pub new_credit: u64,
pub new_royalty: u64,
pub reserve_topup: u64,
pub reserve_release: u64,
pub service_release: u64,
pub terms: Terms,
}
fn add(a: u64, b: u64) -> Result<u64, Error> {
a.checked_add(b).ok_or(Error::Arithmetic)
}
fn sub(a: u64, b: u64) -> Result<u64, Error> {
a.checked_sub(b).ok_or(Error::Arithmetic)
}
impl TailBook {
pub fn backing(&self) -> Result<u64, Error> {
add(
add(
add(
self.book.ledger.backing().map_err(Error::Ledger)?,
self.book.unassigned_buyback,
)?,
self.book.royalty_debt,
)?,
self.reserve,
)
}
pub fn validate(&self) -> Result<(), Error> {
let b = &self.book;
let total = self.backing()? as u128
+ b.royalty_paid as u128
+ b.buyback_spent as u128
+ b.service_spent as u128;
if total != b.total_credits as u128
|| b.royalty_paid as u128 + b.royalty_debt as u128 != (b.total_credits / 5) as u128
|| self.reserve > RESERVE
{
return Err(Error::Invalid);
}
Ok(())
}
fn idle(&self, t: &Terms) -> Plan {
Plan {
next: *self,
action: Action::Idle,
buy: 0,
royalty: 0,
bounty: 0,
new_credit: 0,
new_royalty: 0,
reserve_topup: 0,
reserve_release: 0,
service_release: 0,
terms: *t,
}
}
/// `buy_executable` is the adapter's authenticated full-input preflight.
/// Missing mandatory accounts must revert the whole native outer call.
pub fn plan(
&self,
pot: u64,
now: i64,
cap: u64,
t: &Terms,
support: bool,
buy_executable: bool,
min_payment: u64,
royalty_enabled: bool,
) -> Result<Plan, Error> {
self.validate()?;
if !support {
return Err(Error::MissingSupport);
}
if (t.step_bps, t.holding_bps, t.window_bps, t.window_secs) != (1000, 10000, 2500, 86400) {
return Err(Error::Terms);
}
let fresh = sub(pot, self.backing()?)?;
// An unrecognizable lifetime-credit overflow cannot freeze OLD debt.
// Leave such positive cash unseen and use only previously backed state.
if buy_executable && self.book.total_credits.checked_add(fresh).is_some() {
if let Some(p) = self.base_plan(
pot,
now,
cap,
t,
buy_executable,
fresh,
true,
min_payment,
royalty_enabled,
)? {
return Ok(p);
}
}
// Old Tick/Drain cash is already funded and may progress without new
// entitlement or a reserve refill. Observe still requires full reserve.
if buy_executable {
if let Some(p) = self.base_plan(
pot,
now,
cap,
t,
true,
0,
false,
min_payment,
royalty_enabled,
)? {
return Ok(p);
}
}
// No payable wallet means no royalty-only work. Leave all debt, reserve,
// fresh cash and cadence untouched; the adapter returns an empty answer.
if !royalty_enabled {
return Ok(self.idle(t));
}
let r = self.book.royalty_debt;
if r == 0 {
return self.intake(pot, now, t, fresh, min_payment);
}
if self.reserve < WALLET_BOUNTY {
return Err(Error::ShortReserve);
}
let due = self
.book
.ledger
.last_at
.checked_add(60)
.ok_or(Error::Arithmetic)?;
if now < due {
return Err(Error::Ledger(ledger::Error::NotDue));
}
let (budget, start, anchor, paid) =
t.wallet_budget(pot, 0, now).map_err(|_| Error::Terms)?;
let y = r.min(budget);
// Native step fraction also supplies the tail's batching floor. Tiny
// leftover daily quotas wait without consuming reserve or advancing L.
let minimum = (r / 10 + u64::from(r % 10 != 0)).max(min_payment);
if y < minimum {
return Ok(self.idle(t));
}
let mut next = *self;
next.reserve = sub(next.reserve, WALLET_BOUNTY)?;
next.book.royalty_debt = sub(r, y)?;
next.book.royalty_paid = add(next.book.royalty_paid, y)?;
next.book.service_spent = add(next.book.service_spent, WALLET_BOUNTY)?;
next.book.ledger.last_at = now; // Cadence only; no grant consumed or released.
let release = next.release_if_finished()?;
next.validate()?;
// All fresh pot cash remains completely unobserved in a tail call.
if sub(sub(sub(pot, y)?, WALLET_BOUNTY)?, next.backing()?)? != fresh {
return Err(Error::Invalid);
}
Ok(Plan {
next,
action: Action::RoyaltyTail,
buy: 0,
royalty: y,
bounty: WALLET_BOUNTY,
new_credit: 0,
new_royalty: 0,
reserve_topup: 0,
reserve_release: release,
service_release: 0,
terms: Terms {
window_start: start,
window_pot: anchor,
window_paid: add(paid, y)?,
..*t
},
})
}
/// An intake never interrupts recognized debt settlement or buys tokens.
/// Its reserve and bounty are funded solely by this NEW batch's80% share.
fn intake(
&self,
pot: u64,
now: i64,
t: &Terms,
fresh: u64,
min_payment: u64,
) -> Result<Plan, Error> {
// Initial activation must use the original funded base admission. This
// preserves the startup payout-window anchor and requires no new flag:
// in a fresh book only ordinary Observe can first make F positive.
if self.book.total_credits == 0 {
return Ok(self.idle(t));
}
let Some(f) = self.book.total_credits.checked_add(fresh) else {
return Ok(self.idle(t));
};
let new_royalty = sub(f / 5, self.book.total_credits / 5)?;
let new_buyback = sub(fresh, new_royalty)?;
if self.book.royalty_debt != 0 || new_buyback < MIN_NEW_BUYBACK_INTAKE {
return Ok(self.idle(t));
}
let (budget, start, anchor, paid) =
t.wallet_budget(pot, 0, now).map_err(|_| Error::Terms)?;
let y = new_royalty.min(budget);
if y == 0 || y < min_payment {
return Ok(self.idle(t));
}
let due = self
.book
.ledger
.last_at
.checked_add(60)
.ok_or(Error::Arithmetic)?;
if now < due {
return Err(Error::Ledger(ledger::Error::NotDue));
}
let topup = sub(RESERVE, self.reserve)?;
let new_unassigned = sub(sub(new_buyback, topup)?, WALLET_BOUNTY)?;
let mut next = *self;
next.reserve = RESERVE;
next.book.total_credits = f;
next.book.unassigned_buyback = add(next.book.unassigned_buyback, new_unassigned)?;
next.book.royalty_debt = sub(new_royalty, y)?;
next.book.royalty_paid = add(next.book.royalty_paid, y)?;
next.book.service_spent = add(next.book.service_spent, WALLET_BOUNTY)?;
next.book.ledger.last_at = now;
let release = next.release_if_finished()?;
next.validate()?;
if next.backing()? != sub(sub(pot, y)?, WALLET_BOUNTY)? {
return Err(Error::Invalid);
}
Ok(Plan {
next,
action: Action::RoyaltyIntake,
buy: 0,
royalty: y,
bounty: WALLET_BOUNTY,
new_credit: fresh,
new_royalty,
reserve_topup: topup,
reserve_release: release,
service_release: 0,
terms: Terms {
window_start: start,
window_pot: anchor,
window_paid: add(paid, y)?,
..*t
},
})
}
fn base_plan(
&self,
pot: u64,
now: i64,
cap: u64,
t: &Terms,
buy_executable: bool,
fresh: u64,
recognize: bool,
min_payment: u64,
royalty_enabled: bool,
) -> Result<Option<Plan>, Error> {
let mut next = *self;
let f = add(next.book.total_credits, fresh)?;
let royalty = sub(f / 5, next.book.total_credits / 5)?;
next.book.total_credits = f;
next.book.royalty_debt = add(next.book.royalty_debt, royalty)?;
next.book.unassigned_buyback = add(next.book.unassigned_buyback, sub(fresh, royalty)?)?;
let topup = if recognize {
sub(RESERVE, next.reserve)?
} else {
0
};
if next.book.unassigned_buyback < topup {
return Ok(None);
}
next.book.unassigned_buyback -= topup;
next.reserve += topup;
let cash = add(
next.book.ledger.backing().map_err(Error::Ledger)?,
next.book.unassigned_buyback,
)?;
let base = next
.book
.ledger
.plan(cash, now, cap)
.map_err(Error::Ledger)?;
if base.bounty == 0 || (base.spend > 0 && !buy_executable) {
return Ok(None);
}
if base.kind == Kind::Observe && next.reserve != RESERVE {
return Ok(None);
}
if base.bounty != CALL_COST {
return Err(Error::Invalid);
}
let mut terms = *t;
let y = if royalty_enabled {
let (budget, start, anchor, paid) = t
.wallet_budget(pot, base.spend, now)
.map_err(|_| Error::Terms)?;
let candidate = next.book.royalty_debt.min(budget);
let amount = if candidate >= min_payment {
candidate
} else {
0
};
if amount > 0 {
terms = Terms {
window_start: start,
window_pot: anchor,
window_paid: add(paid, amount)?,
..*t
};
}
amount
} else {
0
};
let bounty = if y > 0 { WALLET_BOUNTY } else { CALL_COST };
let saving = CALL_COST - bounty;
next.book.ledger = base.next;
next.book.unassigned_buyback =
add(sub(next.book.unassigned_buyback, base.credited)?, saving)?;
next.book.royalty_debt -= y;
next.book.royalty_paid = add(next.book.royalty_paid, y)?;
next.book.buyback_spent = add(next.book.buyback_spent, base.spend)?;
next.book.service_spent = add(next.book.service_spent, bounty)?;
let release = next.release_if_finished()?;
next.validate()?;
let unseen = sub(sub(pot, self.backing()?)?, fresh)?;
if sub(
sub(sub(sub(pot, base.spend)?, y)?, bounty)?,
next.backing()?,
)? != unseen
{
return Err(Error::Invalid);
}
Ok(Some(Plan {
next,
action: Action::Base(base.kind),
buy: base.spend,
royalty: y,
bounty,
new_credit: fresh,
new_royalty: royalty,
reserve_topup: topup,
reserve_release: release,
service_release: saving,
terms,
}))
}
fn release_if_finished(&mut self) -> Result<u64, Error> {
if self.book.royalty_debt == 0 && self.book.ledger.remaining == 0 {
let released = self.reserve;
self.reserve = 0;
self.book.unassigned_buyback = add(self.book.unassigned_buyback, released)?;
Ok(released)
} else {
Ok(0)
}
}
}
impl Terms {
pub fn validate(&self) -> Result<(), Error> {
if (
self.step_bps,
self.holding_bps,
self.window_bps,
self.window_secs,
) != (1000, 10000, 2500, 86400)
{
return Err(Error::Terms);
}
Ok(())
}
/// Parent caps are evaluated after script buying but before wallet payout.
/// The percentage budgets exclude the separate one-entry bounty.
pub fn wallet_budget(
&self,
pot_before: u64,
buy: u64,
now: i64,
) -> Result<(u64, i64, u64, u64), Error> {
self.validate()?;
if now < 0 {
return Err(Error::Terms);
}
let after_buy = sub(pot_before, buy)?;
let start = now
.div_euclid(self.window_secs)
.checked_mul(self.window_secs)
.ok_or(Error::Arithmetic)?;
let (window_pot, paid) = if start == self.window_start {
(self.window_pot, self.window_paid)
} else {
(after_buy, 0)
};
let step_cap = fraction(pot_before, self.step_bps)?;
let window_left = fraction(window_pot, self.window_bps)?.saturating_sub(paid);
let budget = step_cap
.min(window_left)
.min(after_buy.saturating_sub(WALLET_BOUNTY));
Ok((fraction(budget, self.holding_bps)?, start, window_pot, paid))
}
}
fn fraction(amount: u64, bps: u16) -> Result<u64, Error> {
u64::try_from((amount as u128) * (bps as u128) / (DENOMINATOR as u128))
.map_err(|_| Error::Arithmetic)
}