1// dilatant
2// a shear thickening supply
3// no framework, no idl, no admin, no upgrade path
4
5#![deny(unsafe_code)]
6
7use solana_program::{
8 account_info::{next_account_info, AccountInfo},
9 clock::Clock,
10 entrypoint,
11 entrypoint::ProgramResult,
12 program::invoke_signed,
13 program_error::ProgramError,
14 pubkey::Pubkey,
15 sysvar::Sysvar,
16};
17
18// ---------------------------------------------------------------
19// constants. compiled in. no instruction reaches these.
20// ---------------------------------------------------------------
21
22/// trailing window, in slots. roughly twelve minutes.
23pub const WINDOW_SLOTS: u64 = 1_800;
24
25/// the window is a ring of buckets rather than a list of transfers,
26/// so the record is bounded and the read cost is constant.
27pub const BUCKETS: usize = 60;
28pub const BUCKET_SLOTS: u64 = WINDOW_SLOTS / BUCKETS as u64; // 30
29
30/// resistance floor and ceiling, in basis points.
31pub const BASE_BPS: u64 = 30;
32pub const CEILING_BPS: u64 = 900;
33
34/// the reference cannot be zero or the curve is undefined before
35/// the first transfer.
36pub const REFERENCE_FLOOR_BPS: u64 = 25;
37
38pub const BPS: u128 = 10_000;
39
40// ---------------------------------------------------------------
41// state. one account. written by the hook, read by anyone.
42// ---------------------------------------------------------------
43
44#[repr(C)]
45#[derive(Clone, Copy)]
46pub struct Material {
47 /// pda bump for the delegate authority
48 pub bump: u8,
49 pub _pad: [u8; 7],
50 /// slot of the most recent observation
51 pub last_slot: u64,
52 /// index of the bucket that last_slot falls in
53 pub cursor: u64,
54 /// highest shear ever recorded, in bps of supply. never falls.
55 pub set_bps: u64,
56 /// total burned by resistance, in base units. never falls.
57 pub taken: u64,
58 /// transfers observed. never falls.
59 pub strikes: u64,
60 /// the ring
61 pub buckets: [u64; BUCKETS],
62}
63
64impl Material {
65 pub const LEN: usize = 8 + 8 + 8 + 8 + 8 + 8 + (8 * BUCKETS);
66
67 /// advance the cursor and zero everything it passes.
68 /// this is the whole of relaxation. nothing is scheduled and
69 /// nothing has to be called for it to happen.
70 fn age(&mut self, slot: u64) {
71 let elapsed = slot.saturating_sub(self.last_slot);
72 let steps = (elapsed / BUCKET_SLOTS).min(BUCKETS as u64);
73
74 let mut i = 1;
75 while i <= steps {
76 let idx = ((self.cursor + i) % BUCKETS as u64) as usize;
77 self.buckets[idx] = 0;
78 i += 1;
79 }
80
81 if steps > 0 {
82 self.cursor = (self.cursor + steps) % BUCKETS as u64;
83 }
84 self.last_slot = slot;
85 }
86
87 /// sum of the ring as a fraction of supply, in bps.
88 /// measured against supply because supply falls permanently.
89 fn shear_bps(&self, supply: u64) -> u64 {
90 if supply == 0 {
91 return 0;
92 }
93 let mut sum: u128 = 0;
94 let mut i = 0;
95 while i < BUCKETS {
96 sum = sum.saturating_add(self.buckets[i] as u128);
97 i += 1;
98 }
99 ((sum * BPS) / supply as u128).min(BPS) as u64
100 }
101
102 fn observe(&mut self, amount: u64) {
103 let idx = (self.cursor % BUCKETS as u64) as usize;
104 self.buckets[idx] = self.buckets[idx].saturating_add(amount);
105 self.strikes = self.strikes.saturating_add(1);
106 }
107}
108
109// ---------------------------------------------------------------
110// the curve. integer only. no floating point anywhere.
111// ---------------------------------------------------------------
112
113/// yield = base + (ceiling - base) * min(1, shear/reference)^2
114///
115/// the ratio is squared rather than the amount, so nothing
116/// overflows at any supply this material can reach. rounding is
117/// toward zero at every step, so the resistance charged is never
118/// more than the resistance computed.
119pub fn yield_bps(shear_bps: u64, set_bps: u64) -> u64 {
120 let reference = core::cmp::max(set_bps, REFERENCE_FLOOR_BPS) as u128;
121 let ratio = ((shear_bps as u128 * BPS) / reference).min(BPS);
122 let squared = (ratio * ratio) / BPS;
123 let span = (CEILING_BPS - BASE_BPS) as u128;
124 BASE_BPS + ((span * squared) / BPS) as u64
125}
126
127pub fn resistance(amount: u64, yield_bps: u64) -> u64 {
128 ((amount as u128 * yield_bps as u128) / BPS) as u64
129}
130
131// ---------------------------------------------------------------
132// entrypoint
133// ---------------------------------------------------------------
134
135entrypoint!(process);
136
137/// discriminator for the transfer hook interface execute
138/// instruction. taken from the interface definition, not typed
139/// from memory. verify against the published constant before
140/// deploying.
141const EXECUTE: [u8; 8] = spl_transfer_hook_interface::instruction::ExecuteInstruction::SPL_DISCRIMINATOR
142 .into_bytes();
143
144pub fn process(
145 program_id: &Pubkey,
146 accounts: &[AccountInfo],
147 data: &[u8],
148) -> ProgramResult {
149 if data.len() < 8 {
150 return Err(ProgramError::InvalidInstructionData);
151 }
152 match data[0..8] {
153 d if d == EXECUTE => execute(program_id, accounts, &data[8..]),
154 _ => Err(ProgramError::InvalidInstructionData),
155 }
156}
157
158// ---------------------------------------------------------------
159// the hook. invoked by the balance program on every transfer.
160// there is no path around this call. it is enforced by the
161// program that holds the balances.
162// ---------------------------------------------------------------
163
164fn execute(
165 program_id: &Pubkey,
166 accounts: &[AccountInfo],
167 data: &[u8],
168) -> ProgramResult {
169 let amount = u64::from_le_bytes(
170 data.get(0..8)
171 .ok_or(ProgramError::InvalidInstructionData)?
172 .try_into()
173 .unwrap(),
174 );
175
176 let iter = &mut accounts.iter();
177 let source = next_account_info(iter)?;
178 let mint = next_account_info(iter)?;
179 let _destination = next_account_info(iter)?;
180 let _owner = next_account_info(iter)?;
181 let _extra_metas = next_account_info(iter)?;
182 let state = next_account_info(iter)?;
183 let delegate = next_account_info(iter)?;
184 let balance_program = next_account_info(iter)?;
185
186 if state.owner != program_id {
187 return Err(ProgramError::IllegalOwner);
188 }
189
190 let slot = Clock::get()?.slot;
191 let supply = read_supply(mint)?;
192
193 let mut m = load(state)?;
194
195 // 1. age the record. everything outside the window is gone.
196 m.age(slot);
197
198 // 2. measure the force applied before this transfer.
199 let shear = m.shear_bps(supply);
200
201 // 3. price this transfer against the state that existed
202 // before it. nothing prices itself.
203 let y = yield_bps(shear, m.set_bps);
204 let take = resistance(amount, y);
205
206 // 4. destroy it. there is no pool. nothing accumulates
207 // anywhere and there is nothing to claim.
208 if take > 0 {
209 burn(
210 balance_program,
211 source,
212 mint,
213 delegate,
214 take,
215 m.bump,
216 program_id,
217 )?;
218 m.taken = m.taken.saturating_add(take);
219 }
220
221 // 5. record it, and mark the set if this is the hardest
222 // this material has ever been pushed. the mark is
223 // permanent and moves in one direction only.
224 m.observe(amount);
225 let after = m.shear_bps(supply);
226 if after > m.set_bps {
227 m.set_bps = after;
228 }
229
230 store(state, &m)
231}
232
233// ---------------------------------------------------------------
234// burn, via the permanent delegate. the delegate is a pda with
235// no private key. no person can sign for it. this is the only
236// place in the program it is used, and it can only burn.
237// ---------------------------------------------------------------
238
239fn burn(
240 balance_program: &AccountInfo,
241 source: &AccountInfo,
242 mint: &AccountInfo,
243 delegate: &AccountInfo,
244 amount: u64,
245 bump: u8,
246 program_id: &Pubkey,
247) -> ProgramResult {
248 let ix = spl_token_2022::instruction::burn(
249 balance_program.key,
250 source.key,
251 mint.key,
252 delegate.key,
253 &[],
254 amount,
255 )?;
256
257 let seeds: &[&[u8]] = &[b"delegate", &[bump]];
258 let _ = program_id;
259
260 invoke_signed(
261 &ix,
262 &[source.clone(), mint.clone(), delegate.clone()],
263 &[seeds],
264 )
265}