bordrfull
← Workshop
Trade hookApproved and deployedPassed with notes

Split

2% sell and transfer tax to the launcher

Runs inside every trade and transfer of the token.

Launch config comingSubmit yours ↗

What it does

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

Takes 2% of the tokens on every sell, wallet transfer and post-graduation buy, all sent to the launcher, half meant for holder airdrops.

On every sell, wallet-to-wallet transfer and post-graduation buy, the hook takes 2% of the tokens moved as one cut to the launcher's holding. Half is the launcher's own share. The other half is a holders' share that the launcher is expected to airdrop by hand, using the time-weighted balances the hook records in each holding. Buys on the curve, the launchpad's own moves and the launcher's own trades are free. It takes nothing at all unless the wallet that sent prepare is the launch's creator. It never refuses a transfer, and its rates never change.

The most it takes

2%

2% of every sell, transfer and post-graduation buy; curve buys free

Largest cut in the trading test2%
Builder’s share of the creator fee50%
Launches with it0

Every rule

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

OnWhenWhat happensAt mostTo
BuysBuys on the curvefree0%—
BuysBuys after graduation2% of the tokens to the launcher's wallet: 1% theirs, 1% for them to airdrop to holders2%A wallet
SellsSells2% of the tokens to the launcher's wallet: 1% theirs, 1% for them to airdrop to holders2%A wallet
TradesAmounts under 100 base unitsfree: each 1% share rounds down to 00%—
TradesBefore the launcher holds the tokenfree until the launcher's holding of the token exists0%—
TradesThe launcher's own tradesfree0%—
TransfersTransfers2% of the tokens to the launcher's wallet, as on sells2%A wallet

What to watch

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

  • mediumThe holders' 1% goes to the launcher's wallet. Any airdrop depends on the launcher, and nothing on chain enforces it.
  • mediumHoldings can never be closed: the hook writes a record even when a holding is emptied, so its rent stays locked.
  • lowThe tax turns on only once the launcher's holding of the token exists. Until then trades pay nothing.
  • lowIf someone else sends prepare for the mint before the launcher does, the hook takes nothing, ever.

Security review

Takes 2% of the tokens on every sell, wallet transfer and post-graduation buy, all sent to the launcher, half meant for holder airdrops.

Passed with notes
  • mediumEmptied holdings keep non-zero hook data, so no holding can ever be closed

    checkpoint (src/math.rs) always writes version 1, including when balanceafter is 0, for both sides in beforetransfer and for the source in beforeburn. The token program refuses closeholding when the hook data is not all zero under WRITESHOOKDATA, so every holder's rent is locked for good. Fix: return 0u8; 64 when balanceafter == 0.

  • lowprepare needs no signature from the mint, so anyone can pre-empt it and disable the tax

    Anyone may send prepare for any mint address (src/lib.rs prepare). The sender becomes the launcher, and a launch whose creator differs sets LAUNCHERNOTCREATOR for good, so the hook takes nothing. Doing this requires knowing the mint address before the launch. Fix: require the mint to sign prepare, or bind the launcher to an argument the mint signs.

  • lowThe tax switches on only when the launcher's holding exists

    launcherholdingcantake returns false until the launcher's holding of the mint is created, so early sells and transfers are free. Anyone can create that holding, but the launcher controls when it first exists for them. It is a one-way change, from off to on.

  • infoThe holders' share is paid to the launcher's wallet, and airdrops rely on trust

    Both 1% shares go as one cut to the launcher's holding. The comments disclose this, and airdropsharetotal is public, but nothing makes the launcher pay holders. Launchers and buyers should treat the hook as a 2% tax paid to the launcher.

  • infoThe pool vault and the launch holdings also get balance records

    checkpoint is written for every source and destination, including the pool's vault and the launch's holding. Anyone computing airdrop shares from the time-weighted balances must leave these out.

Checks

Approved 13 Oct, 08:56 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

    170,944 bytes, hash 26b816ccdd9f68e1930d0544395427cf7f8f5888db5e62b26a4fad0b9f55edfd.

  • Trading testPassed

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

  • What it does, read by AINotes

    It found: Emptied holdings keep non-zero hook data, so no holding can ever be closed.

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

Builder5KcD…b87u
Hook programCULa…Rtku
Launch configNot made yet
Upgrade authorityCS1N…DW6W
Build hash26b816cc…9f55edfd
Program size170,944 bytes
Hook runs
before transferbefore burntransfer returns deltawrites hook data
Submitted13 Oct, 08:43 UTC

Track record

No coin has launched with it yet.
In its builder’s words

1% to the launcher and 1% to holders on every sell and transfer. Curve buys pay nothing so coins graduate.

Source

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

src/lib.rs410 lines ›
//! Split: a Bordrless token hook that takes 2% of every sell and wallet-to-wallet transfer, and of
//! every buy once the token has graduated: 1% for the launcher to keep and 1% as a share for the
//! token's holders, which the launcher also receives and airdrops to holders by hand.
//!
//! What it does, in full:
//! - On every sell and wallet-to-wallet send, and every buy once the token has graduated, it
//!   takes 1% of the amount for the launcher and 1% for holders, each rounded down (a transfer of
//!   under 100 base units pays nothing). The token program sends both, as one cut, to the
//!   launcher's holding of the token, taken from what the receiver gets. (The token program
//!   refuses two cuts to the same holding.)
//! - The hook cannot pay holders itself: a Workshop hook moves no tokens and calls no other
//!   program, and the launchpad's holder rewards can't run beside a custom hook. So the launcher
//!   holds the holders' share and is expected to airdrop it to holders. The hook can't make them
//!   do it: holders rely on the launcher for every payout.
//! - It keeps two public running totals in its state, `launcher_share_total` and
//!   `airdrop_share_total`, so anyone can compare the holders' share with what the launcher has
//!   airdropped.
//! - In each holding's 64 bytes of hook data it records the holding's balance and its
//!   time-weighted balance (`Checkpoint` in `math.rs`), on every transfer and burn, so anyone can
//!   compute each holder's fair share of an airdrop from the chain alone.
//!
//! 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 also checks that this wallet is the launch's creator on the launchpad. If it is
//! not, the hook takes nothing for that mint, ever.
//!
//! It takes nothing:
//! - while the launcher has no holding of the token yet (it exists once they first hold it);
//! - when the launcher's holding is the sender or the receiver of the transfer (the token program
//!   never lets a cut go to either side of its own transfer, so the launcher's own trades and
//!   airdrops are free);
//! - on the launchpad's own moves of the token (to or from the launch account, at graduation);
//! - on buys while the token is still on its launch curve. Bordrless's share of a cut on a curve
//!   buy comes out of the curve's SOL reserve, which would leave the curve short of its graduation
//!   threshold and could keep the token from graduating. Once it has graduated, buys pay too;
//! - until the launch account can be read, and never if its creator is not the launcher.
//!
//! It never refuses a transfer or a burn, and has no instruction besides `prepare` and its
//! callbacks: 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!("39CYHGuSRzqRj3ET9Dfe73jgfJbXLw4hA5kgvP174M6R");

/// The launcher's share: 1% of each transfer that pays (no curve buys), in basis points.
pub const LAUNCHER_SHARE_BPS: u64 = 100;
/// The holders' share, which the launcher receives to airdrop: 1% of each transfer that pays
/// (no curve buys), in basis points.
pub const AIRDROP_SHARE_BPS: u64 = 100;
/// The flags a launch config names for this hook: `before_transfer` (1) and `before_burn` (16),
/// a cut on transfers (64), and the holdings' hook data (128).
pub const FLAGS: u16 = 209;

/// 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.
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";
/// 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).
pub const LAUNCHER_HOLDING_INDEX: u8 = TOKEN_PREFIX_ACCOUNTS as u8 + 2;

/// The callbacks' extra accounts, after the token program's five: this hook's state (writable, for
/// the running totals), the mint's launch (read for its creator, its pool and whether it is on its
/// curve), and the launcher's holding of the mint (writable: where both shares go). None depends
/// on who sends or receives.
pub fn extra_accounts(launcher: &Pubkey) -> HookAccountList {
    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)],
            },
        },
        ExtraAccount {
            writable: true,
            source: AccountSource::Pda {
                program: TOKEN_PROGRAM,
                seeds: vec![
                    Seed::Literal(HOLDING_SEED.to_vec()),
                    Seed::Account(1),
                    Seed::Literal(launcher.to_bytes().to_vec()),
                ],
            },
        },
    ])
}

#[program]
pub mod split {
    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 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.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.launcher_share_total = 0;
        state.airdrop_share_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),
        )
    }

    /// Before every transfer: 1% of the amount for the launcher and 1% for holders, as one cut to
    /// the launcher's holding when it can take it (none on a buy while the token is on its curve),
    /// and both holdings' records brought up to date.
    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 now = Clock::get()?.unix_timestamp;
        let facts = read_launch_facts(&ctx.accounts.launch, &ctx.accounts.state.mint);
        let launcher_ok = confirm_launcher(&mut ctx.accounts.state, facts.as_ref());
        let holding_ok =
            launcher_holding_can_take(&ctx.accounts.launcher_holding, &ctx.accounts.state, &args);
        // No cut on a buy while the token is on its curve (see the top of this file).
        let curve_buy = is_curve_buy(facts.as_ref(), &args);
        let state = &mut ctx.accounts.state;
        let (launcher_share, airdrop_share) =
            if launcher_ok && holding_ok && !curve_buy && !is_launch_move(&state.launch, &args) {
                (
                    cut_of(args.amount, LAUNCHER_SHARE_BPS),
                    cut_of(args.amount, AIRDROP_SHARE_BPS),
                )
            } else {
                (0, 0)
            };
        let cut = total_cut(args.amount, launcher_share, airdrop_share);
        // Running totals of each share, informational: they never block anything.
        state.launcher_share_total = state
            .launcher_share_total
            .saturating_add(u128::from(launcher_share));
        state.airdrop_share_total = state
            .airdrop_share_total
            .saturating_add(u128::from(airdrop_share));
        let (source_after, destination_after) = balances_after(
            args.source == args.destination,
            args.source_balance,
            args.destination_balance,
            args.amount,
            cut,
        );
        let mut answer = HookReturn {
            source_hook_data: Some(checkpoint(
                &args.source_hook_data,
                args.source_balance,
                source_after,
                now,
            )),
            destination_hook_data: Some(checkpoint(
                &args.destination_hook_data,
                args.destination_balance,
                destination_after,
                now,
            )),
            ..HookReturn::default()
        };
        // A cut must be above zero, so a free transfer answers none.
        if cut > 0 {
            answer.deltas = vec![Delta {
                amount: cut,
                account: LAUNCHER_HOLDING_INDEX,
            }];
        }
        Ok(answer)
    }

    /// Before a burn: the burning holding's record brought up to date. Nothing is taken.
    pub fn before_burn(ctx: Context<Callback>, args: TokenHookArgs) -> Result<HookReturn> {
        check_call(&ctx, &args)?;
        if args.op != TokenOp::Burn {
            return Ok(HookReturn::default());
        }
        let now = Clock::get()?.unix_timestamp;
        Ok(HookReturn {
            source_hook_data: Some(checkpoint(
                &args.source_hook_data,
                args.source_balance,
                args.source_balance.saturating_sub(args.amount),
                now,
            )),
            ..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
}

/// 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: its creator, its pool, and whether it is still on its
/// bonding curve.
#[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 (not graduated).
    pub on_curve: bool,
}

/// 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,
    })
}

/// [`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)
}

/// Whether the transfer is a buy on the launch's curve: out of the pool while it is on its curve.
pub fn is_curve_buy(facts: Option<&LaunchFacts>, args: &TokenHookArgs) -> bool {
    match facts {
        Some(f) => curve_buy(f.on_curve, f.pool.to_bytes(), args.source_owner.to_bytes()),
        None => false,
    }
}

/// Confirms, once the launch can be read, that the launcher is its creator, and remembers it.
fn confirm_launcher(state: &mut HookState, facts: Option<&LaunchFacts>) -> bool {
    let (check, on) = next_launcher_check(
        state.launcher_check,
        facts.map(|f| f.creator.to_bytes()),
        state.launcher.to_bytes(),
    );
    state.launcher_check = check;
    on
}

/// Whether the launcher's holding can take a cut of this transfer: it exists, is of this mint, is
/// not frozen, and is neither side of the transfer.
fn launcher_holding_can_take(
    holding: &AccountInfo,
    state: &HookState,
    args: &TokenHookArgs,
) -> bool {
    if is_party(&state.launcher_holding, args) {
        return false;
    }
    match read_holding(holding) {
        Some(view) => view.mint == state.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 both shares.
    pub launcher: Pubkey,
    /// The launcher's holding of the mint: `["holding", mint, launcher]` under the token program.
    pub launcher_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,
    /// Everything the launcher received as their own share.
    pub launcher_share_total: u128,
    /// Everything sent to the launcher as the holders' share, for them to airdrop.
    pub airdrop_share_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 every 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 (the mint for a burn).
    pub destination: UncheckedAccount<'info>,
    /// CHECK: who signed the transfer or burn.
    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>,
}

/// 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.rs188 lines ›
//! The hook's arithmetic and decisions, on plain integers and bytes: the shares, whether the
//! launcher is the launch's creator, and each holding's record of what it held and for how long.
//! Nothing here can panic.

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

/// `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; the shares are on.
pub const LAUNCHER_CONFIRMED: u8 = 1;
/// `launcher_check`: the launch's creator is someone else; the shares stay off for good.
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;

/// The version byte of a holding's record once this hook has written it.
pub const HOOK_DATA_VERSION: u8 = 1;

/// `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
    }
}

/// The next `launcher_check` and whether the share is on, 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)
}

/// What this hook keeps in each holding's 64 bytes of hook data, little-endian:
/// - `[0..8)`: `balance`, the holding's balance after its last transfer or burn (u64);
/// - `[8..16)`: `time`, the unix time of that transfer or burn (i64);
/// - `[16..32)`: `weight`, its time-weighted balance up to then: the sum, over every stretch
///   between two writes, of the balance held times the seconds it was held (u128, token-seconds);
/// - `[32]`: `version`, 1 once written (all zeros means never written);
/// - `[33..64)`: zeros.
///
/// A holding's weight at any later time `t` is `weight + balance * (t - time)`. Its fair share of
/// an airdrop for the period from `t0` to `t1` is its weight gained over the period divided by the
/// total gained by every holding that shares.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Checkpoint {
    /// Balance after the last write.
    pub balance: u64,
    /// Unix time of the last write.
    pub time: i64,
    /// Token-seconds held up to the last write.
    pub weight: u128,
    /// `HOOK_DATA_VERSION` once written, 0 before.
    pub version: u8,
}

impl Checkpoint {
    /// Reads a holding's 64 bytes (all zeros reads as never written).
    pub fn decode(data: &[u8; 64]) -> Self {
        let mut balance = [0u8; 8];
        balance.copy_from_slice(&data[0..8]);
        let mut time = [0u8; 8];
        time.copy_from_slice(&data[8..16]);
        let mut weight = [0u8; 16];
        weight.copy_from_slice(&data[16..32]);
        Checkpoint {
            balance: u64::from_le_bytes(balance),
            time: i64::from_le_bytes(time),
            weight: u128::from_le_bytes(weight),
            version: data[32],
        }
    }

    /// The 64 bytes to write.
    pub fn encode(&self) -> [u8; 64] {
        let mut out = [0u8; 64];
        out[0..8].copy_from_slice(&self.balance.to_le_bytes());
        out[8..16].copy_from_slice(&self.time.to_le_bytes());
        out[16..32].copy_from_slice(&self.weight.to_le_bytes());
        out[32] = self.version;
        out
    }

    /// The weight up to `now`, counting since the last write only what the holding really held
    /// throughout: the smaller of the recorded balance and `balance_before` (its balance just
    /// before this write). Nothing accrues before the first write, or if the clock went back.
    pub fn weight_at(&self, balance_before: u64, now: i64) -> u128 {
        if self.version == 0 {
            return self.weight;
        }
        let held = if self.balance < balance_before {
            self.balance
        } else {
            balance_before
        };
        let secs = now
            .checked_sub(self.time)
            .and_then(|s| u64::try_from(s).ok())
            .unwrap_or(0);
        self.weight
            .saturating_add(u128::from(held).saturating_mul(u128::from(secs)))
    }
}

/// The new 64 bytes for a holding whose balance goes from `balance_before` to `balance_after` at
/// `now`, given its current bytes.
pub fn checkpoint(data: &[u8; 64], balance_before: u64, balance_after: u64, now: i64) -> [u8; 64] {
    let last = Checkpoint::decode(data);
    let time = if last.version != 0 && last.time > now {
        last.time
    } else {
        now
    };
    Checkpoint {
        balance: balance_after,
        time,
        weight: last.weight_at(balance_before, now),
        version: HOOK_DATA_VERSION,
    }
    .encode()
}

/// The source's and the destination's balances after a transfer of `amount` of which `cut` goes
/// elsewhere (the token program takes cuts from what the destination receives). `same` is a
/// transfer from a holding to itself, which only loses the cut.
pub fn balances_after(
    same: bool,
    source_before: u64,
    destination_before: u64,
    amount: u64,
    cut: u64,
) -> (u64, u64) {
    let received = amount.saturating_sub(cut);
    if same {
        let balance = source_before
            .saturating_sub(amount)
            .saturating_add(received);
        (balance, balance)
    } else {
        (
            source_before.saturating_sub(amount),
            destination_before.saturating_add(received),
        )
    }
}

/// The one cut that carries both shares: their sum, never more than `amount`.
pub fn total_cut(amount: u64, launcher_share: u64, airdrop_share: u64) -> u64 {
    let sum = launcher_share.saturating_add(airdrop_share);
    if sum > amount {
        amount
    } else {
        sum
    }
}

/// Whether a transfer is a buy on the launch's curve: the token is on its curve and the transfer
/// comes out of its pool (on the curve, nothing else ever leaves the pool for a wallet).
pub fn curve_buy(on_curve: bool, pool: [u8; 32], source_owner: [u8; 32]) -> bool {
    on_curve && source_owner == pool
}
Split · Workshop · Bordrfull