bordrfull
← Workshop
Trade hookApproved and deployedPassed with notes

Track Record

On-chain wallet track record, no fees

Runs inside every trade and transfer of the token.

Launch config comingSubmit yours ↗

What it does

Bordrless’s reading of the source, written 14 Oct, 19:24 UTC. Read by AI from the code, not an audit.

Takes nothing and refuses nothing: it records each wallet's buys, sells, dumps, wallet payments and attester marks in its holding's hook data.

Before every transfer it updates a 64-byte record in each wallet holding it touches. Buys from the launch pool, sells into it, sends into other programs and wallet-to-wallet payments are tallied in whole tokens; a single sell of 1% of the pool's depth or more counts as a dump. Payments count toward the payee's weighted total in proportion to how long the payer has held, in full after 30 days. Two hard-coded attester keys can mark any wallet by sending it 1 to 15 base units. It takes no cut and never refuses a transfer.

The most it takes

0%

Takes nothing from any trade or transfer

Largest cut in the trading test0%
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
BuysBuysfree; records whole tokens bought and when0%—
SellsSellsfree; a sell of 1% of pool depth or more is recorded as a dump0%—
TransfersTransfersfree; wallet to wallet is recorded as a payment, weighted over 30 days of holding0%—
TransfersAttester marks1 to 15 base units from either of two fixed keys set a mark code on the receiver0%—

What to watch

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

  • mediumTwo fixed attester keys can write or revoke the attestation mark of any wallet's record at any time.
  • lowRecords are never cleared, so a holding that sold out can never be closed and its rent stays locked.
  • lowPayment counts and weighted totals can be inflated by sending tokens between one's own wallets.
  • lowDump detection reads virtualbase at a fixed byte offset in the launch account.

Security review

Takes nothing and refuses nothing: it records each wallet's buys, sells, dumps, wallet payments and attester marks in its holding's hook data.

Passed with notes
  • mediumSelf and circular transfers inflate a wallet's payment record

    src/record.rs onpayment counts every wallet-to-wallet transfer, including a transfer to the same holding or between wallets one person controls. A wallet held 30 days can loop tokens to raise received, receivedweighted and paymentsin without limit. Skip source == destination, and document that circular payments between colluding wallets are not filtered.

  • mediumTwo hard-coded attester keys can write any wallet's attestation fields

    src/lib.rs:49-50, ATTESTERS. Whoever holds either key can set or revoke attestlevel on any wallet with a 1-15 unit send, and these keys cannot be changed after deploy. The code cannot show they are unsignable placeholders. If they are off-curve, the Mark path is dead, because kind() requires the source to be on curve. Disclose who holds them, or ship verifiably off-curve keys.

  • lowHook data never returns to zero, so holdings can never be closed

    WRITESHOOKDATA is set and depart() keeps the record when a wallet sells out. The token program refuses closeholding while hook data is non-zero, so every holder's rent is locked for good. This is intended for keeping history, but launchers and holders should be told.

  • lowvirtualbase read at a fixed offset 161 of the Launch account

    src/lib.rs LAUNCHVIRTUALBASEOFFSET is not taken from the launchpad's layout. If the offset is wrong, curve depth and the dump flag are misrecorded, and a layout change silently alters records. Read it through the launchpad's crate, or test the offset against it.

  • infoComment says marks expire after 30 days; the code stores them without expiry

    src/lib.rs:19 says marks expire unless renewed. onmark writes the code and time and nothing expires on chain; expiry is left to readers. Make the wording match the code.

Checks

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

    171,256 bytes, hash 18dde8922f2d2e77214f7294f6a7d677ab7a9a0679b46b45cb6aa8b64d691816.

  • Trading testPassed

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

  • What it does, read by AINotes

    It found: Self and circular transfers inflate a wallet's payment record; Two hard-coded attester keys can write any wallet's attestation fields.

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.
  • 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 programF1R5…imT8
Launch configNot made yet
Upgrade authorityCS1N…DW6W
Build hash18dde892…4d691816
Program size171,256 bytes
Hook runs
before transferwrites hook data
Submitted14 Oct, 19:12 UTC

Track record

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

On-chain reputation for wallets: records buys, sells and dumps (sells of 1%+ of pool depth), payment history, and attester marks (identity/autonomous). Takes nothing from anyone. Attesters can only write marks, never move tokens.

Source

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

src/lib.rs357 lines ›
//! Track Record: a Bordrless token hook that keeps an on-chain record of how every wallet behaves
//! in this token's economy, so agents (and anyone else) can check a counterparty before trading
//! with it, hiring it or accepting its payment.
//!
//! It takes nothing and never refuses a transfer. Before each transfer it writes the record of each
//! wallet the transfer touches, in the 64 bytes of hook data every holding keeps (layout and rules
//! in `record.rs`):
//! - a transfer out of the launch's pool to a wallet is a buy;
//! - from a wallet into the pool is a sell, and a sell of 1% of the pool's depth or more is a dump;
//! - from a wallet into any other program is a sell too, measured against the pool's last known
//!   depth, so selling through another venue can't hide a dump;
//! - wallet to wallet is a payment: the payer's `paid`, the payee's `received`, and the payee's
//!   `received_weighted`, where each payment counts in proportion to how long its payer has had
//!   the token (in full after 30 days), so fresh wallets can't manufacture a reputation;
//! - from one of the two attester keys below to a wallet, 1 to 15 base units are a mark, not a
//!   payment: 1 vouches for who runs the wallet, 2 that its key lives in an enclave running known
//!   code, 3 revokes, and 4 to 15 are kept for meanings the reader defines later. A mark writes the
//!   code, which key and when; how long a mark stays valid, and which keys to trust, is the
//!   reader's decision (marks expire after 30 days unless renewed).
//!
//! Nothing is recorded for programs (the pool, vaults, companions), for the launchpad's own moves
//! of the token, or before the launch account can be read (during the launch's own transaction).
//! The hook's only instructions are `prepare` and its callback: nobody can change what it does
//! after `prepare`.

#![allow(unexpected_cfgs)]

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

mod record;
pub use record::*;

declare_id!("CgGucVXn8zpZ199Z6SniM1gwkLNF39Y35M8nmRphNQ13");

/// The flags a launch config names for this hook: `before_transfer` (1), which writes both sides'
/// hook data (128). It answers no cut.
pub const FLAGS: u16 = 129;

/// The two attester keys: the platform's key and its cold backup. A transfer of 1 to 15 base
/// units from either to a wallet is a mark (see `record.rs`). Put your own two public keys here
/// (`set-attesters.py` does it). The placeholders this ships with are addresses no key can sign
/// for: with them in place, nobody can mark anything.
pub const ATTESTERS: [Pubkey; 2] = [
    Pubkey::from_str_const("EBMNxA64qxY8iNHC1Kp8kS5TykjRLdTduQUcDkUTzRHc"),
    Pubkey::from_str_const("HXVeBRPS48vLMfUZGFWG2XHgeNJJpD14JTJPnXnwchhc"),
];

/// The Bordrless launchpad: its `["launch", mint]` account names the launch's pool, whether it is on
/// its curve and the curve's virtual tokens.
pub const LAUNCH_PROGRAM: Pubkey =
    Pubkey::from_str_const("1jcBymHxBjniZDhNPy51Vgm5Nz7pLUdxa9UBHc4TavC");
/// The Bordrless token program: the only caller of the callback.
pub const TOKEN_PROGRAM: Pubkey =
    Pubkey::from_str_const("2XoEWp8cF3kRXg74eVwPAyTFhVCAztn3V88komxAvr22");
/// `["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";

/// Where the launchpad's `Launch` account keeps `virtual_base`.
pub const LAUNCH_VIRTUAL_BASE_OFFSET: usize = 161;

/// The callback's extra accounts, after the token program's five: this hook's state (writable: the
/// pool's last known depth and running counts) and the mint's launch (read for its pool and
/// curve). Neither depends on who sends or receives.
pub fn extra_accounts() -> 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)],
            },
        },
    ])
}

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

    /// The standard `prepare`: the state for `mint`, and the registry. Anyone may send it, once per
    /// mint; the launch sends it for each new mint itself.
    pub fn prepare(ctx: Context<Prepare>) -> Result<()> {
        let mint = ctx.accounts.mint.key();
        let state = &mut ctx.accounts.state;
        state.version = 1;
        state.bump = ctx.bumps.state;
        state.mint = mint;
        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.last_depth = 0;
        state.trades = 0;
        state.payments = 0;
        state.marks = 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(),
        )
    }

    /// Before every transfer: the records of the wallets it touches, 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 Some(facts) = read_launch_facts(&ctx.accounts.launch, &ctx.accounts.state.mint) else {
            return Ok(HookReturn::default());
        };
        let launch = ctx.accounts.state.launch;
        if args.source_owner == launch || args.destination_owner == launch {
            return Ok(HookReturn::default());
        }
        let now = stamp(Clock::get()?.unix_timestamp);
        let from_attester = attester_index(&args.source_owner);
        let what = kind(
            args.source_owner == facts.pool,
            args.destination_owner == facts.pool,
            args.source_owner.is_on_curve(),
            args.destination_owner.is_on_curve(),
            from_attester.is_some(),
            args.amount,
        );
        let mut src = Record::decode(&args.source_hook_data);
        let mut dst = Record::decode(&args.destination_hook_data);
        let state = &mut ctx.accounts.state;
        let (write_src, write_dst) = match what {
            Kind::Buy => {
                state.last_depth = depth(args.source_balance, &facts);
                state.trades = state.trades.saturating_add(1);
                on_buy(
                    &mut dst,
                    args.amount,
                    args.decimals,
                    args.destination_balance,
                    now,
                );
                (false, true)
            }
            Kind::Sell => {
                let pool_depth = depth(args.destination_balance, &facts);
                state.last_depth = pool_depth;
                state.trades = state.trades.saturating_add(1);
                on_sell(
                    &mut src,
                    args.amount,
                    args.decimals,
                    args.source_balance,
                    pool_depth,
                    false,
                    now,
                );
                (true, false)
            }
            Kind::SellElsewhere => {
                on_sell(
                    &mut src,
                    args.amount,
                    args.decimals,
                    args.source_balance,
                    state.last_depth,
                    true,
                    now,
                );
                (true, false)
            }
            Kind::Payment => {
                state.payments = state.payments.saturating_add(1);
                on_payment(
                    &mut src,
                    &mut dst,
                    args.amount,
                    args.decimals,
                    args.source_balance,
                    args.destination_balance,
                    now,
                );
                (true, true)
            }
            Kind::Mark => {
                let key = from_attester.unwrap_or(0);
                if !on_mark(&mut dst, args.amount, key, now) {
                    return Ok(HookReturn::default());
                }
                state.marks = state.marks.saturating_add(1);
                (false, true)
            }
            Kind::FromProgram => {
                on_from_program(&mut dst, args.destination_balance, now);
                (false, true)
            }
            Kind::Ignore => (false, false),
        };
        Ok(HookReturn {
            source_hook_data: write_src.then(|| src.encode()),
            destination_hook_data: write_dst.then(|| dst.encode()),
            ..HookReturn::default()
        })
    }
}

/// Which attester key `owner` is, if any.
pub fn attester_index(owner: &Pubkey) -> Option<u8> {
    ATTESTERS.iter().position(|a| a == owner).map(|i| i as u8)
}

/// 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.
pub fn depth(pool_balance: u64, facts: &LaunchFacts) -> u64 {
    if facts.on_curve {
        pool_balance.saturating_add(facts.virtual_base)
    } else {
        pool_balance
    }
}

/// What the hook reads of its mint's launch.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LaunchFacts {
    /// 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,
}

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

/// The launch's facts from the launchpad's `Launch` account for `mint`; `None` for anything else
/// (an account not written yet, as during the launch's own transaction).
fn read_launch_facts(launch: &AccountInfo, mint: &Pubkey) -> Option<LaunchFacts> {
    if *launch.owner != LAUNCH_PROGRAM {
        return None;
    }
    let data = launch.try_borrow_data().ok()?;
    let view = parse_launch(&data, mint)?;
    Some(LaunchFacts {
        pool: view.pool,
        on_curve: view.on_curve,
        virtual_base: virtual_base_of(&data)?,
    })
}

/// 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,
    /// 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,
    /// The pool's depth at its last trade, for sells into other programs.
    pub last_depth: u64,
    /// Buys and sells through the pool recorded.
    pub trades: u64,
    /// Wallet-to-wallet payments recorded.
    pub payments: u64,
    /// Attester marks recorded.
    pub marks: u64,
}

/// Accounts of the standard `prepare`.
#[derive(Accounts)]
pub struct Prepare<'info> {
    /// Pays the rent.
    #[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>,
}

/// 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/record.rs336 lines ›
//! Track Record's per-wallet record and the rules that update it, on plain bytes and integers.
//! Nothing here can panic.
//!
//! Every holding of the token keeps 64 bytes of hook data. This hook writes a wallet's record
//! there, before each transfer it is part of. The record holds facts, never a score: the score is
//! computed off chain from these facts by a published formula, so it can improve without changing
//! this program.
//!
//! Layout (little-endian):
//!
//! | Bytes  | Field               | Meaning                                                          |
//! |--------|---------------------|------------------------------------------------------------------|
//! | 0      | `version`           | 1                                                                |
//! | 1      | `flags`             | bit 0: ever dumped; bit 1: ever sent tokens into another program |
//! | 2..6   | `first_seen`        | when tokens first arrived (unix seconds)                         |
//! | 6..10  | `holding_since`     | start of the current unbroken holding; 0 after selling out        |
//! | 10..14 | `last_active`       | its last transfer                                                |
//! | 14..18 | `bought`            | whole tokens bought from the launch's pool                       |
//! | 18..22 | `sold`              | whole tokens sold into the pool or sent into another program     |
//! | 22..26 | `paid`              | whole tokens paid to other wallets                               |
//! | 26..30 | `received`          | whole tokens received from other wallets                         |
//! | 30..34 | `received_weighted` | the same, each payment weighted by how long its payer has held   |
//! | 34..36 | `payments_out`      | payments of a whole token or more made                           |
//! | 36..38 | `payments_in`       | payments of a whole token or more received                       |
//! | 38..40 | `dumps`             | sells of 1% of the pool's depth or more                          |
//! | 40..44 | `last_dump_at`      | when the last one was                                            |
//! | 44     | `attest_level`      | 0 never marked, else the last mark code: 1 identity, 2 autonomy, |
//! |        |                     | 3 revoked, 4..15 reserved for the reader to define               |
//! | 45     | `attest_key`        | which attester key wrote the mark                                |
//! | 46..50 | `attested_at`       | when (a revocation also writes its time)                         |
//! | 50..64 | `reserved`          | kept as found, for later hooks of the same family                |
//!
//! Totals are whole tokens, saturating at `u32::MAX`; counts saturate at `u16::MAX`.

/// Bytes of hook data a holding keeps.
pub const RECORD_LEN: usize = 64;
/// This layout.
pub const RECORD_VERSION: u8 = 1;

/// `flags`: the wallet has made a dump.
pub const FLAG_DUMPED: u8 = 1;
/// `flags`: the wallet has sent tokens into a program other than the launch's pool.
pub const FLAG_SOLD_ELSEWHERE: u8 = 2;

/// Basis points in a whole.
pub const BPS: u128 = 10_000;
/// A single sell of at least this share of the pool's depth is a dump: 1%.
pub const DUMP_BPS: u128 = 100;
/// A payment counts in full toward its payee's weighted total once its payer first got the token
/// this long ago (30 days); a newer payer's payment counts in proportion.
pub const FULL_WEIGHT_SECS: u64 = 30 * 86_400;

/// `attest_level`: never marked.
pub const ATTEST_NONE: u8 = 0;
/// `attest_level`: an attester vouched for who runs the wallet.
pub const ATTEST_IDENTITY: u8 = 1;
/// `attest_level`: an attester verified the wallet's key lives in an enclave running known code.
pub const ATTEST_AUTONOMOUS: u8 = 2;
/// `attest_level`: the attester withdrew its mark.
pub const ATTEST_REVOKED: u8 = 3;
/// What an attester sends, in base units, to mark a wallet: identity attested.
pub const MARK_IDENTITY: u64 = 1;
/// ... autonomy attested.
pub const MARK_AUTONOMOUS: u64 = 2;
/// ... the mark revoked.
pub const MARK_REVOKE: u64 = 3;
/// The highest mark code. Every code from 1 to this is a mark, and the code itself is stored as
/// `attest_level`; codes above 3 carry no meaning here, and the reader decides what they mean.
pub const MARK_MAX: u64 = 15;

/// A wallet's record.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Record {
    pub version: u8,
    pub flags: u8,
    pub first_seen: u32,
    pub holding_since: u32,
    pub last_active: u32,
    pub bought: u32,
    pub sold: u32,
    pub paid: u32,
    pub received: u32,
    pub received_weighted: u32,
    pub payments_out: u16,
    pub payments_in: u16,
    pub dumps: u16,
    pub last_dump_at: u32,
    pub attest_level: u8,
    pub attest_key: u8,
    pub attested_at: u32,
    pub reserved: [u8; 14],
}

fn u16_at(d: &[u8; RECORD_LEN], at: usize) -> u16 {
    u16::from_le_bytes([d[at], d[at + 1]])
}
fn u32_at(d: &[u8; RECORD_LEN], at: usize) -> u32 {
    u32::from_le_bytes([d[at], d[at + 1], d[at + 2], d[at + 3]])
}

impl Record {
    /// Reads a holding's hook data (all zeros for a wallet never seen).
    pub fn decode(d: &[u8; RECORD_LEN]) -> Record {
        let mut reserved = [0u8; 14];
        reserved.copy_from_slice(&d[50..64]);
        Record {
            version: d[0],
            flags: d[1],
            first_seen: u32_at(d, 2),
            holding_since: u32_at(d, 6),
            last_active: u32_at(d, 10),
            bought: u32_at(d, 14),
            sold: u32_at(d, 18),
            paid: u32_at(d, 22),
            received: u32_at(d, 26),
            received_weighted: u32_at(d, 30),
            payments_out: u16_at(d, 34),
            payments_in: u16_at(d, 36),
            dumps: u16_at(d, 38),
            last_dump_at: u32_at(d, 40),
            attest_level: d[44],
            attest_key: d[45],
            attested_at: u32_at(d, 46),
            reserved,
        }
    }

    /// The 64 bytes to write back, always as this layout's version.
    pub fn encode(&self) -> [u8; RECORD_LEN] {
        let mut d = [0u8; RECORD_LEN];
        d[0] = RECORD_VERSION;
        d[1] = self.flags;
        d[2..6].copy_from_slice(&self.first_seen.to_le_bytes());
        d[6..10].copy_from_slice(&self.holding_since.to_le_bytes());
        d[10..14].copy_from_slice(&self.last_active.to_le_bytes());
        d[14..18].copy_from_slice(&self.bought.to_le_bytes());
        d[18..22].copy_from_slice(&self.sold.to_le_bytes());
        d[22..26].copy_from_slice(&self.paid.to_le_bytes());
        d[26..30].copy_from_slice(&self.received.to_le_bytes());
        d[30..34].copy_from_slice(&self.received_weighted.to_le_bytes());
        d[34..36].copy_from_slice(&self.payments_out.to_le_bytes());
        d[36..38].copy_from_slice(&self.payments_in.to_le_bytes());
        d[38..40].copy_from_slice(&self.dumps.to_le_bytes());
        d[40..44].copy_from_slice(&self.last_dump_at.to_le_bytes());
        d[44] = self.attest_level;
        d[45] = self.attest_key;
        d[46..50].copy_from_slice(&self.attested_at.to_le_bytes());
        d[50..64].copy_from_slice(&self.reserved);
        d
    }
}

/// Unix seconds as the record keeps them: never 0 (0 means "never"), never past `u32::MAX`.
pub fn stamp(now: i64) -> u32 {
    u32::try_from(now.max(1)).unwrap_or(u32::MAX)
}

/// `amount` base units in whole tokens, rounded down.
pub fn whole(amount: u64, decimals: u8) -> u64 {
    match 10u64.checked_pow(u32::from(decimals)) {
        Some(one) => amount / one,
        None => 0,
    }
}

fn add32(total: u32, more: u64) -> u32 {
    u32::try_from(u64::from(total).saturating_add(more)).unwrap_or(u32::MAX)
}

/// Whether a single sell of `amount` into a pool `depth` deep is a dump (1% of the depth or more).
/// An unknown depth (0) never makes one.
pub fn is_dump(amount: u64, depth: u64) -> bool {
    depth > 0 && u128::from(amount) * BPS >= u128::from(depth) * DUMP_BPS
}

/// How much of a `whole_amount` payment counts toward the payee's weighted total: in full once the
/// payer first got the token `FULL_WEIGHT_SECS` ago, in proportion before, nothing from a payer
/// with no record.
pub fn weighted(whole_amount: u64, payer: &Record, now: u32) -> u64 {
    if payer.first_seen == 0 {
        return 0;
    }
    let held = u64::from(now.saturating_sub(payer.first_seen)).min(FULL_WEIGHT_SECS);
    u64::try_from(u128::from(whole_amount) * u128::from(held) / u128::from(FULL_WEIGHT_SECS))
        .unwrap_or(u64::MAX)
}

/// Tokens arrived at a wallet that had `balance_before`.
fn arrive(rec: &mut Record, balance_before: u64, now: u32) {
    if rec.first_seen == 0 {
        rec.first_seen = now;
    }
    if balance_before == 0 || rec.holding_since == 0 {
        rec.holding_since = now;
    }
    rec.last_active = now;
}

/// `amount` left a wallet that had `balance_before`.
fn depart(rec: &mut Record, balance_before: u64, amount: u64, now: u32) {
    if rec.first_seen == 0 {
        // Tokens that arrived before the hook could read the launch (the launch's own transaction).
        rec.first_seen = now;
    }
    if amount >= balance_before {
        rec.holding_since = 0;
    }
    rec.last_active = now;
}

/// A buy of `amount` from the pool, by a wallet that had `balance_before`.
pub fn on_buy(rec: &mut Record, amount: u64, decimals: u8, balance_before: u64, now: u32) {
    arrive(rec, balance_before, now);
    rec.bought = add32(rec.bought, whole(amount, decimals));
}

/// A sell of `amount` into a pool `depth` deep (or, `elsewhere`, into another program, against the
/// pool's last known depth), by a wallet that had `balance_before`.
pub fn on_sell(
    rec: &mut Record,
    amount: u64,
    decimals: u8,
    balance_before: u64,
    depth: u64,
    elsewhere: bool,
    now: u32,
) {
    depart(rec, balance_before, amount, now);
    rec.sold = add32(rec.sold, whole(amount, decimals));
    if elsewhere {
        rec.flags |= FLAG_SOLD_ELSEWHERE;
    }
    if is_dump(amount, depth) {
        rec.flags |= FLAG_DUMPED;
        rec.dumps = rec.dumps.saturating_add(1);
        rec.last_dump_at = now;
    }
}

/// A payment of `amount` from one wallet to another. The weight comes from the payer's record as it
/// was before this payment.
#[allow(clippy::too_many_arguments)]
pub fn on_payment(
    payer: &mut Record,
    payee: &mut Record,
    amount: u64,
    decimals: u8,
    payer_balance_before: u64,
    payee_balance_before: u64,
    now: u32,
) {
    let w = whole(amount, decimals);
    let counted = weighted(w, payer, now);
    depart(payer, payer_balance_before, amount, now);
    arrive(payee, payee_balance_before, now);
    payer.paid = add32(payer.paid, w);
    payee.received = add32(payee.received, w);
    payee.received_weighted = add32(payee.received_weighted, counted);
    if w >= 1 {
        payer.payments_out = payer.payments_out.saturating_add(1);
        payee.payments_in = payee.payments_in.saturating_add(1);
    }
}

/// Tokens a program (a vault, a companion's payout, an airdrop contract) sent to a wallet.
pub fn on_from_program(rec: &mut Record, balance_before: u64, now: u32) {
    arrive(rec, balance_before, now);
}

/// An attester's mark: `amount` is the code (1 identity, 2 autonomy, 3 revoke, up to `MARK_MAX`),
/// stored as it is; `key` is the index of the attester key that sent it. Returns whether `amount`
/// was a mark at all. A mark is not a payment and starts no history.
pub fn on_mark(rec: &mut Record, amount: u64, key: u8, now: u32) -> bool {
    if !is_mark_code(amount) {
        return false;
    }
    rec.attest_level = amount as u8;
    rec.attest_key = key;
    rec.attested_at = now;
    true
}

/// Whether an attester sending `amount` base units is a mark (1 to `MARK_MAX`).
pub fn is_mark_code(amount: u64) -> bool {
    (MARK_IDENTITY..=MARK_MAX).contains(&amount)
}

/// What a transfer is, from where its tokens come from and go to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
    /// Out of the launch's pool to a wallet.
    Buy,
    /// From a wallet into the launch's pool.
    Sell,
    /// From a wallet into any other program (another pool, a vault, an escrow).
    SellElsewhere,
    /// From a wallet to a wallet.
    Payment,
    /// From an attester key to a wallet, of 1 to `MARK_MAX` base units.
    Mark,
    /// From a program (not the pool) to a wallet.
    FromProgram,
    /// Program to program, or the pool to a program: nothing to record.
    Ignore,
}

/// Classifies a transfer. A wallet is an address on the ed25519 curve (one a key can sign for).
pub fn kind(
    from_pool: bool,
    to_pool: bool,
    from_wallet: bool,
    to_wallet: bool,
    from_attester: bool,
    amount: u64,
) -> Kind {
    if from_pool {
        return if to_wallet { Kind::Buy } else { Kind::Ignore };
    }
    if to_pool {
        return if from_wallet {
            Kind::Sell
        } else {
            Kind::Ignore
        };
    }
    match (from_wallet, to_wallet) {
        (true, true) if from_attester && is_mark_code(amount) => {
            Kind::Mark
        }
        (true, true) => Kind::Payment,
        (true, false) => Kind::SellElsewhere,
        (false, true) => Kind::FromProgram,
        (false, false) => Kind::Ignore,
    }
}