Track Record
On-chain wallet track record, no fees
Runs inside every trade and transfer of the token.
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
Takes nothing from any trade or transfer
Every rule
When it applies, what it takes, and where that goes.
| On | When | What happens | At most | To |
|---|---|---|---|---|
| Buys | Buys | free; records whole tokens bought and when | 0% | — |
| Sells | Sells | free; a sell of 1% of pool depth or more is recorded as a dump | 0% | — |
| Transfers | Transfers | free; wallet to wallet is recorded as a payment, weighted over 30 days of holding | 0% | — |
| Transfers | Attester marks | 1 to 15 base units from either of two fixed keys set a mark code on the receiver | 0% | — |
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.
- 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
Track record
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,
}
}