Let him cook
Flip tax: up to 15% on quick sells
Runs inside every trade and transfer of the token.
What it does
Bordrless’s reading of the source, written 14 Oct, 08:15 UTC. Read by AI from the code, not an audit.
Sells into the launch pool pay up to 15% of the tokens sold. The fee is set by how new the tokens are and by profit on the curve. Buys and transfers are free.
Each buy is kept as a lot with its time; up to 3 lots per holding, newest sold first. A sell into the launch pool pays 15% of tokens under 1 hour old, falling to 0% at 3 hours. On the bonding curve it also computes 20% of the sale's profit, capped at 10% of the sale, and takes the larger of the two. Half goes to the launch's creator when it is whoever sent prepare, the rest locked in a seal no one can move. Nothing is taken until the seal's holding exists, and no transfer is ever refused.
The most it takes
Up to 15% of the tokens sold; nothing on buys or transfers
Every rule
When it applies, what it takes, and where that goes.
| On | When | What happens | At most | To |
|---|---|---|---|---|
| Buys | Buys | free; the tokens are stamped with their time and the curve price after the buy | 0% | — |
| Sells | Sells of tokens under 1 hour old | 15% of those tokens, half to the creator, half locked for ever in the seal | 15% | The creator |
| Sells | Sells of tokens 1 to 3 hours old | 15% at 1 hour, falling in a straight line to 0% at 3 hours, split the same way | 15% | The creator |
| Sells | Sells of tokens over 3 hours old | 0% age fee | 0% | — |
| Sells | Sells at a profit, on the curve | 20% of the profit in tokens, at most 10% of the sale; the larger of this and the age fee | 10% | The creator |
| Transfers | Transfers | free; the tokens keep their age and recorded price | 0% | — |
What to watch
Things a launcher and a buyer should know before they use it.
- lowOn a launch pool the DEX also takes 25% of the cut's value in SOL from the seller, so a quick dump costs about 18.75%.
- lowNo tax is taken until someone creates the seal's holding of the token after the launch.
- lowHalf of every tax is locked for ever, not burned: those tokens still count in the supply.
- lowOnly the launch pool is taxed: another pool or a wallet-to-wallet deal pays nothing.
- lowAfter graduation, adding liquidity with fresh tokens pays like a sale.
Security review
Sells into the launch pool pay up to 15% of the tokens sold. The fee is set by how new the tokens are and by profit on the curve. Buys and transfers are free.
- lowProfit tax reads virtualbase at a fixed offset (161) of the Launch account
src/lib.rs LAUNCHVIRTUALBASE = 161 is not checked against the launchpad's layout (the creator at 42 matches readlaunch's mint at 10). If the offset is wrong, curve prices are wrong and the profit tax is mis-charged. Read it through the launchpad's layout or pin it with a test.
- lowTax is skipped until the seal holding is created
src/lib.rs beforetransfer sets taxed only when the seal holding exists. Until someone calls createholding for the seal owner after the mint exists, every sell is free. The launch flow should create it right after launch.
- lowPrice uses the pool vault balance, which donations can move
curveprice is computed from the vault balance plus the virtual base, not the DEX's reserve. Tokens sent straight to the vault lower later sale prices and so the profit tax. Each donation is costly, and this is disclosed.
- lowAfter graduation liquidity moves are treated as trades
Adding liquidity is a transfer into the pool and pays the age fee on fresh tokens. Removing it stamps the tokens fresh like a buy. LPs selling soon after a removal pay up to 15%. This is disclosed in the description.
- infoCreator half depends on who sent prepare
The creator half is paid only when the launch's creator equals prepare's payer and that wallet's holding exists. If prepare was front-run, or the creator is a companion address, the whole tax goes to the locked seal. Nobody gains from it.
- infoSmall rounding and recorded-price edge cases favour the seller
bpsof rounds down, so tiny sells pay nothing. addcovered drops the recorded price on overflow. Unpriced tokens dilute the profit-tax share. Each effect is negligible or disclosed.
Checks
Approved 14 Oct, 08:16 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
179,776 bytes, hash fcac80a3496f684195b2269f3b8fcb33e49117479b20a5d2fcafcd6d41a3a4fa.
- 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 AIPassed
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
Quick flips pay, patience doesn’t. Tokens sold within an hour of being bought pay 15%, fading to nothing by the third hour. On the bonding curve, a seller pays 20% of their profit and a seller at a loss pays nothing. Half the tax is locked away for good, half goes to the token’s creator.
Source
The source Bordrless checked and built. Read it before you launch with it.
src/lib.rs396 lines ›
//! Let him cook: a Bordrless token hook that makes quick flips pay, and, on the curve, taxes only the
//! sellers who leave with a profit.
//!
//! - **The shield.** Tokens sold into the launch pool within an hour of arriving pay 15% of
//! themselves; over the next two hours that falls in a straight line to nothing. Each buy is
//! kept as a lot with its own time and pays at its own age, so buying again neither makes older
//! tokens younger nor gives new ones an older age; a wallet's newest tokens are sold first, so
//! holding old ones doesn't hide a quick flip; and each lot keeps its time when it is sent on,
//! so neither does another wallet. A holding keeps three lots: a fourth merges the two closest
//! in time into the newer one's time, which can only make tokens younger.
//! - **Only winners pay.** While the launch is on its bonding curve, each wallet's tokens
//! remember the curve's price right after they were bought. A sale that leaves the price
//! above that pays 20% of the profit, never more than 10% of the sale; any other sale pays
//! nothing. Once the launch graduates this ends: liquidity can then come and go, and a pool's
//! token balance no longer tells its price.
//! - A sale pays the larger of the two, never both. Buys and wallet-to-wallet sends pay nothing,
//! and nothing is ever refused.
//!
//! Half of the tax goes to the launch's creator, in tokens, and the other half to the mint's seal:
//! a holding owned by an address of this program that nothing can ever move out of, so those
//! tokens are locked, not burned, and the supply does not fall. The creator is whoever the
//! launchpad's own launch account names, read on every sale: `prepare` remembers who sent it, and
//! the creator's half is paid only while that is the launch's creator. Whoever sends `prepare`
//! first therefore gains nothing; if it was not the launch's creator, or the creator's holding of
//! the token does not exist, or the creator is the seller, that half goes to the seal too. Until
//! someone creates the seal's holding (the token program's `create_holding`, open to anyone once
//! the mint exists), no tax is taken. The hook keeps nothing of its own to change: its state is
//! written once, by `prepare`, and only read after.
//!
//! WHAT IT DOES NOT COVER, on purpose. Only sales into the launch pool are taxed: a sale through
//! another pool someone opens, or a private deal between wallets, pays nothing, and the tokens it
//! delivers carry no stamp. After graduation, adding liquidity is a transfer into the pool and pays
//! like a sale of fresh tokens, and removing it is stamped like a buy. Tokens that arrive with no
//! recorded price (bought off the curve, sent by a wallet that held some, or received as tax) lower
//! the share of a sale the profit tax can reach. Tokens sent straight to the pool's vault outside a
//! trade lower the price the hook computes, which lowers the profit tax. The price of a sale is
//! worked out as if all of it reached the pool, which is slightly in the seller's favour. On a
//! launch pool the DEX also takes 25% of the value of what the hook cuts, in SOL, from the seller.
//!
//! The rules are in [`rules`]; this file is the standard `prepare` and the one callback. The hook
//! calls nothing, moves no SOL and has no instruction but these two: nobody can change how it
//! behaves for a mint once it is prepared.
#![allow(unexpected_cfgs)]
use anchor_lang::prelude::*;
use bordrless_game::read_launch;
use bordrless_hook::{
hook_accounts_address, hook_signer, write_registry, AccountSource, Delta, ExtraAccount,
HookAccountList, HookReturn, Seed, TokenHookArgs, TokenOp, TOKEN_PREFIX_ACCOUNTS,
};
pub mod rules;
use rules::{curve_price, on_buy, on_sell, on_send, split};
declare_id!("De89mNABW7o17tycn2T3gTxvBk1yXcB56ieiRgKfGAFK");
/// The Bordrless token program: the only caller of the callback.
pub const TOKEN_PROGRAM: Pubkey =
Pubkey::from_str_const("2XoEWp8cF3kRXg74eVwPAyTFhVCAztn3V88komxAvr22");
/// The Bordrless launchpad.
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";
/// `["seal", mint]`: the owner of the mint's seal, an address of this program nobody signs for.
pub const SEAL_SEED: &[u8] = b"seal";
/// 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 seal's holding in the callback's accounts: the token program's five, the state,
/// the seal, the launch, the creator's holding.
pub const SEAL_INDEX: u8 = TOKEN_PREFIX_ACCOUNTS as u8 + 1;
/// Index of the creator's holding in the callback's accounts.
pub const CREATOR_INDEX: u8 = TOKEN_PREFIX_ACCOUNTS as u8 + 3;
/// Where `creator: Pubkey` sits in the launchpad's `Launch` account (discriminator, version,
/// bump, mint, then the creator).
pub const LAUNCH_CREATOR: usize = 42;
/// Where `virtual_base: u64` sits in the launchpad's `Launch` account (the discriminator first;
/// every field before it is fixed-size).
pub const LAUNCH_VIRTUAL_BASE: usize = 161;
/// The callback's extra accounts, after the token program's five: the state (read only), the
/// seal's holding of the mint, the launch (read for its pool, its curve and its creator) and the
/// holding of whoever sent `prepare` (paid the creator's half while the launch names them).
pub fn extra_accounts(seal_owner: &Pubkey, creator: &Pubkey) -> HookAccountList {
HookAccountList::new(vec![
ExtraAccount {
writable: false,
source: AccountSource::Pda {
program: crate::ID,
seeds: vec![Seed::Literal(STATE_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(seal_owner.to_bytes().to_vec()),
],
},
},
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(creator.to_bytes().to_vec()),
],
},
},
])
}
#[program]
pub mod let_him_cook {
use super::*;
/// The standard `prepare`: the state for `mint` and its registry. Anyone may send it, once
/// per mint. It remembers who sent it, but that is worth nothing unless the launch later names
/// the same address as its creator (the launch prepares its own Workshop hook).
pub fn prepare(ctx: Context<Prepare>) -> Result<()> {
let mint = ctx.accounts.mint.key();
let creator = ctx.accounts.payer.key();
let seal_owner = Pubkey::find_program_address(&[SEAL_SEED, mint.as_ref()], &crate::ID).0;
let state = &mut ctx.accounts.state;
state.version = 1;
state.bump = ctx.bumps.state;
state.mint = mint;
state.launch =
Pubkey::find_program_address(&[LAUNCH_SEED, mint.as_ref()], &LAUNCH_PROGRAM).0;
state.seal_owner = seal_owner;
state.seal_holding = Pubkey::find_program_address(
&[HOLDING_SEED, mint.as_ref(), seal_owner.as_ref()],
&TOKEN_PROGRAM,
)
.0;
state.creator = creator;
state.creator_holding = Pubkey::find_program_address(
&[HOLDING_SEED, mint.as_ref(), creator.as_ref()],
&TOKEN_PROGRAM,
)
.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(&seal_owner, &creator),
)
}
/// Before every transfer: a buy stamped with its time and its curve price, a send carrying
/// both on, a sell taxed.
pub fn before_transfer(ctx: Context<Callback>, args: TokenHookArgs) -> Result<HookReturn> {
check_call(&ctx, &args)?;
if args.op != TokenOp::Transfer {
return Ok(HookReturn::default());
}
// A transfer from a holding to itself moves nothing, so there is nothing to record. (The
// token program refuses one before it gets here; the hook does not rely on that.)
if args.source == args.destination {
return Ok(HookReturn::default());
}
let now = Clock::get()?.unix_timestamp;
let seal_ready =
*ctx.accounts.seal.owner == TOKEN_PROGRAM && !ctx.accounts.seal.data_is_empty();
let state = &ctx.accounts.state;
let curve = read_curve(&ctx.accounts.launch, &state.mint);
let pool = curve.map_or(Pubkey::default(), |c| c.pool);
let (launch, seal_owner) = (state.launch, state.seal_owner);
let known_pool = pool != Pubkey::default();
// The curve's virtual base, while the launch is on its curve: what turns the pool's token
// balance into a price.
let virtual_base = curve.filter(|c| c.on_curve).map(|c| c.virtual_base);
let (from, to) = (args.source_owner, args.destination_owner);
let apart = |owner: &Pubkey| *owner == launch || *owner == seal_owner;
// A buy: out of the pool, into a wallet.
if known_pool && from == pool {
if apart(&to) {
return Ok(HookReturn::default());
}
// The price a buy is recorded at is the highest it reaches: the curve's price once it
// is done. A buyer's own push on the price is never counted as their profit.
let price = virtual_base.and_then(|vb| {
let after = (u128::from(args.source_balance) + u128::from(vb))
.checked_sub(u128::from(args.amount))?;
curve_price(after, after)
});
let data = on_buy(
&args.destination_hook_data,
args.destination_balance,
args.amount,
now,
price,
);
return Ok(HookReturn {
destination_hook_data: Some(data),
..HookReturn::default()
});
}
// The launch's own deposits, and anything out of the seal, are nobody's trade.
if apart(&from) {
return Ok(HookReturn::default());
}
// A sell: out of a wallet, into the pool.
if known_pool && to == pool {
// The price a sale is judged at is the lowest it reaches: the curve's price once all of
// it is sold. So selling back what one just bought, or part of it, never shows a profit.
let price = virtual_base.and_then(|vb| {
let after = (u128::from(args.destination_balance) + u128::from(vb))
.checked_add(u128::from(args.amount))?;
curve_price(after, after)
});
let (tax, data) = on_sell(
&args.source_hook_data,
args.source_balance,
args.amount,
now,
price,
seal_ready,
);
// Half to the creator while the launch names the address `prepare` remembered, its
// holding exists and the seller is someone else; otherwise all of it to the seal.
let creator_paid = curve.is_some_and(|c| c.creator == state.creator)
&& args.source != state.creator_holding
&& *ctx.accounts.creator_holding.owner == TOKEN_PROGRAM
&& !ctx.accounts.creator_holding.data_is_empty();
let (to_creator, to_seal) = if creator_paid { split(tax) } else { (0, tax) };
// A delta must be above zero, so an untaxed part answers none.
let deltas = [(to_seal, SEAL_INDEX), (to_creator, CREATOR_INDEX)]
.into_iter()
.filter(|(amount, _)| *amount > 0)
.map(|(amount, account)| Delta { amount, account })
.collect();
return Ok(HookReturn {
deltas,
source_hook_data: Some(data),
..HookReturn::default()
});
}
// A send: the tokens' age and prices go with them.
let (source, destination) = on_send(
&args.source_hook_data,
args.source_balance,
&args.destination_hook_data,
args.destination_balance,
args.amount,
now,
);
Ok(HookReturn {
source_hook_data: Some(source),
destination_hook_data: (!apart(&to)).then_some(destination),
..HookReturn::default()
})
}
}
/// What the hook reads of its mint's launch.
#[derive(Clone, Copy)]
struct Curve {
/// The launch's pool.
pool: Pubkey,
/// Whether the launch is still on its bonding curve.
on_curve: bool,
/// The curve's virtual base reserve.
virtual_base: u64,
/// Who launched it.
creator: Pubkey,
}
/// Reads the launch of `mint`, once it can be read (not during its own transaction).
fn read_curve(launch: &AccountInfo, mint: &Pubkey) -> Option<Curve> {
let view = read_launch(launch, mint)?;
let data = launch.try_borrow_data().ok()?;
let mut bytes = [0u8; 8];
bytes.copy_from_slice(data.get(LAUNCH_VIRTUAL_BASE..LAUNCH_VIRTUAL_BASE + 8)?);
let mut creator = [0u8; 32];
creator.copy_from_slice(data.get(LAUNCH_CREATOR..LAUNCH_CREATOR + 32)?);
Some(Curve {
pool: view.pool,
on_curve: view.on_curve,
virtual_base: u64::from_le_bytes(bytes),
creator: Pubkey::new_from_array(creator),
})
}
/// 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(),
hook_signer(&TOKEN_PROGRAM, &crate::ID).0,
HookError::BadHookSigner
);
require_keys_eq!(args.mint, ctx.accounts.state.mint, HookError::WrongAccount);
Ok(())
}
/// The state for one mint, at `["state", mint]`.
#[account]
#[derive(InitSpace)]
pub struct ShieldState {
/// Layout version.
pub version: u8,
/// Bump.
pub bump: u8,
/// The mint.
pub mint: Pubkey,
/// The launchpad's `["launch", mint]`: its moves are never taxed.
pub launch: Pubkey,
/// The seal's owner, `["seal", mint]` under this program.
pub seal_owner: Pubkey,
/// The seal's holding of the mint: where the tax goes.
pub seal_holding: Pubkey,
/// Who sent `prepare`: paid the creator's half only while the launch names them its creator.
pub creator: Pubkey,
/// Their holding of the mint.
pub creator_holding: Pubkey,
}
/// 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 + ShieldState::INIT_SPACE, seeds = [STATE_SEED, mint.key().as_ref()], bump)]
pub state: Box<Account<'info, ShieldState>>,
/// 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(seeds = [STATE_SEED, mint.key().as_ref()], bump = state.bump)]
pub state: Box<Account<'info, ShieldState>>,
/// CHECK: the seal's holding of the mint (address-checked; it exists once someone creates it).
#[account(mut, address = state.seal_holding @ HookError::WrongAccount)]
pub seal: UncheckedAccount<'info>,
/// 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 holding of whoever sent `prepare` (address-checked; may not exist).
#[account(mut, address = state.creator_holding @ HookError::WrongAccount)]
pub creator_holding: UncheckedAccount<'info>,
}
/// Errors.
#[error_code]
pub enum HookError {
#[msg("the hook signer is not the token program's signer for this hook")]
BadHookSigner,
#[msg("an account is not the one this mint's hook expects")]
WrongAccount,
}
src/rules.rs366 lines ›
//! The rules, as plain functions of a holding's 64 bytes of hook data: no accounts and no clock
//! of their own, so they can be tested without a validator.
//!
//! A holding's hook data, once this hook has written it: `"ESC2"`, then `price` and `covered`
//! (little-endian u64), then up to [`LOTS`] lots of fresh tokens, each `since` (unix seconds,
//! little-endian u32) and `amount` (little-endian u64), newest first. All zeros is a holding with
//! nothing fresh and no recorded price.
//!
//! - **Fresh tokens** are the ones bought or received within the last three hours, kept in lots,
//! each with the time it arrived. Each lot pays the shield at its own age, so buying again never
//! makes older tokens younger, and new tokens never take an older age. Selling takes the newest
//! lots first. When a holding has more lots than fit, the two closest in time are merged and the
//! merged lot keeps the newer time: a merge can only make tokens younger, never older, and buys
//! made close together (the usual case) lose next to nothing.
//! - **Covered tokens** are the ones whose buying price on the curve is known: `covered` of them,
//! at `price` on average, each buy counted at the curve's price right after it. A sale that
//! leaves the curve's price above that pays the profit tax.
//!
//! Both travel with the tokens when a wallet sends them on, each lot with its own time, so moving
//! them to another wallet changes nothing.
use bordrless_hook::HOOK_DATA_LEN;
/// The first bytes of a holding's hook data once this hook has written it: layout 2.
pub const MAGIC: [u8; 4] = *b"ESC2";
/// How many lots of fresh tokens a holding keeps.
pub const LOTS: usize = 3;
/// Where the lots start in the hook data, and how long each is.
const LOTS_AT: usize = 20;
const LOT_LEN: usize = 12;
/// Basis points.
pub const BPS: u64 = 10_000;
/// The shield: what selling fresh tokens pays, in basis points of them (15%).
pub const SHIELD_BPS: u64 = 1_500;
/// The shield is whole for this long after the tokens arrive (an hour).
pub const SHIELD_FLAT_SECS: i64 = 3_600;
/// Then it falls in a straight line to nothing over this long (two hours).
pub const SHIELD_DECAY_SECS: i64 = 7_200;
/// The profit tax, on the curve: this share of a sale's profit, in basis points (20%).
pub const PROFIT_BPS: u64 = 2_000;
/// The profit tax never takes more than this share of the tokens sold (10%).
pub const PROFIT_CAP_BPS: u64 = 1_000;
/// The scale of a curve price: `PRICE_SCALE / ((base + virtual base) before · after)`.
pub const PRICE_SCALE: u128 = 1_000_000_000_000_000_000_000_000_000_000_000_000;
/// Fresh tokens that arrived together.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Lot {
/// When they arrived (unix seconds).
pub since: u32,
/// How many.
pub amount: u64,
}
/// What a holding's hook data says.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Wallet {
/// The fresh lots, newest first; empty ones (amount 0) at the end.
pub lots: [Lot; LOTS],
/// The average curve price of its covered tokens.
pub price: u64,
/// How many of its tokens have a recorded price.
pub covered: u64,
}
/// A clock reading as stored in a lot. Times before 1970 or after 2106 do not occur on Solana.
fn stamp(now: i64) -> u32 {
now.clamp(0, i64::from(u32::MAX)) as u32
}
impl Wallet {
/// Reads a holding of `balance` tokens; anything this hook did not write is an empty record,
/// and nothing is ever counted beyond the balance (a burn leaves the record behind).
pub fn decode(data: &[u8; HOOK_DATA_LEN], balance: u64) -> Self {
if data[..4] != MAGIC {
return Self::default();
}
let u64_at = |at: usize| {
let mut b = [0u8; 8];
b.copy_from_slice(&data[at..at + 8]);
u64::from_le_bytes(b)
};
let mut w = Self {
lots: [Lot::default(); LOTS],
price: u64_at(4),
covered: u64_at(12).min(balance),
};
// The lots, newest first, never adding up to more than the balance.
let mut left = balance;
for (i, lot) in w.lots.iter_mut().enumerate() {
let at = LOTS_AT + i * LOT_LEN;
let mut s = [0u8; 4];
s.copy_from_slice(&data[at..at + 4]);
let amount = u64_at(at + 4).min(left);
left -= amount;
*lot = Lot {
since: u32::from_le_bytes(s),
amount,
};
}
w.tidy();
w
}
/// The hook data of this record; an empty one is all zeros, so the holding can be closed.
pub fn encode(&self) -> [u8; HOOK_DATA_LEN] {
let mut data = [0u8; HOOK_DATA_LEN];
if self.fresh() == 0 && self.covered == 0 {
return data;
}
data[..4].copy_from_slice(&MAGIC);
data[4..12].copy_from_slice(&self.price.to_le_bytes());
data[12..20].copy_from_slice(&self.covered.to_le_bytes());
for (i, lot) in self.lots.iter().enumerate() {
let at = LOTS_AT + i * LOT_LEN;
data[at..at + 4].copy_from_slice(&lot.since.to_le_bytes());
data[at + 4..at + 12].copy_from_slice(&lot.amount.to_le_bytes());
}
data
}
/// How many of its tokens are fresh.
pub fn fresh(&self) -> u64 {
self.lots.iter().map(|l| l.amount).sum()
}
/// Empty lots to the end, the rest newest first.
fn tidy(&mut self) {
for lot in self.lots.iter_mut().filter(|l| l.amount == 0) {
lot.since = 0;
}
self.lots
.sort_by(|a, b| (b.amount > 0).cmp(&(a.amount > 0)).then(b.since.cmp(&a.since)));
}
/// Forgets lots the shield no longer covers at `now`.
fn settle(&mut self, now: i64) {
for lot in self.lots.iter_mut() {
if lot.amount > 0 && shield_bps(now.saturating_sub(i64::from(lot.since))) == 0 {
lot.amount = 0;
}
}
self.tidy();
if self.covered == 0 {
self.price = 0;
}
}
/// Takes in `amount` fresh tokens that arrived at `since`. A lot from the same second joins
/// it; otherwise it is a lot of its own, and if that is one too many, the two lots closest in
/// time are merged into the newer one's time.
fn add_fresh(&mut self, amount: u64, since: u32) {
if amount == 0 {
return;
}
if let Some(lot) = self.lots.iter_mut().find(|l| l.amount > 0 && l.since == since) {
lot.amount = lot.amount.saturating_add(amount);
return;
}
let mut all = [Lot::default(); LOTS + 1];
all[..LOTS].copy_from_slice(&self.lots);
all[LOTS] = Lot { since, amount };
let mut n = all.iter().filter(|l| l.amount > 0).count();
all.sort_by(|a, b| (b.amount > 0).cmp(&(a.amount > 0)).then(b.since.cmp(&a.since)));
if n > LOTS {
// `all` is newest first; the closest pair is (i, i + 1), and i is the newer one.
let i = (0..n - 1)
.min_by_key(|&i| all[i].since - all[i + 1].since)
.unwrap_or(0);
all[i].amount = all[i].amount.saturating_add(all[i + 1].amount);
all.copy_within(i + 2..n, i + 1);
n -= 1;
all[n] = Lot::default();
}
self.lots.copy_from_slice(&all[..LOTS]);
}
/// Takes `amount` of its fresh tokens away, newest first, and answers them as lots.
fn take_fresh(&mut self, amount: u64) -> [Lot; LOTS] {
let mut taken = [Lot::default(); LOTS];
let mut left = amount;
for (lot, out) in self.lots.iter_mut().zip(taken.iter_mut()) {
let part = lot.amount.min(left);
if part == 0 {
continue;
}
*out = Lot {
since: lot.since,
amount: part,
};
lot.amount -= part;
left -= part;
}
self.tidy();
taken
}
/// Takes in `amount` tokens bought at `price`.
fn add_covered(&mut self, amount: u64, price: u64) {
if amount == 0 || price == 0 {
return;
}
let total = u128::from(self.covered) + u128::from(amount);
let sum = u128::from(self.covered)
.checked_mul(u128::from(self.price))
.and_then(|a| {
u128::from(amount)
.checked_mul(u128::from(price))
.and_then(|b| a.checked_add(b))
});
// A price that can't be averaged is not recorded: tokens without one pay no profit tax.
if let Some(average) = sum.and_then(|s| u64::try_from(s / total).ok()) {
self.price = average;
self.covered = self.covered.saturating_add(amount);
}
}
}
/// `a · b / d`, rounded down; `None` on overflow or a zero divisor.
pub fn mul_div(a: u128, b: u128, d: u128) -> Option<u128> {
if d == 0 {
return None;
}
a.checked_mul(b).map(|p| p / d)
}
/// The shield on tokens `age` seconds old, in basis points: whole for [`SHIELD_FLAT_SECS`], then
/// falling in a straight line to nothing over [`SHIELD_DECAY_SECS`].
pub fn shield_bps(age: i64) -> u64 {
if age < SHIELD_FLAT_SECS {
return SHIELD_BPS;
}
let into = age - SHIELD_FLAT_SECS;
if into >= SHIELD_DECAY_SECS {
return 0;
}
SHIELD_BPS * (SHIELD_DECAY_SECS - into) as u64 / SHIELD_DECAY_SECS as u64
}
/// The shield on `lots` sold at `now`: each lot at its own age.
pub fn shield_tax(lots: &[Lot], now: i64) -> u64 {
lots.iter()
.map(|l| bps_of(l.amount, shield_bps(now.saturating_sub(i64::from(l.since)))))
.fold(0u64, u64::saturating_add)
}
/// The curve's average price of a trade that moves the pool's effective base (its tokens plus
/// the curve's virtual base) from `before` to `after`: on a constant-product curve the quote paid
/// per token is `k / (before · after)`, and `k` never changes on the curve, so this is a price up
/// to that constant. With both the same it is the curve's price at that point, which is what the
/// hook records and compares: the price a trade leaves behind. `None` when it can't be told.
pub fn curve_price(before: u128, after: u128) -> Option<u64> {
let product = before.checked_mul(after)?;
if product == 0 {
return None;
}
u64::try_from(PRICE_SCALE / product).ok().filter(|p| *p > 0)
}
/// A buy: `amount` tokens out of the pool into a holding of `balance`, at `now`, at the curve
/// price `price` when the launch is on its curve. Answers the holding's new hook data.
pub fn on_buy(
data: &[u8; HOOK_DATA_LEN],
balance: u64,
amount: u64,
now: i64,
price: Option<u64>,
) -> [u8; HOOK_DATA_LEN] {
let mut w = Wallet::decode(data, balance);
w.settle(now);
w.add_fresh(amount, stamp(now));
if let Some(price) = price {
w.add_covered(amount, price);
}
w.encode()
}
/// A send of `amount` from a holding of `balance` to a holding of `to_balance`: the newest
/// tokens go first, each lot with its own time, and a matching share of the recorded prices.
/// Answers the sender's and the receiver's new hook data.
pub fn on_send(
from: &[u8; HOOK_DATA_LEN],
balance: u64,
to: &[u8; HOOK_DATA_LEN],
to_balance: u64,
amount: u64,
now: i64,
) -> ([u8; HOOK_DATA_LEN], [u8; HOOK_DATA_LEN]) {
let mut s = Wallet::decode(from, balance);
let mut d = Wallet::decode(to, to_balance);
s.settle(now);
d.settle(now);
let covered = share(amount, s.covered, balance);
for lot in s.take_fresh(amount) {
d.add_fresh(lot.amount, lot.since);
}
d.add_covered(covered, s.price);
s.covered -= covered;
if balance <= amount {
s = Wallet::default();
}
s.settle(now);
(s.encode(), d.encode())
}
/// A sell of `amount` out of a holding of `balance`, at `now`, at the curve price `price` when
/// the launch is on its curve. `taxed` says whether the tax has somewhere to go; until it has,
/// none is taken. Answers the tax, in tokens, and the seller's new hook data.
pub fn on_sell(
data: &[u8; HOOK_DATA_LEN],
balance: u64,
amount: u64,
now: i64,
price: Option<u64>,
taxed: bool,
) -> (u64, [u8; HOOK_DATA_LEN]) {
let mut w = Wallet::decode(data, balance);
w.settle(now);
let covered = share(amount, w.covered, balance);
let shield = shield_tax(&w.take_fresh(amount), now);
let profit = price.map_or(0, |p| profit_tax(amount, covered, w.price, p));
w.covered -= covered;
if balance <= amount {
w = Wallet::default();
}
w.settle(now);
let tax = if taxed { shield.max(profit).min(amount) } else { 0 };
(tax, w.encode())
}
/// The profit tax on a sale of `amount` tokens at `sold_at`, `covered` of them bought at
/// `bought_at`: [`PROFIT_BPS`] of the profit, in tokens, never more than [`PROFIT_CAP_BPS`] of the
/// sale. Tokens without a recorded price, and a sale at or below it, pay none.
pub fn profit_tax(amount: u64, covered: u64, bought_at: u64, sold_at: u64) -> u64 {
if covered == 0 || bought_at == 0 || sold_at <= bought_at {
return 0;
}
let gain = u128::from(sold_at - bought_at);
let tax = mul_div(u128::from(covered), gain, u128::from(sold_at))
.and_then(|profit| mul_div(profit, u128::from(PROFIT_BPS), u128::from(BPS)))
.and_then(|t| u64::try_from(t).ok())
.unwrap_or(0);
tax.min(bps_of(amount, PROFIT_CAP_BPS))
}
/// `bps` of `amount`, rounded down.
pub fn bps_of(amount: u64, bps: u64) -> u64 {
(u128::from(amount) * u128::from(bps) / u128::from(BPS)) as u64
}
/// The part of `amount`, out of `balance`, that comes from the `covered` tokens: the same share.
fn share(amount: u64, covered: u64, balance: u64) -> u64 {
if balance == 0 {
return 0;
}
let part = u128::from(amount.min(balance)) * u128::from(covered) / u128::from(balance);
(part as u64).min(covered)
}
/// How a tax of `tax` tokens is split: half to the launch's creator, the rest to the seal.
/// Answers (creator, seal).
pub fn split(tax: u64) -> (u64, u64) {
let creator = tax / 2;
(creator, tax - creator)
}