bordrfull
← Workshop
Trade hookApproved and deployedPassed with notes

Circuit Breaker

Sell fee up to 8% during sell-offs

Runs inside every trade and transfer of the token.

What it does

Bordrless’s reading of the source, written 13 Oct, 17:07 UTC. Read by AI from the code, not an audit.

Sells pay nothing in calm markets and up to 8% of the tokens while net selling is heavy, to the launcher's holding; buys are free.

The hook keeps one sell-pressure figure: net selling into the launch pool as a share of its depth (pool tokens plus the curve's virtual tokens). Sells add to it, buys subtract, and it halves every 30 minutes. A sell pays the average rate over the pressure it adds: 0% below 1% of depth, rising linearly to 8% at 6%. Sends to wallets are free. Sends to non-wallet addresses pay the current rate. Fees go to the launcher's holding, or to a locked furnace if the launcher is not the creator or is a side of the transfer. It never refuses a transfer.

The most it takes

8%

Up to 8% of a sell, nothing on buys or wallet sends

Largest cut in the trading test8%
Builder’s share of the creator fee50%
Launches with it1

Every rule

When it applies, what it takes, and where that goes.

OnWhenWhat happensAt mostTo
BuysBuysfree; each buy lowers the sell pressure by its share of pool depth0%—
SellsPressure under 1% of pool depth0% hook tax0%—
SellsPressure 1%–6% of pool depth0% at 1%, rising 1.6 points per 1% of depth, of the tokens, to the launcher's holding8%A wallet
SellsPressure 6%+ of pool depth8% of the tokens sold, to the launcher's holding8%A wallet
SellsThe launcher's own sellsSame rate, to the furnace holding, where the tokens stay locked for ever8%Burned
TransfersTransfers to walletsfree0%—
TransfersTransfers to non-wallet addressesThe current pressure's rate, 0% to 8% of the tokens, to the launcher's holding8%A wallet

What to watch

Things a launcher and a buyer should know before they use it.

  • lowOne large dump can push the fee to 8% for every later seller, and it takes about 3 hours to calm from the maximum.
  • lowThe launcher keeps the fees, so their own sells from a second wallet cost them little and raise everyone's sell fee.
  • lowSending tokens to any non-wallet address (a vault, a multisig, another pool) pays the current rate, up to 8%.
  • lowAdding liquidity after graduation pays the rate like a sell.
  • lowFurnace tokens are locked for ever but not burned, so they still count in the supply.

Security review

Sells pay nothing in calm markets and up to 8% of the tokens while net selling is heavy, to the launcher's holding; buys are free.

Passed with notes
  • mediumLauncher can raise every seller's fee cheaply through a second wallet

    Fees go to the launcher's holding (lib.rs payee/Launcher). A launcher who sells from another wallet gets their own cut back, less Bordrless's 25% share. That pushes pressure toward 8%, and other sellers then pay it for hours. Fix: send cuts from sells that raise pressure to the furnace, or cap the launcher's share.

  • lowLaunch account layout read at hard-coded offsets 42 and 161

    math.rs reads creator at 42 and virtualbase at 161 by hand rather than through a crate reader. If the launchpad's Launch layout differs, depth is miscomputed: fees become too high or never charge, or the launcher check fails. Fix: pin these offsets to the launchpad's layout in a test, or read them through bordrlessgame.

  • lowAnyone can send prepare first and become the recorded launcher

    prepare takes the payer as launcher with no mint signature. A front-runner who sees the mint address before the launch can prepare it, and every fee then goes to the furnace for ever. Fix: require the mint keypair to sign prepare.

  • infoFurnace is a lock, not a burn

    The furnace PDA never signs and the hook has no burn instruction, so its tokens are locked for ever but remain in the supply. Burning them would need an extra instruction like Half-Life's stoke.

  • infoFees are dropped until the furnace holding exists

    When the launcher can't take a cut and the furnace holding hasn't been created, payee is Nobody and the sell pays nothing. Create the furnace holding right after launch.

Checks

Approved 13 Oct, 14:35 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

    177,376 bytes, hash 112220bba4fa97f0e33b51cdfd7a8d1e3373e9a8033f2c9930d8d9c4db2d62be.

  • Trading testPassed

    Bought, sold and sent by test wallets over a simulated year; the largest cut it took was 8%.

  • What it does, read by AINotes

    It found: Launcher can raise every seller's fee cheaply through a second wallet.

What the trading test covered
  • Simulated a launch at unix time 1791849600 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.
  • 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).
  • Not caught: what an upgrade would change. Bordrless keeps the hook's upgrade authority; a new binary is simulated again before it is deployed.

On chain

BuilderEWYD…qBHT
Hook program85C9…yhnv
Launch configJ9K9…GVp6
Upgrade authorityCS1N…DW6W
Build hash112220bb…db2d62be
Program size177,376 bytes
Hook runs
before transfertransfer returns delta
Submitted13 Oct, 14:31 UTC

Track record

Coins launched with it1
Cuts taken95
FUGU75,542,154 FUGU taken
In its builder’s words

Dynamic anti-dump fee: sells are free in calm markets, climbing to 8% when panic selling passes 6% of pool depth. Buys and time bring it back down. Splitting dumps across wallets doesn't lower the fee.

Source

The source Bordrless checked and built. Read it before you launch with it.

src/lib.rs441 lines ›
//! Circuit Breaker: a Bordrless token hook that charges sells only while the token is in a sell
//! cascade, and calms down by itself as buyers come back or time passes.
//!
//! The hook keeps one number for the mint, the sell pressure: net selling into the launch's pool
//! in recent minutes, as a share of the pool's depth (its tokens, plus the curve's virtual tokens
//! while the token is on its curve). Every sell into the pool adds to it, every buy out of the pool
//! takes away from it, and it halves every 30 minutes on its own.
//!
//! A sell pays the average rate over the pressure it adds itself:
//! - pressure under 1% of the pool's depth (about a 2% drop in price): nothing;
//! - from 1% to 6% (about an 11% drop): rising in a straight line from 0% to 8%;
//! - 6% or more: 8%.
//! So a small sell in a calm market pays nothing, and the same sell during a dump pays more.
//! Because a sell pays for the pressure it adds, splitting it into pieces at the same moment does
//! not lower the fee, and buying first to lower the pressure does not help either.
//!
//! Buys never pay (a cut on buys while the token is on its curve would keep the curve from
//! reaching its graduation threshold). Wallet-to-wallet sends never pay. Tokens sent to any other
//! address that is not a wallet (another pool, a vault, an escrow) pay the current rate, without
//! changing the pressure, so selling elsewhere does not get around the breaker.
//!
//! Where the fee goes: to the launcher's holding of the token. When the launcher's holding is the
//! sender or the receiver (the token program never lets a cut go to either side of its own
//! transfer), the fee goes to the furnace instead: a holding owned by this program at
//! `["furnace", mint]`, which nothing can ever move tokens out of. So the launcher's own sells pay
//! like anyone's. The furnace holding exists once anyone creates it with the token program's
//! `create_holding` (permissionless, once, after the launch); until then, fees bound for it are not
//! taken.
//!
//! Who the launcher is: the wallet that sends this hook's `prepare` for the mint. The launch page
//! sends `prepare` from the launcher's wallet, in the launch's own transaction. Once the launch
//! exists, the hook checks that this wallet is the launch's creator on the launchpad. If it is
//! not, the launcher is never paid and every fee goes to the furnace.
//!
//! Adding liquidity to the launch's pool after graduation moves tokens into the pool like a sell,
//! so it adds pressure and pays the rate; taking liquidity out relieves pressure like a buy.
//! Nothing is taken on the launchpad's own moves of the token (to or from the launch account, at
//! graduation), or before the launch account can be read. Fees are rounded down, and the hook
//! never refuses a transfer. It has no instruction besides `prepare` and its callback: nobody can
//! change what it does after `prepare`.

#![allow(unexpected_cfgs)]

use anchor_lang::prelude::*;
use bordrless_game::{parse_launch, read_holding};
use bordrless_hook::{
    hook_accounts_address, hook_signer, write_registry, AccountSource, Delta, ExtraAccount,
    HookAccountList, HookReturn, Seed, TokenHookArgs, TokenOp, TOKEN_PREFIX_ACCOUNTS,
};

mod math;
pub use math::*;

declare_id!("7ChZspbBRJDxNRDQBx3QGS8bUvxdfNbDgGbcLx3zf2yb");

/// The flags a launch config names for this hook: `before_transfer` (1), which may answer a cut
/// (64).
pub const FLAGS: u16 = 65;

/// The Bordrless token program: the only caller of the callbacks, and the owner of every holding.
pub const TOKEN_PROGRAM: Pubkey =
    Pubkey::from_str_const("2XoEWp8cF3kRXg74eVwPAyTFhVCAztn3V88komxAvr22");
/// The Bordrless launchpad: its `["launch", mint]` account names the launch's creator, its pool,
/// whether it is on its curve and the curve's virtual tokens.
pub const LAUNCH_PROGRAM: Pubkey =
    Pubkey::from_str_const("1jcBymHxBjniZDhNPy51Vgm5Nz7pLUdxa9UBHc4TavC");
/// `["state", mint]`: this hook's state for a mint.
pub const STATE_SEED: &[u8] = b"state";
/// `["furnace", mint]`: the owner of the furnace holding, a PDA of this program that never signs.
pub const FURNACE_SEED: &[u8] = b"furnace";
/// The launchpad's `["launch", mint]` seed.
pub const LAUNCH_SEED: &[u8] = b"launch";
/// The token program's `["holding", mint, owner]` seed.
pub const HOLDING_SEED: &[u8] = b"holding";

/// Index of the launcher's holding in a callback's accounts: the token program's five, then the
/// registry's extras (the state, the launch, the launcher's holding, the furnace holding).
pub const LAUNCHER_HOLDING_INDEX: u8 = TOKEN_PREFIX_ACCOUNTS as u8 + 2;
/// Index of the furnace holding in a callback's accounts.
pub const FURNACE_HOLDING_INDEX: u8 = TOKEN_PREFIX_ACCOUNTS as u8 + 3;

/// The callbacks' extra accounts, after the token program's five: this hook's state (writable,
/// for the pressure and the running totals), the mint's launch (read for its creator, pool and
/// curve), the launcher's
/// holding of the mint and the furnace holding (both writable: where fees go). None depends on who
/// sends or receives.
pub fn extra_accounts(launcher: &Pubkey, furnace_owner: &Pubkey) -> HookAccountList {
    let holding_of = |owner: &Pubkey| ExtraAccount {
        writable: true,
        source: AccountSource::Pda {
            program: TOKEN_PROGRAM,
            seeds: vec![
                Seed::Literal(HOLDING_SEED.to_vec()),
                Seed::Account(1),
                Seed::Literal(owner.to_bytes().to_vec()),
            ],
        },
    };
    HookAccountList::new(vec![
        ExtraAccount {
            writable: true,
            source: AccountSource::Pda {
                program: crate::ID,
                seeds: vec![Seed::Literal(STATE_SEED.to_vec()), Seed::Account(1)],
            },
        },
        ExtraAccount {
            writable: false,
            source: AccountSource::Pda {
                program: LAUNCH_PROGRAM,
                seeds: vec![Seed::Literal(LAUNCH_SEED.to_vec()), Seed::Account(1)],
            },
        },
        holding_of(launcher),
        holding_of(furnace_owner),
    ])
}

#[program]
pub mod circuit_breaker {
    use super::*;

    /// The standard `prepare`: the state for `mint`, with the sender as the launcher, and the
    /// registry. Anyone may send it, once per mint; the launch sends it from the launcher's wallet.
    pub fn prepare(ctx: Context<Prepare>) -> Result<()> {
        let mint = ctx.accounts.mint.key();
        let launcher = ctx.accounts.payer.key();
        let furnace_owner =
            Pubkey::find_program_address(&[FURNACE_SEED, mint.as_ref()], &crate::ID).0;
        let state = &mut ctx.accounts.state;
        state.version = 1;
        state.bump = ctx.bumps.state;
        state.mint = mint;
        state.launcher = launcher;
        state.launcher_holding = holding_address(&mint, &launcher);
        state.furnace_owner = furnace_owner;
        state.furnace_holding = holding_address(&mint, &furnace_owner);
        state.launch =
            Pubkey::find_program_address(&[LAUNCH_SEED, mint.as_ref()], &LAUNCH_PROGRAM).0;
        state.hook_signer = hook_signer(&TOKEN_PROGRAM, &crate::ID).0;
        state.launcher_check = LAUNCHER_UNCHECKED;
        state.pressure = 0;
        state.pressure_at = 0;
        state.launcher_total = 0;
        state.furnace_total = 0;
        let (registry, bump) = hook_accounts_address(&crate::ID, &mint);
        require_keys_eq!(
            ctx.accounts.registry.key(),
            registry,
            HookError::WrongAccount
        );
        write_registry(
            &ctx.accounts.payer.to_account_info(),
            &ctx.accounts.registry.to_account_info(),
            &ctx.accounts.system_program.to_account_info(),
            &crate::ID,
            &mint,
            bump,
            &extra_accounts(&launcher, &furnace_owner),
        )
    }

    /// Before every transfer: the pressure brought up to date, and the fee a sell (or a transfer to
    /// an address that is not a wallet) pays at it, to the launcher's holding or the furnace.
    pub fn before_transfer(ctx: Context<Callback>, args: TokenHookArgs) -> Result<HookReturn> {
        check_call(&ctx, &args)?;
        if args.op != TokenOp::Transfer {
            return Ok(HookReturn::default());
        }
        let Some(facts) = read_launch_facts(&ctx.accounts.launch, &ctx.accounts.state.mint) else {
            return Ok(HookReturn::default());
        };
        let now = Clock::get()?.unix_timestamp;
        let launcher_ok = confirm_launcher(&mut ctx.accounts.state, &facts);
        let (mint, launch, launcher_holding, furnace_holding) = {
            let s = &ctx.accounts.state;
            (s.mint, s.launch, s.launcher_holding, s.furnace_holding)
        };
        if is_launch_move(&launch, &args) {
            return Ok(HookReturn::default());
        }
        let state = &mut ctx.accounts.state;
        let pressure = decay(state.pressure, now.saturating_sub(state.pressure_at));
        let cut = match flow(&facts.pool, &args) {
            Flow::OutOfPool => {
                let depth =
                    effective_depth(args.source_balance, facts.virtual_base, facts.on_curve);
                state.pressure = pressure.saturating_sub(contribution(args.amount, depth));
                0
            }
            Flow::IntoPool => {
                let depth =
                    effective_depth(args.destination_balance, facts.virtual_base, facts.on_curve);
                let added = contribution(args.amount, depth);
                state.pressure = add_pressure(pressure, added);
                sell_cut(args.amount, pressure, added)
            }
            Flow::Other => {
                state.pressure = pressure;
                if args.destination_owner.is_on_curve() {
                    0
                } else {
                    cut_of(args.amount, rate_at(pressure))
                }
            }
        };
        state.pressure_at = state.pressure_at.max(now);
        if cut == 0 {
            return Ok(HookReturn::default());
        }
        let launcher_can = can_take(
            &ctx.accounts.launcher_holding,
            &launcher_holding,
            &mint,
            &args,
        );
        let furnace_can = can_take(
            &ctx.accounts.furnace_holding,
            &furnace_holding,
            &mint,
            &args,
        );
        let state = &mut ctx.accounts.state;
        let account = match payee(launcher_ok, launcher_can, furnace_can) {
            Payee::Launcher => {
                state.launcher_total = state.launcher_total.saturating_add(u128::from(cut));
                LAUNCHER_HOLDING_INDEX
            }
            Payee::Furnace => {
                state.furnace_total = state.furnace_total.saturating_add(u128::from(cut));
                FURNACE_HOLDING_INDEX
            }
            Payee::Nobody => return Ok(HookReturn::default()),
        };
        Ok(HookReturn {
            deltas: vec![Delta {
                amount: cut,
                account,
            }],
            ..HookReturn::default()
        })
    }
}

/// The token program's holding of `owner` for `mint`.
pub fn holding_address(mint: &Pubkey, owner: &Pubkey) -> Pubkey {
    Pubkey::find_program_address(
        &[HOLDING_SEED, mint.as_ref(), owner.as_ref()],
        &TOKEN_PROGRAM,
    )
    .0
}

/// Whether the transfer is one of the launchpad's own moves of the token (to or from the launch).
pub fn is_launch_move(launch: &Pubkey, args: &TokenHookArgs) -> bool {
    args.source_owner == *launch || args.destination_owner == *launch
}

/// Which way a transfer moves against the launch's pool.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Flow {
    /// Out of the pool: a buy, or liquidity taken out.
    OutOfPool,
    /// Into the pool: a sell, or liquidity added.
    IntoPool,
    /// Anything else: a send between holdings.
    Other,
}

/// Which way `args` moves against `pool`.
pub fn flow(pool: &Pubkey, args: &TokenHookArgs) -> Flow {
    if args.source_owner == *pool {
        Flow::OutOfPool
    } else if args.destination_owner == *pool {
        Flow::IntoPool
    } else {
        Flow::Other
    }
}

/// Whether `holding` is either side of the transfer, which the token program never lets a cut name.
pub fn is_party(holding: &Pubkey, args: &TokenHookArgs) -> bool {
    args.source == *holding || args.destination == *holding
}

/// What the hook reads of its mint's launch.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LaunchFacts {
    /// Who launched it.
    pub creator: Pubkey,
    /// The launch's pool on the DEX.
    pub pool: Pubkey,
    /// Whether it is still on its bonding curve.
    pub on_curve: bool,
    /// The curve's virtual tokens.
    pub virtual_base: u64,
}

/// The launch's facts, from the launchpad's `Launch` account data for `mint`; `None` for anything
/// else (an account not written yet, as during the launch's own transaction).
pub fn launch_facts(data: &[u8], mint: &Pubkey) -> Option<LaunchFacts> {
    let view = parse_launch(data, mint)?;
    let creator = Pubkey::new_from_array(creator_bytes(data)?);
    Some(LaunchFacts {
        creator,
        pool: view.pool,
        on_curve: view.on_curve,
        virtual_base: virtual_base_of(data)?,
    })
}

/// [`launch_facts`] from the launch account: owned by the launchpad, its data readable.
fn read_launch_facts(launch: &AccountInfo, mint: &Pubkey) -> Option<LaunchFacts> {
    if *launch.owner != LAUNCH_PROGRAM {
        return None;
    }
    let data = launch.try_borrow_data().ok()?;
    launch_facts(&data, mint)
}

/// Confirms, once, that the launcher is the launch's creator, and remembers the answer.
fn confirm_launcher(state: &mut HookState, facts: &LaunchFacts) -> bool {
    let (check, on) = next_launcher_check(
        state.launcher_check,
        Some(facts.creator.to_bytes()),
        state.launcher.to_bytes(),
    );
    state.launcher_check = check;
    on
}

/// Whether `holding` (whose address must be `expected`) can take a cut of this transfer: it
/// exists, is of this mint, is not frozen, and is neither side of the transfer.
fn can_take(holding: &AccountInfo, expected: &Pubkey, mint: &Pubkey, args: &TokenHookArgs) -> bool {
    if holding.key != expected || is_party(expected, args) {
        return false;
    }
    match read_holding(holding) {
        Some(view) => view.mint == *mint && !view.frozen,
        None => false,
    }
}

/// Every callback: signed by the token program's signer for this hook, about this state's mint.
fn check_call(ctx: &Context<Callback>, args: &TokenHookArgs) -> Result<()> {
    let signer = &ctx.accounts.hook_signer;
    require!(signer.is_signer, HookError::BadHookSigner);
    require_keys_eq!(
        signer.key(),
        ctx.accounts.state.hook_signer,
        HookError::BadHookSigner
    );
    require_keys_eq!(args.mint, ctx.accounts.state.mint, HookError::WrongAccount);
    Ok(())
}

/// The hook's state for one mint, at `["state", mint]`.
#[account]
#[derive(InitSpace)]
pub struct HookState {
    /// Layout version.
    pub version: u8,
    /// Bump.
    pub bump: u8,
    /// The mint.
    pub mint: Pubkey,
    /// Who sent `prepare`: the launcher, who receives the fees.
    pub launcher: Pubkey,
    /// The launcher's holding of the mint: `["holding", mint, launcher]` under the token program.
    pub launcher_holding: Pubkey,
    /// `["furnace", mint]` under this program: owns the furnace holding and never signs.
    pub furnace_owner: Pubkey,
    /// The furnace holding: `["holding", mint, furnace_owner]` under the token program.
    pub furnace_holding: Pubkey,
    /// The launchpad's `["launch", mint]`.
    pub launch: Pubkey,
    /// The token program's signer for this hook: `["hook-authority", this program]` under it.
    pub hook_signer: Pubkey,
    /// Whether the launcher is the launch's creator (`LAUNCHER_*`).
    pub launcher_check: u8,
    /// The sell pressure as of `pressure_at`, in billionths of the pool's depth.
    pub pressure: u64,
    /// When the pressure was last brought up to date (unix time).
    pub pressure_at: i64,
    /// Everything the launcher has received.
    pub launcher_total: u128,
    /// Everything sent to the furnace.
    pub furnace_total: u128,
}

/// Accounts of the standard `prepare`.
#[derive(Accounts)]
pub struct Prepare<'info> {
    /// Pays the rent, and is the launcher.
    #[account(mut)]
    pub payer: Signer<'info>,
    /// CHECK: the mint the hook is prepared for; need not exist yet.
    pub mint: UncheckedAccount<'info>,
    #[account(init, payer = payer, space = 8 + HookState::INIT_SPACE, seeds = [STATE_SEED, mint.key().as_ref()], bump)]
    pub state: Box<Account<'info, HookState>>,
    /// CHECK: the registry PDA, written here (address-checked in the handler).
    #[account(mut)]
    pub registry: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

/// Accounts of the callback: the token program's five, then the registry's extras.
#[derive(Accounts)]
pub struct Callback<'info> {
    /// CHECK: the token program's signer for this hook (checked in `check_call`).
    pub hook_signer: UncheckedAccount<'info>,
    /// CHECK: the mint.
    pub mint: UncheckedAccount<'info>,
    /// CHECK: the source holding.
    pub source: UncheckedAccount<'info>,
    /// CHECK: the destination holding.
    pub destination: UncheckedAccount<'info>,
    /// CHECK: who signed the transfer.
    pub authority: UncheckedAccount<'info>,
    #[account(mut, seeds = [STATE_SEED, mint.key().as_ref()], bump = state.bump)]
    pub state: Box<Account<'info, HookState>>,
    /// CHECK: the mint's launch account (address-checked; not readable during the launch).
    #[account(address = state.launch @ HookError::WrongAccount)]
    pub launch: UncheckedAccount<'info>,
    /// CHECK: the launcher's holding (address-checked; may not exist yet, read in the handler).
    #[account(mut, address = state.launcher_holding @ HookError::WrongAccount)]
    pub launcher_holding: UncheckedAccount<'info>,
    /// CHECK: the furnace holding (address-checked; may not exist yet, read in the handler).
    #[account(mut, address = state.furnace_holding @ HookError::WrongAccount)]
    pub furnace_holding: UncheckedAccount<'info>,
}

/// Errors.
#[error_code]
pub enum HookError {
    #[msg("an account is not the one this mint's hook expects")]
    WrongAccount,
    #[msg("the hook signer is not the token program's signer for this hook")]
    BadHookSigner,
}
src/math.rs196 lines ›
//! The hook's arithmetic and decisions, on plain integers and bytes: the sell pressure and how it
//! decays, the fee it sets, who is paid, and whether the launcher is the launch's creator. Nothing
//! here can panic.

/// Basis points in a whole.
pub const BPS: u64 = 10_000;

/// Pressure is measured in billionths of the pool's depth: this is net selling equal to the whole
/// depth.
pub const PRESSURE_ONE: u64 = 1_000_000_000;
/// Below this pressure sells pay nothing: net selling of 1% of the pool's depth (about a 2% drop
/// in price).
pub const START_PRESSURE: u64 = 10_000_000;
/// At or above this pressure sells pay the most: net selling of 6% of the pool's depth (about an
/// 11% drop in price).
pub const FULL_PRESSURE: u64 = 60_000_000;
/// Pressure never counts above the whole depth, so it always calms within a few hours.
pub const MAX_PRESSURE: u64 = PRESSURE_ONE;
/// The most a sell pays: 8%, in basis points of the amount.
pub const MAX_RATE_BPS: u64 = 800;
/// Pressure halves every 30 minutes with no trading.
pub const HALF_LIFE_SECS: i64 = 1_800;

/// `launcher_check`: not known yet (the launch can't be read yet).
pub const LAUNCHER_UNCHECKED: u8 = 0;
/// `launcher_check`: the launcher is the launch's creator.
pub const LAUNCHER_CONFIRMED: u8 = 1;
/// `launcher_check`: the launch's creator is someone else; the launcher is never paid.
pub const LAUNCHER_NOT_CREATOR: u8 = 2;

/// Where the launchpad's `Launch` account keeps `creator`: after its discriminator (8 bytes),
/// `version` (1), `bump` (1) and `mint` (32), and right before `pool`.
pub const LAUNCH_CREATOR_OFFSET: usize = 42;
/// Where it keeps `virtual_base`: after `pool`, `quote_mint`, `status`, `creator_fee_bps`,
/// `lp_fee_bps`, `sniper_window_secs`, `sniper_start_bps` and `virtual_quote`.
pub const LAUNCH_VIRTUAL_BASE_OFFSET: usize = 161;

/// `pressure` after `elapsed` seconds without trading: halved for every whole half-life, and in a
/// straight line between two halvings. Nothing decays if the clock did not move forward.
pub fn decay(pressure: u64, elapsed: i64) -> u64 {
    if elapsed <= 0 {
        return pressure;
    }
    let halvings = elapsed / HALF_LIFE_SECS;
    if halvings >= 64 {
        return 0;
    }
    let p = pressure >> halvings;
    let rest = elapsed % HALF_LIFE_SECS;
    let drop = u128::from(p) * u128::from(rest.unsigned_abs())
        / (2 * u128::from(HALF_LIFE_SECS.unsigned_abs()));
    p - u64::try_from(drop).unwrap_or(p)
}

/// The pool's depth as a trade sees it: its token balance, plus the curve's virtual tokens while
/// the token is on its curve (they set the curve's price just as real ones do).
pub fn effective_depth(pool_balance: u64, virtual_base: u64, on_curve: bool) -> u64 {
    if on_curve {
        pool_balance.saturating_add(virtual_base)
    } else {
        pool_balance
    }
}

/// The pressure `amount` tokens make against a pool of `depth`: `amount / depth` in billionths,
/// never more than `MAX_PRESSURE` (an empty pool counts as the most).
pub fn contribution(amount: u64, depth: u64) -> u64 {
    if depth == 0 {
        return MAX_PRESSURE;
    }
    let c = u128::from(amount) * u128::from(PRESSURE_ONE) / u128::from(depth);
    u64::try_from(c).unwrap_or(MAX_PRESSURE).min(MAX_PRESSURE)
}

/// The rate at `pressure`, in basis points: nothing below `START_PRESSURE`, rising in a straight
/// line to `MAX_RATE_BPS` at `FULL_PRESSURE`, and `MAX_RATE_BPS` above.
pub fn rate_at(pressure: u64) -> u64 {
    if pressure <= START_PRESSURE {
        0
    } else if pressure >= FULL_PRESSURE {
        MAX_RATE_BPS
    } else {
        let r = u128::from(MAX_RATE_BPS) * u128::from(pressure - START_PRESSURE)
            / u128::from(FULL_PRESSURE - START_PRESSURE);
        u64::try_from(r).unwrap_or(MAX_RATE_BPS)
    }
}

/// The area under the rate from no pressure to `pressure` (basis points times pressure units).
pub fn rate_area(pressure: u64) -> u128 {
    let max = u128::from(MAX_RATE_BPS);
    let ramp = u128::from(FULL_PRESSURE - START_PRESSURE);
    if pressure <= START_PRESSURE {
        0
    } else if pressure <= FULL_PRESSURE {
        let x = u128::from(pressure - START_PRESSURE);
        max * x * x / (2 * ramp)
    } else {
        max * ramp / 2 + max * u128::from(pressure - FULL_PRESSURE)
    }
}

/// What a sell of `amount` pays when the pressure before it is `before` and it adds `added`: the
/// amount times the average rate over the pressure the sell itself adds. Splitting a sell into
/// pieces at the same moment pays (to rounding) the same, and buying first to lower the pressure
/// does not help, because a later sell adds the pressure back.
pub fn sell_cut(amount: u64, before: u64, added: u64) -> u64 {
    if added == 0 {
        return cut_of(amount, rate_at(before));
    }
    let after = before.saturating_add(added);
    let area = rate_area(after).saturating_sub(rate_area(before));
    let cut = u128::from(amount) * area / (u128::from(added) * u128::from(BPS));
    let cut = u64::try_from(cut).unwrap_or(0);
    if cut > amount {
        amount
    } else {
        cut
    }
}

/// The pressure after a sell that adds `added`.
pub fn add_pressure(pressure: u64, added: u64) -> u64 {
    pressure.saturating_add(added).min(MAX_PRESSURE)
}

/// `bps` basis points of `amount`, rounded down; never more than `amount`.
pub fn cut_of(amount: u64, bps: u64) -> u64 {
    let bps = if bps > BPS { BPS } else { bps };
    let cut = u128::from(amount) * u128::from(bps) / u128::from(BPS);
    let cut = u64::try_from(cut).unwrap_or(0);
    if cut > amount {
        amount
    } else {
        cut
    }
}

/// Who receives a cut.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Payee {
    /// The launcher's holding.
    Launcher,
    /// The furnace: a holding this hook owns, which nothing can ever move tokens out of.
    Furnace,
    /// Nobody: the cut is not taken.
    Nobody,
}

/// Who is paid: the launcher when they are the launch's creator and their holding can take the cut;
/// otherwise the furnace when its holding can; otherwise nobody. A holding "can take" the cut when
/// it exists, is of this mint, is not frozen and is neither side of the transfer.
pub fn payee(launcher_confirmed: bool, launcher_can_take: bool, furnace_can_take: bool) -> Payee {
    if launcher_confirmed && launcher_can_take {
        Payee::Launcher
    } else if furnace_can_take {
        Payee::Furnace
    } else {
        Payee::Nobody
    }
}

/// The next `launcher_check` and whether the launcher may be paid, given the creator the launch
/// names (`None` while the launch can't be read).
pub fn next_launcher_check(
    current: u8,
    creator: Option<[u8; 32]>,
    launcher: [u8; 32],
) -> (u8, bool) {
    match current {
        LAUNCHER_CONFIRMED => (LAUNCHER_CONFIRMED, true),
        LAUNCHER_NOT_CREATOR => (LAUNCHER_NOT_CREATOR, false),
        _ => match creator {
            Some(c) if c == launcher => (LAUNCHER_CONFIRMED, true),
            Some(_) => (LAUNCHER_NOT_CREATOR, false),
            None => (LAUNCHER_UNCHECKED, false),
        },
    }
}

/// The 32 bytes of `creator` in a `Launch` account's data, when it is long enough.
pub fn creator_bytes(data: &[u8]) -> Option<[u8; 32]> {
    let slice = data.get(LAUNCH_CREATOR_OFFSET..LAUNCH_CREATOR_OFFSET + 32)?;
    let mut out = [0u8; 32];
    out.copy_from_slice(slice);
    Some(out)
}

/// `virtual_base` in a `Launch` account's data, when it is long enough.
pub fn virtual_base_of(data: &[u8]) -> Option<u64> {
    let slice = data.get(LAUNCH_VIRTUAL_BASE_OFFSET..LAUNCH_VIRTUAL_BASE_OFFSET + 8)?;
    let mut out = [0u8; 8];
    out.copy_from_slice(slice);
    Some(u64::from_le_bytes(out))
}
Circuit Breaker · Workshop · Bordrfull