Flat Cut
1% of sells and transfers to the launcher
Runs inside every trade and transfer of the token.
What it does
Bordrless’s reading of the source, written 15 Oct, 01:14 UTC. Read by AI from the code, not an audit.
Takes 1% of every sell and wallet-to-wallet transfer, and of every buy once the coin graduates, and sends it to the launcher's wallet.
Every sell, every transfer between wallets, and every buy after the coin graduates pays 1% of the amount moved. The cut is rounded down, so transfers of under 100 of the coin's smallest units pay nothing. The cut goes to the wallet that set up the hook, but only once that wallet holds some of the coin and only if it is the launch's creator. Otherwise nothing is ever taken. Buys on the bonding curve are free. The launcher's own trades are free. It never refuses a transfer and nothing about it can change after launch.
The most it takes
1% of each sell, transfer and post-graduation buy
Every rule
When it applies, what it takes, and where that goes.
| On | When | What happens | At most | To |
|---|---|---|---|---|
| Buys | Buys on the curve | Nothing is taken | 0% | — |
| Buys | Buys after graduation | 1% of each buy, to the launcher's wallet | 1% | A wallet |
| Sells | Sells | 1% of each sell, to the launcher's wallet | 1% | A wallet |
| Trades | The launcher's own trades | Nothing is taken | 0% | — |
| Transfers | Transfers | 1% of each transfer between wallets, to the launcher's wallet | 1% | A wallet |
What to watch
Things a launcher and a buyer should know before they use it.
- lowThe launcher's wallet receives 1% of most transfers and pays nothing on its own trades.
- lowNothing is taken until the launcher's wallet holds some of the coin.
- lowOn a sell, Bordrless takes 25% of the cut's value on top, so sellers pay about 1.25%.
- lowTrades on any other pool for the coin pay 1% too, buys included.
- lowIf anyone but the launch's creator sets the hook up for the coin, it takes nothing, ever.
Security review
Takes 1% of every sell and wallet-to-wallet transfer, and of every buy once the coin graduates, and sends it to the launcher's wallet.
- lowAnyone who sends prepare first becomes the launcher, which can switch the cut off for good
src/lib.rs prepare records the payer as the launcher. confirmlauncher only ever turns cuts off when that payer is not the launch's creator, so a front-runner cannot redirect the cut. They can only disable it. This needs the mint address before it is broadcast, which the site keeps secret. One fix is to require the mint to sign prepare.
- infoCuts on buys after graduation are charged to the coin's pool
On a buy after graduation the cut comes from the output. The DEX sets Bordrless's 25% share of it aside from the pool's SOL side (hooks-v2 §3.1), so locked liquidity shrinks slightly with each such buy. This is by design, but it is the same effect the curve-buy exemption avoids.
- infoThe launcher's exemption is structural and disclosed
The token program refuses a cut that goes to either side of its own transfer, so the launcher's trades pay nothing. launcherholdingcantake handles this and the description states it.
- infoA companion launch never pays the cut
On a companion launch the creator is the companion address, not the wallet that sent prepare. nextlaunchercheck therefore turns cuts off permanently for that mint.
- infoLaunch account offsets match the launchpad layout
src/math.rs reads the creator at offset 42, between the mint (10) and the pool (74). This matches the Launch layout the other starters read, and parselaunch checks the mint.
Checks
Approved 13 Oct, 08:48 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
165,264 bytes, hash 1b3a3199e50fe50df3fed681e1124e2bc30ae3137241a6c138b9e8fddef5a929.
- Trading testPassed
Bought, sold and sent by test wallets over a simulated year; the largest cut it took was 1%.
- What it does, read by AIPassed
What the trading test covered
- Simulated a launch at unix time 1791849600 with a 100 bps creator fee: the creator and 6 fresh wallets per round bought, sent to each other, partly sold and sold out at +31s, +1h, +1d, +7d, +30d, +365d, then 50 more trades and a last round.
- Not caught: a rule triggered by a specific amount or by a condition the simulation never reproduces (a price, a supply, a balance or a number of trades it does not reach).
- Not caught: a rule aimed at specific hard-coded wallets (the simulation's creator and traders are its own keys, not the real ones).
- Not caught: anything after +365 days, and behaviour after graduation (the simulation trades on the launch pool and never fills the curve).
- Not caught: what an upgrade would change. Bordrless keeps the hook's upgrade authority; a new binary is simulated again before it is deployed.
On chain
Track record
1% fee on sells and transfers to the launcher. Curve buys pay nothing so coins graduate. Simple, predictable, no surprises.
Source
The source Bordrless checked and built. Read it before you launch with it.
src/lib.rs340 lines ›
//! Flat Cut: a Bordrless token hook that takes 1% of every sell and wallet-to-wallet transfer of
//! the token, and of every buy once the token has graduated, and sends it, as tokens, to the
//! launcher's own holding of the token.
//!
//! Who the launcher is: the wallet that sends this hook's `prepare` for the mint. The launch page
//! sends `prepare` from the launcher's wallet, in the launch's own transaction. Once the launch
//! exists, the hook also checks that this wallet is the launch's creator on the launchpad. If it is
//! not, the hook takes nothing for that mint, ever.
//!
//! What it takes: 1% of the amount of each transfer (a sell, a wallet-to-wallet send, or a buy
//! once the token has graduated), rounded down, so a transfer of under 100 base units pays
//! nothing. The token program takes the cut from what the receiver gets. The hook takes nothing:
//! - while the launcher has no holding of the token yet (it exists once they first hold it);
//! - when the launcher's holding is the sender or the receiver of the transfer (the token program
//! never lets a cut go to either side of its own transfer, so the launcher's own trades are free);
//! - on the launchpad's own moves of the token (to or from the launch account, at graduation);
//! - on buys while the token is still on its launch curve. Bordrless's share of a cut on a curve
//! buy comes out of the curve's SOL reserve, which would leave the curve short of its graduation
//! threshold and could keep the token from graduating. Once it has graduated, buys pay too;
//! - until the launch account can be read, and never if its creator is not the launcher.
//!
//! It never refuses a transfer, keeps no data in holdings, and has no instruction besides
//! `prepare` and its callback: nobody can change what it does after `prepare`.
#![allow(unexpected_cfgs)]
use anchor_lang::prelude::*;
use bordrless_game::{parse_launch, read_holding};
use bordrless_hook::{
hook_accounts_address, hook_signer, write_registry, AccountSource, Delta, ExtraAccount,
HookAccountList, HookReturn, Seed, TokenHookArgs, TokenOp, TOKEN_PREFIX_ACCOUNTS,
};
mod math;
pub use math::*;
declare_id!("Gd6d9PmqSQdMKGxLm54pAyUi5n7eqsLa75mDohA2WsDy");
/// The cut: 1% of each transfer that pays (no curve buys), in basis points.
pub const CUT_BPS: u64 = 100;
/// The flags a launch config names for this hook: `before_transfer` (1), which may answer a cut
/// (64).
pub const FLAGS: u16 = 65;
/// The Bordrless token program: the only caller of the callbacks, and the owner of every holding.
pub const TOKEN_PROGRAM: Pubkey =
Pubkey::from_str_const("2XoEWp8cF3kRXg74eVwPAyTFhVCAztn3V88komxAvr22");
/// The Bordrless launchpad: its `["launch", mint]` account names the launch's creator.
pub const LAUNCH_PROGRAM: Pubkey =
Pubkey::from_str_const("1jcBymHxBjniZDhNPy51Vgm5Nz7pLUdxa9UBHc4TavC");
/// `["state", mint]`: this hook's state for a mint.
pub const STATE_SEED: &[u8] = b"state";
/// The launchpad's `["launch", mint]` seed.
pub const LAUNCH_SEED: &[u8] = b"launch";
/// The token program's `["holding", mint, owner]` seed.
pub const HOLDING_SEED: &[u8] = b"holding";
/// Index of the launcher's holding in a callback's accounts: the token program's five, then the
/// registry's extras (the state, the launch, the launcher's holding).
pub const LAUNCHER_HOLDING_INDEX: u8 = TOKEN_PREFIX_ACCOUNTS as u8 + 2;
/// The callbacks' extra accounts, after the token program's five: this hook's state (writable, for
/// the running total), the mint's launch (read for its creator, its pool and whether it is on its
/// curve), and the launcher's holding of the mint (writable: where the cut goes). None depends on
/// who sends or receives.
pub fn extra_accounts(launcher: &Pubkey) -> HookAccountList {
HookAccountList::new(vec![
ExtraAccount {
writable: true,
source: AccountSource::Pda {
program: crate::ID,
seeds: vec![Seed::Literal(STATE_SEED.to_vec()), Seed::Account(1)],
},
},
ExtraAccount {
writable: false,
source: AccountSource::Pda {
program: LAUNCH_PROGRAM,
seeds: vec![Seed::Literal(LAUNCH_SEED.to_vec()), Seed::Account(1)],
},
},
ExtraAccount {
writable: true,
source: AccountSource::Pda {
program: TOKEN_PROGRAM,
seeds: vec![
Seed::Literal(HOLDING_SEED.to_vec()),
Seed::Account(1),
Seed::Literal(launcher.to_bytes().to_vec()),
],
},
},
])
}
#[program]
pub mod flat_cut {
use super::*;
/// The standard `prepare`: the state for `mint`, with the sender as the launcher, and the
/// registry. Anyone may send it, once per mint; the launch sends it from the launcher's wallet.
pub fn prepare(ctx: Context<Prepare>) -> Result<()> {
let mint = ctx.accounts.mint.key();
let launcher = ctx.accounts.payer.key();
let state = &mut ctx.accounts.state;
state.version = 1;
state.bump = ctx.bumps.state;
state.mint = mint;
state.launcher = launcher;
state.launcher_holding = holding_address(&mint, &launcher);
state.launch =
Pubkey::find_program_address(&[LAUNCH_SEED, mint.as_ref()], &LAUNCH_PROGRAM).0;
state.hook_signer = hook_signer(&TOKEN_PROGRAM, &crate::ID).0;
state.launcher_check = LAUNCHER_UNCHECKED;
state.collected_total = 0;
let (registry, bump) = hook_accounts_address(&crate::ID, &mint);
require_keys_eq!(
ctx.accounts.registry.key(),
registry,
HookError::WrongAccount
);
write_registry(
&ctx.accounts.payer.to_account_info(),
&ctx.accounts.registry.to_account_info(),
&ctx.accounts.system_program.to_account_info(),
&crate::ID,
&mint,
bump,
&extra_accounts(&launcher),
)
}
/// Before every transfer: 1% of the amount to the launcher's holding when it can take it; none
/// on a buy while the token is on its curve.
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 facts = read_launch_facts(&ctx.accounts.launch, &ctx.accounts.state.mint);
let launcher_ok = confirm_launcher(&mut ctx.accounts.state, facts.as_ref());
let holding_ok =
launcher_holding_can_take(&ctx.accounts.launcher_holding, &ctx.accounts.state, &args);
// No cut on a buy while the token is on its curve (see the top of this file).
let curve_buy = is_curve_buy(facts.as_ref(), &args);
let state = &mut ctx.accounts.state;
let cut =
if launcher_ok && holding_ok && !curve_buy && !is_launch_move(&state.launch, &args) {
cut_of(args.amount, CUT_BPS)
} else {
0
};
// A cut must be above zero, so a free transfer answers none.
if cut == 0 {
return Ok(HookReturn::default());
}
// A running total of what the launcher received, informational: it never blocks anything.
state.collected_total = state.collected_total.saturating_add(u128::from(cut));
Ok(HookReturn {
deltas: vec![Delta {
amount: cut,
account: LAUNCHER_HOLDING_INDEX,
}],
..HookReturn::default()
})
}
}
/// The token program's holding of `owner` for `mint`.
pub fn holding_address(mint: &Pubkey, owner: &Pubkey) -> Pubkey {
Pubkey::find_program_address(
&[HOLDING_SEED, mint.as_ref(), owner.as_ref()],
&TOKEN_PROGRAM,
)
.0
}
/// Whether the transfer is one of the launchpad's own moves of the token (to or from the launch).
pub fn is_launch_move(launch: &Pubkey, args: &TokenHookArgs) -> bool {
args.source_owner == *launch || args.destination_owner == *launch
}
/// Whether `holding` is either side of the transfer, which the token program never lets a cut name.
pub fn is_party(holding: &Pubkey, args: &TokenHookArgs) -> bool {
args.source == *holding || args.destination == *holding
}
/// What the hook reads of its mint's launch: its creator, its pool, and whether it is still on its
/// bonding curve.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LaunchFacts {
/// Who launched it.
pub creator: Pubkey,
/// The launch's pool on the DEX.
pub pool: Pubkey,
/// Whether it is still on its bonding curve (not graduated).
pub on_curve: bool,
}
/// The launch's facts, from the launchpad's `Launch` account data for `mint`; `None` for anything
/// else (an account not written yet, as during the launch's own transaction).
pub fn launch_facts(data: &[u8], mint: &Pubkey) -> Option<LaunchFacts> {
let view = parse_launch(data, mint)?;
let creator = Pubkey::new_from_array(creator_bytes(data)?);
Some(LaunchFacts {
creator,
pool: view.pool,
on_curve: view.on_curve,
})
}
/// [`launch_facts`] from the launch account: owned by the launchpad, its data readable.
fn read_launch_facts(launch: &AccountInfo, mint: &Pubkey) -> Option<LaunchFacts> {
if *launch.owner != LAUNCH_PROGRAM {
return None;
}
let data = launch.try_borrow_data().ok()?;
launch_facts(&data, mint)
}
/// Whether the transfer is a buy on the launch's curve: out of the pool while it is on its curve.
pub fn is_curve_buy(facts: Option<&LaunchFacts>, args: &TokenHookArgs) -> bool {
match facts {
Some(f) => curve_buy(f.on_curve, f.pool.to_bytes(), args.source_owner.to_bytes()),
None => false,
}
}
/// Confirms, once the launch can be read, that the launcher is its creator, and remembers it.
fn confirm_launcher(state: &mut HookState, facts: Option<&LaunchFacts>) -> bool {
let (check, on) = next_launcher_check(
state.launcher_check,
facts.map(|f| f.creator.to_bytes()),
state.launcher.to_bytes(),
);
state.launcher_check = check;
on
}
/// Whether the launcher's holding can take a cut of this transfer: it exists, is of this mint, is
/// not frozen, and is neither side of the transfer.
fn launcher_holding_can_take(
holding: &AccountInfo,
state: &HookState,
args: &TokenHookArgs,
) -> bool {
if is_party(&state.launcher_holding, args) {
return false;
}
match read_holding(holding) {
Some(view) => view.mint == state.mint && !view.frozen,
None => false,
}
}
/// Every callback: signed by the token program's signer for this hook, about this state's mint.
fn check_call(ctx: &Context<Callback>, args: &TokenHookArgs) -> Result<()> {
let signer = &ctx.accounts.hook_signer;
require!(signer.is_signer, HookError::BadHookSigner);
require_keys_eq!(
signer.key(),
ctx.accounts.state.hook_signer,
HookError::BadHookSigner
);
require_keys_eq!(args.mint, ctx.accounts.state.mint, HookError::WrongAccount);
Ok(())
}
/// The hook's state for one mint, at `["state", mint]`.
#[account]
#[derive(InitSpace)]
pub struct HookState {
/// Layout version.
pub version: u8,
/// Bump.
pub bump: u8,
/// The mint.
pub mint: Pubkey,
/// Who sent `prepare`: the launcher, who receives the cut.
pub launcher: Pubkey,
/// The launcher's holding of the mint: `["holding", mint, launcher]` under the token program.
pub launcher_holding: Pubkey,
/// The launchpad's `["launch", mint]`.
pub launch: Pubkey,
/// The token program's signer for this hook: `["hook-authority", this program]` under it.
pub hook_signer: Pubkey,
/// Whether the launcher is the launch's creator (`LAUNCHER_*`).
pub launcher_check: u8,
/// Everything the launcher has received from the cut.
pub collected_total: u128,
}
/// Accounts of the standard `prepare`.
#[derive(Accounts)]
pub struct Prepare<'info> {
/// Pays the rent, and is the launcher.
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: the mint the hook is prepared for; need not exist yet.
pub mint: UncheckedAccount<'info>,
#[account(init, payer = payer, space = 8 + HookState::INIT_SPACE, seeds = [STATE_SEED, mint.key().as_ref()], bump)]
pub state: Box<Account<'info, HookState>>,
/// CHECK: the registry PDA, written here (address-checked in the handler).
#[account(mut)]
pub registry: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
/// Accounts of the callback: the token program's five, then the registry's extras.
#[derive(Accounts)]
pub struct Callback<'info> {
/// CHECK: the token program's signer for this hook (checked in `check_call`).
pub hook_signer: UncheckedAccount<'info>,
/// CHECK: the mint.
pub mint: UncheckedAccount<'info>,
/// CHECK: the source holding.
pub source: UncheckedAccount<'info>,
/// CHECK: the destination holding.
pub destination: UncheckedAccount<'info>,
/// CHECK: who signed the transfer.
pub authority: UncheckedAccount<'info>,
#[account(mut, seeds = [STATE_SEED, mint.key().as_ref()], bump = state.bump)]
pub state: Box<Account<'info, HookState>>,
/// CHECK: the mint's launch account (address-checked; not readable during the launch).
#[account(address = state.launch @ HookError::WrongAccount)]
pub launch: UncheckedAccount<'info>,
/// CHECK: the launcher's holding (address-checked; may not exist yet, read in the handler).
#[account(mut, address = state.launcher_holding @ HookError::WrongAccount)]
pub launcher_holding: UncheckedAccount<'info>,
}
/// Errors.
#[error_code]
pub enum HookError {
#[msg("an account is not the one this mint's hook expects")]
WrongAccount,
#[msg("the hook signer is not the token program's signer for this hook")]
BadHookSigner,
}
src/math.rs61 lines ›
//! The hook's arithmetic and decisions, on plain integers and bytes: the cut, and whether the
//! launcher is the launch's creator. Nothing here can panic.
/// Basis points in a whole.
pub const BPS: u64 = 10_000;
/// `launcher_check`: not known yet (the launch can't be read yet).
pub const LAUNCHER_UNCHECKED: u8 = 0;
/// `launcher_check`: the launcher is the launch's creator; cuts are on.
pub const LAUNCHER_CONFIRMED: u8 = 1;
/// `launcher_check`: the launch's creator is someone else; cuts stay off for good.
pub const LAUNCHER_NOT_CREATOR: u8 = 2;
/// Where the launchpad's `Launch` account keeps `creator`: after its discriminator (8 bytes),
/// `version` (1), `bump` (1) and `mint` (32), and right before `pool`.
pub const LAUNCH_CREATOR_OFFSET: usize = 42;
/// `bps` basis points of `amount`, rounded down; never more than `amount`.
pub fn cut_of(amount: u64, bps: u64) -> u64 {
let bps = if bps > BPS { BPS } else { bps };
let cut = u128::from(amount) * u128::from(bps) / u128::from(BPS);
let cut = u64::try_from(cut).unwrap_or(0);
if cut > amount {
amount
} else {
cut
}
}
/// The next `launcher_check` and whether cuts are on, given the creator the launch names (`None`
/// while the launch can't be read).
pub fn next_launcher_check(
current: u8,
creator: Option<[u8; 32]>,
launcher: [u8; 32],
) -> (u8, bool) {
match current {
LAUNCHER_CONFIRMED => (LAUNCHER_CONFIRMED, true),
LAUNCHER_NOT_CREATOR => (LAUNCHER_NOT_CREATOR, false),
_ => match creator {
Some(c) if c == launcher => (LAUNCHER_CONFIRMED, true),
Some(_) => (LAUNCHER_NOT_CREATOR, false),
None => (LAUNCHER_UNCHECKED, false),
},
}
}
/// The 32 bytes of `creator` in a `Launch` account's data, when it is long enough.
pub fn creator_bytes(data: &[u8]) -> Option<[u8; 32]> {
let slice = data.get(LAUNCH_CREATOR_OFFSET..LAUNCH_CREATOR_OFFSET + 32)?;
let mut out = [0u8; 32];
out.copy_from_slice(slice);
Some(out)
}
/// Whether a transfer is a buy on the launch's curve: the token is on its curve and the transfer
/// comes out of its pool (on the curve, nothing else ever leaves the pool for a wallet).
pub fn curve_buy(on_curve: bool, pool: [u8; 32], source_owner: [u8; 32]) -> bool {
on_curve && source_owner == pool
}