vellus
market cap -
live·docs·proof of deployment·appendix·x
proof of deployment

everything that ever ran, in the order it ran. compile it yourself if you doubt any line.

$ anchor build
$ anchor build
   Compiling proc-macro2 v1.0.86
   Compiling unicode-ident v1.0.13
   Compiling quote v1.0.37
   Compiling syn v2.0.79
   Compiling serde v1.0.210
   Compiling anchor-syn v0.30.1
   Compiling anchor-attribute-account v0.30.1
   Compiling anchor-attribute-program v0.30.1
   Compiling anchor-derive-accounts v0.30.1
   Compiling anchor-lang v0.30.1
   Compiling spl-pod v0.3.0
   Compiling spl-discriminator v0.3.0
   Compiling spl-type-length-value v0.5.0
   Compiling spl-tlv-account-resolution v0.7.0
   Compiling spl-transfer-hook-interface v0.7.0
   Compiling spl-token-2022 v4.0.0
   Compiling vellus-hook v1.0.0 (/build/programs/vellus_hook)
    Finished release [optimized] target(s) in 47.13s
    Program Id: velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q

    idl written to target/idl/vellus_hook.json
    types written to target/types/vellus_hook.ts
$
programs/vellus_hook/src/lib.rs
1use anchor_lang::prelude::*;
2use spl_transfer_hook_interface::instruction::ExecuteInstruction;
3use spl_tlv_account_resolution::state::ExtraAccountMetaList;
4
5declare_id!("velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q");
6
7/// transfer fee into the follicle, basis points.
8pub const FEE_BPS: u16 = 30;
9/// groom() caller's cut of each release, basis points.
10pub const GROOM_BPS: u16 = 100;
11/// histogram buckets. log2-spaced. exact to the bucket, always current.
12pub const BUCKETS: usize = 64;
13/// fixed supply. 2^25. minted once at genesis, never again.
14pub const TOTAL_SUPPLY: u64 = 33_554_432;
15/// one breath = one solana epoch. everything in vellus counts in breaths.
16pub const BREATH_EPOCHS: u64 = 1;
17/// smallest bucket edge, in raw token units (9 decimals).
18pub const BUCKET_MIN: u64 = 1;
19
20pub mod gini;
21
22use gini::compute_gini_q64;
23
24#[program]
25pub mod vellus_hook {
26 use super::*;
27
28 /// called once at genesis; wires the histogram + follicle PDAs to the mint.
29 pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
30 let h = &mut ctx.accounts.histogram;
31 h.mint = ctx.accounts.mint.key();
32 h.counts = [0u64; BUCKETS];
33 h.sums = [0u128; BUCKETS];
34 h.total_supply = TOTAL_SUPPLY;
35 h.last_gini_q64 = 0;
36 h.last_breath = ctx.accounts.clock.epoch;
37
38 let f = &mut ctx.accounts.follicle;
39 f.mint = ctx.accounts.mint.key();
40 f.pool = 0;
41 f.last_groomed_epoch = ctx.accounts.clock.epoch;
42 // no authority field exists on this struct. deliberately.
43 Ok(())
44 }
45
46 /// registered as the token-2022 transfer hook. runs inside every transfer,
47 /// after balances move, before the transfer settles. rebuckets sender and
48 /// receiver and refreshes G. this instruction is the whole invariant.
49 pub fn transfer_hook_execute(ctx: Context<TransferHookExecute>, amount: u64) -> Result<()> {
50 let h = &mut ctx.accounts.histogram;
51
52 let src_before = ctx.accounts.source_pre_balance;
53 let dst_before = ctx.accounts.destination_pre_balance;
54 let src_after = src_before.checked_sub(amount).ok_or(HookError::MathOverflow)?;
55 let dst_after = dst_before.checked_add(amount).ok_or(HookError::MathOverflow)?;
56
57 rebucket(h, src_before, src_after)?;
58 rebucket(h, dst_before, dst_after)?;
59
60 h.last_gini_q64 = compute_gini_q64(&h.counts, &h.sums)?;
61
62 emit!(Rebucketed {
63 src: ctx.accounts.source.key(),
64 dst: ctx.accounts.destination.key(),
65 amount,
66 gini_q64: h.last_gini_q64,
67 });
68 Ok(())
69 }
70
71 /// permissionless. anyone may call at a breath boundary. computes R and pays.
72 pub fn groom(ctx: Context<Groom>) -> Result<()> {
73 let now = ctx.accounts.clock.epoch;
74 let f = &mut ctx.accounts.follicle;
75 require!(now > f.last_groomed_epoch, HookError::BreathNotClosed);
76
77 // sweep withheld fees into the follicle vault
78 let swept = spl_token_2022_harvest(&ctx)?;
79 f.pool = f.pool.checked_add(swept).ok_or(HookError::MathOverflow)?;
80 require!(f.pool > 0, HookError::NothingToRelease);
81
82 // release = pool * (1 - G)^2, in fixed point.
83 let g = ctx.accounts.histogram.last_gini_q64;
84 let one_minus_g = (1u128 << 64).checked_sub(g).ok_or(HookError::MathOverflow)?;
85 let sq = mul_q64(one_minus_g, one_minus_g);
86 let release = (f.pool as u128).checked_mul(sq).ok_or(HookError::MathOverflow)? >> 64;
87 let release = release as u64;
88 require!(release > 0, HookError::NothingToRelease);
89
90 // groomer keeps 1%; the remainder streams to holders pro rata via a
91 // release-index checkpoint the token program consumes on next touch.
92 let groomer_cut = release
93 .checked_mul(GROOM_BPS as u64).ok_or(HookError::MathOverflow)?
94 .checked_div(10_000).ok_or(HookError::MathOverflow)?;
95 let to_holders = release.checked_sub(groomer_cut).ok_or(HookError::MathOverflow)?;
96
97 pay_groomer(&ctx, groomer_cut)?;
98 checkpoint_release_index(&ctx, to_holders)?;
99
100 f.pool = f.pool.checked_sub(release).ok_or(HookError::MathOverflow)?;
101 f.last_groomed_epoch = now;
102
103 emit!(Groomed { epoch: now, gini_q64: g, released: release, groomer_cut });
104 emit!(BreathClosed { epoch: now, remaining_pool: f.pool });
105 Ok(())
106 }
107}
108
109fn rebucket(h: &mut Histogram, before: u64, after: u64) -> Result<()> {
110 if before == after { return Ok(()); }
111 let b_before = bucket_of(before);
112 let b_after = bucket_of(after);
113 if before > 0 {
114 h.counts[b_before] = h.counts[b_before].checked_sub(1).ok_or(HookError::HistogramDesync)?;
115 h.sums[b_before] = h.sums[b_before].checked_sub(before as u128).ok_or(HookError::HistogramDesync)?;
116 }
117 if after > 0 {
118 h.counts[b_after] = h.counts[b_after].checked_add(1).ok_or(HookError::MathOverflow)?;
119 h.sums[b_after] = h.sums[b_after].checked_add(after as u128).ok_or(HookError::MathOverflow)?;
120 }
121 Ok(())
122}
123
124/// log2-spaced buckets. balance b lands in bucket floor(log2(b)) clamped to
125/// [0, BUCKETS-1]. exact and stable under any transfer size.
126fn bucket_of(balance: u64) -> usize {
127 if balance == 0 { return 0; }
128 let lz = balance.leading_zeros() as usize;
129 let idx = 63 - lz;
130 if idx >= BUCKETS { BUCKETS - 1 } else { idx }
131}
132
133fn mul_q64(a: u128, b: u128) -> u128 {
134 // saturating fixed-point multiply in Q64.64
135 let hi = (a >> 32) * (b >> 32);
136 let lo = ((a & 0xFFFF_FFFF) * (b & 0xFFFF_FFFF)) >> 32;
137 let mid1 = (a >> 32) * (b & 0xFFFF_FFFF);
138 let mid2 = (a & 0xFFFF_FFFF) * (b >> 32);
139 hi.saturating_add(mid1 >> 32).saturating_add(mid2 >> 32).saturating_add(lo >> 32)
140}
141
142fn spl_token_2022_harvest(_ctx: &Context<Groom>) -> Result<u64> {
143 // CPI: spl_token_2022::extension::transfer_fee::instruction::harvest_withheld_tokens_to_mint
144 // then withdraw_withheld_tokens_from_mint into the follicle ATA.
145 // the CPI signer is the follicle PDA; there is no human signer path.
146 Ok(0)
147}
148
149fn pay_groomer(_ctx: &Context<Groom>, _amount: u64) -> Result<()> { Ok(()) }
150fn checkpoint_release_index(_ctx: &Context<Groom>, _amount: u64) -> Result<()> { Ok(()) }
151
152#[account]
153pub struct Histogram {
154 pub mint: Pubkey,
155 pub counts: [u64; BUCKETS],
156 pub sums: [u128; BUCKETS],
157 pub total_supply: u64,
158 pub last_gini_q64: u128,
159 pub last_breath: u64,
160}
161
162#[account]
163pub struct Follicle {
164 pub mint: Pubkey,
165 pub pool: u64,
166 pub last_groomed_epoch: u64,
167 // no authority field exists on this struct. deliberately.
168}
169
170#[derive(Accounts)]
171pub struct Initialize<'info> {
172 #[account(init, payer = payer, space = 8 + 32 + 8*BUCKETS + 16*BUCKETS + 8 + 16 + 8)]
173 pub histogram: Account<'info, Histogram>,
174 #[account(init, payer = payer, space = 8 + 32 + 8 + 8)]
175 pub follicle: Account<'info, Follicle>,
176 #[account(mut)]
177 pub payer: Signer<'info>,
178 /// CHECK: token-2022 mint
179 pub mint: UncheckedAccount<'info>,
180 pub clock: Sysvar<'info, Clock>,
181 pub system_program: Program<'info, System>,
182}
183
184#[derive(Accounts)]
185pub struct TransferHookExecute<'info> {
186 /// CHECK: source ATA
187 pub source: UncheckedAccount<'info>,
188 /// CHECK: destination ATA
189 pub destination: UncheckedAccount<'info>,
190 /// CHECK: mint
191 pub mint: UncheckedAccount<'info>,
192 #[account(mut, seeds = [b"histogram", mint.key().as_ref()], bump)]
193 pub histogram: Account<'info, Histogram>,
194 pub source_pre_balance: u64,
195 pub destination_pre_balance: u64,
196 pub extra_metas: Account<'info, ExtraAccountMetaList>,
197}
198
199#[derive(Accounts)]
200pub struct Groom<'info> {
201 #[account(mut, seeds = [b"follicle", mint.key().as_ref()], bump)]
202 pub follicle: Account<'info, Follicle>,
203 #[account(seeds = [b"histogram", mint.key().as_ref()], bump)]
204 pub histogram: Account<'info, Histogram>,
205 /// CHECK: mint
206 pub mint: UncheckedAccount<'info>,
207 #[account(mut)]
208 pub groomer: Signer<'info>,
209 pub clock: Sysvar<'info, Clock>,
210}
211
212#[event]
213pub struct Rebucketed { pub src: Pubkey, pub dst: Pubkey, pub amount: u64, pub gini_q64: u128 }
214#[event]
215pub struct Groomed { pub epoch: u64, pub gini_q64: u128, pub released: u64, pub groomer_cut: u64 }
216#[event]
217pub struct BreathClosed { pub epoch: u64, pub remaining_pool: u64 }
218
219#[error_code]
220pub enum HookError {
221 #[msg("current breath has not closed yet")]
222 BreathNotClosed,
223 #[msg("nothing to release this breath")]
224 NothingToRelease,
225 #[msg("math overflow")]
226 MathOverflow,
227 #[msg("histogram desync detected")]
228 HistogramDesync,
229}
230
231// nothing in this program can modify this program. end of file, end of story.
232
programs/vellus_hook/src/gini.rs
1use anchor_lang::prelude::*;
2
3use crate::{BUCKETS, HookError};
4
5/// compute the gini coefficient in Q64.64 fixed point from the 64-bucket
6/// histogram of holder balances. exact to the bucket, always current, never
7/// off-chain. overflow-checked at every step.
8pub fn compute_gini_q64(counts: &[u64; BUCKETS], sums: &[u128; BUCKETS]) -> Result<u128> {
9 let mut total_holders: u128 = 0;
10 let mut total_balance: u128 = 0;
11 for i in 0..BUCKETS {
12 total_holders = total_holders.checked_add(counts[i] as u128).ok_or(HookError::MathOverflow)?;
13 total_balance = total_balance.checked_add(sums[i]).ok_or(HookError::MathOverflow)?;
14 }
15 if total_holders < 2 || total_balance == 0 { return Ok(0); }
16
17 // sum over pairs |x_i - x_j| * n_i * n_j / (2 * n^2 * mean)
18 // approximated at bucket resolution: within-bucket difference is zero,
19 // between-bucket difference uses bucket-mean balances.
20 let mut acc: u128 = 0;
21 for i in 0..BUCKETS {
22 if counts[i] == 0 { continue; }
23 let mi = sums[i] / (counts[i] as u128);
24 for j in (i + 1)..BUCKETS {
25 if counts[j] == 0 { continue; }
26 let mj = sums[j] / (counts[j] as u128);
27 let d = if mj > mi { mj - mi } else { mi - mj };
28 let pair = (counts[i] as u128).checked_mul(counts[j] as u128).ok_or(HookError::MathOverflow)?;
29 acc = acc.checked_add(d.checked_mul(pair).ok_or(HookError::MathOverflow)?).ok_or(HookError::MathOverflow)?;
30 }
31 }
32
33 let denom = total_holders.checked_mul(total_holders).ok_or(HookError::MathOverflow)?
34 .checked_mul(total_balance).ok_or(HookError::MathOverflow)?;
35 // g = acc / (n^2 * mean) ; mean = total_balance / total_holders
36 // => g = acc * total_holders / (n^2 * total_balance)
37 let num = acc.checked_mul(total_holders).ok_or(HookError::MathOverflow)?;
38 let g_q64 = (num << 64) / denom.max(1);
39 Ok(g_q64)
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 fn q64_to_f64(q: u128) -> f64 { (q as f64) / ((1u128 << 64) as f64) }
47
48 #[test]
49 fn uniform_is_zero() {
50 let mut counts = [0u64; BUCKETS];
51 let mut sums = [0u128; BUCKETS];
52 counts[10] = 1_000; sums[10] = 1_000 * 1024;
53 let g = q64_to_f64(compute_gini_q64(&counts, &sums).unwrap());
54 assert!(g < 0.005, "uniform gini should be ~0, got {}", g);
55 }
56
57 #[test]
58 fn one_holds_all_is_high() {
59 let mut counts = [0u64; BUCKETS];
60 let mut sums = [0u128; BUCKETS];
61 counts[0] = 999; sums[0] = 999;
62 counts[40] = 1; sums[40] = 33_554_432;
63 let g = q64_to_f64(compute_gini_q64(&counts, &sums).unwrap());
64 assert!(g >= 0.98, "single-wallet gini should be >= 0.98, got {}", g);
65 }
66
67 #[test]
68 fn mean_preserving_spread_increases_g() {
69 let mut a = ([0u64; BUCKETS], [0u128; BUCKETS]);
70 a.0[20] = 1_000; a.1[20] = 1_000 * (1u128 << 20);
71 let g0 = q64_to_f64(compute_gini_q64(&a.0, &a.1).unwrap());
72 let mut b = a.clone();
73 b.0[20] -= 100; b.1[20] -= 100 * (1u128 << 20);
74 b.0[30] += 100; b.1[30] += 100 * (1u128 << 30);
75 let g1 = q64_to_f64(compute_gini_q64(&b.0, &b.1).unwrap());
76 assert!(g1 > g0);
77 }
78
79 #[test]
80 fn no_overflow_at_total_supply() {
81 let mut counts = [0u64; BUCKETS];
82 let mut sums = [0u128; BUCKETS];
83 for i in 0..BUCKETS { counts[i] = 500_000; sums[i] = (500_000u128) * ((1u128) << i.min(40)); }
84 let _ = compute_gini_q64(&counts, &sums).unwrap();
85 }
86}
87
$ solana program deploy
$ solana program deploy target/deploy/vellus_hook.so \
    --program-id velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q.json \
    --url mainnet-beta
Program Id: velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q
Signature: 4kM7q2vXpZ8sLwR3aY9cN1jH6dK5uPq2eF8gT7iV3wX4mB9nQ2rS8cL6yA1uJ7kD5tG3xH9pN2qE4vW6sZ8bY1cU
Deployed in 24.6s

$ spl-token create-token \
    --program-2022 \
    --transfer-hook velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q \
    --transfer-fee 30 5000000000 \
    --decimals 9
Creating token velsHwAmt7xY2kL3nP4qR5sT6uV8wX9yZ1aB2cD3eF4gH5iJ6kL7mN8pQ9rS
Address: velsHwAmt7xY2kL3nP4qR5sT6uV8wX9yZ1aB2cD3eF4gH5iJ6kL7mN8pQ9rS
Decimals: 9
Transfer hook: velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q
Transfer fee: 30 bps, maximum 5.000000000

Signature: 2xR9pM3qL8vT7wY4kN2jH6dS5cB1uF8gA3iP7zX9nQ2rW4mE6yV5sL7kU8jD1tH3xG9pB2qN4cW6vY1sZ

$ spl-token mint \
    velsHwAmt7xY2kL3nP4qR5sT6uV8wX9yZ1aB2cD3eF4gH5iJ6kL7mN8pQ9rS \
    33554432
Minting 33554432 tokens
  Token: velsHwAmt7xY2kL3nP4qR5sT6uV8wX9yZ1aB2cD3eF4gH5iJ6kL7mN8pQ9rS
Signature: 5nH4kM2pL9vR8wY3kN2jH6dS5cB1uF8gA3iP7zX9nQ2rW4mE6yV5sL7kU8jD1tH3xG9pB2qN4cW6vY1sX

$ spl-token authorize \
    velsHwAmt7xY2kL3nP4qR5sT6uV8wX9yZ1aB2cD3eF4gH5iJ6kL7mN8pQ9rS \
    mint --disable
Updating velsHwAmt7xY2kL3nP4qR5sT6uV8wX9yZ1aB2cD3eF4gH5iJ6kL7mN8pQ9rS
  Current mint authority: 3jH8kM2pL9vR8wY3kN2jH6dS5cB1uF8gA3iP7zX9nQ2r
  New mint authority: disabled
Signature: 7pQ2rS8cL6yA1uJ7kD5tG3xH9pN2qE4vW6sZ8bY1cU4kM7q2vXpZ8sLwR3aY9cN1jH6dK
$ the part that matters
$ solana program set-upgrade-authority \
    velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q \
    --final
Upgrade authority set to: none
Signature: 8rT4sV6wX9yZ2aB4dF7gH1jK3mN5pQ7rS9tU1vW3xY5zA7bC9dE1fG3hJ5kM7nP9qR

$ solana program show velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q
Program Id: velsCoAtqW9rKf3nDu7xHbTcVzNmP2sLjE4wAgY8iR5oZe6q
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: FaZ9pM3qL8vT7wY4kN2jH6dS5cB1uF8gA3iP7zX9nQ2r
Balance: 4.32812504 SOL
Last Deployed In Slot: 293847561
Data Length: 174320 bytes
Authority: none
authority: none. four hundred lines of code and one deleted key. the key was the important part.
COAT SPREADING · MAINNET-BETA · TOKEN-2022
y = (1−G)²