-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlst_maths.txt
More file actions
386 lines (342 loc) · 11.8 KB
/
Copy pathlst_maths.txt
File metadata and controls
386 lines (342 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
changes to make:
1. in withdrawls, after claiming also, withdrawls are shown, but dont show
if withdraw status is 1 means claimed
2. claim button dont enable if vault has ot suffiecint balance so that
user claim before admin withdraw from split to withdraw vault
3
where to put the ix to withdrawing from split accounts to withdrawl vault- cant put in unstake ix
as if main stake account is in activated state, then split account will be in deactivating state after
unstake ix, and not possible to withdraw from deactivating split stake acc, to withdraw from it, it must be
in fully deactivated state, and then call a withdraw ix from it.
so maybe a new ix to for admin to call: withdraw from split state acc to withdrawl vault pda
🔟 Recommended build order (DO NOT SKIP)
1️⃣ Reuse your Stake Manager as-is
2️⃣ Write LST Manager State
3️⃣ Implement InitializeLST
4️⃣ Implement DepositSOL (mint LST)
5️⃣ Implement RequestWithdraw
6️⃣ Implement ProcessWithdraw
7️⃣ Add rebalancing + split/merge
## **How industry LST protocols do it:**
**Marinade, Lido, Jito, and other LSTs:**
1. **Vault/Reserve account holds all SOL** - controlled by program PDAs
2. **Protocol pays for all operations** - stake account creation, splitting, merging
3. **Users only pay transaction fees** - typically <0.001 SOL
4. **Permissioned staking operators** - not every user can call stake; usually:
- Admin/crank calls it periodically
- Or keeper bots trigger staking when thresholds are met
5. **Batching** - stake multiple users' deposits together in one operation
lst manager, lst manager vault
Sure 🙂 let’s do this purely with numbers, very simple, no jargon.
Step 1: Pool starts empty
Assume an LST protocol (on Solana).
Total SOL in pool = 0 SOL
Total LST tokens = 0 LST
Step 2: User deposits SOL
User A deposits 100 SOL
Protocol mints LST at a 1:1 rate at the start.
Total SOL in pool = 100 SOL
Total LST supply = 100 LST
So:
1 LST = 1 SOL
User A owns:
100 LST
Step 3: Staking rewards come in
Assume after some time:
Validators earn 10 SOL as rewards
Now:
Total SOL in pool = 110 SOL
Total LST supply = still 100 LST (no new LST minted)
New exchange rate:
1 LST = 110 / 100 = 1.10 SOL
User A still has:
100 LST
But value is now:
100 LST × 1.10 = 110 SOL
✅ Value increased without minting more LST
Step 4: New user deposits later
User B deposits 55 SOL
Minting must be fair, so LST is minted at current rate:
Minted LST = 55 / 1.10 = 50 LST
Now totals:
Total SOL = 165 SOL
Total LST = 150 LST
Exchange rate:
165 / 150 = 1.10 SOL per LST
No one is diluted.
Step 5: More rewards come in
Later, staking earns 15 SOL more.
Now:
Total SOL = 180 SOL
Total LST = 150 LST
New rate:
1 LST = 180 / 150 = 1.20 SOL
User values:
User A: 100 × 1.20 = 120 SOL
User B: 50 × 1.20 = 60 SOL
Step 6: What if something bad happens (slashing)?
Assume a 5 SOL slash happens.
Now:
Total SOL = 175 SOL
Total LST = 150 LST
New rate:
1 LST = 175 / 150 = 1.1667 SOL
❌ Exchange rate decreased:
from 1.20 → 1.1667 SOL
User A value:
100 × 1.1667 ≈ 116.67 SOL
Step 7: Market price vs true value (important)
Even if:
True value = 1 LST = 1.1667 SOL
On a DEX it could trade at:
1.14 SOL (discount)
1.18 SOL (premium)
That’s trading behavior, not staking math.
Key takeaway (one line)
LST value = Total SOL in pool / Total LST supply
Rewards → increase SOL → value goes up
Slashing / loss → decrease SOL → value goes down
LST count usually stays fixed
what happens when user unstaked, the LST tokens are burned or like somethignelse
Great follow-up 👍
Yes — LST tokens are effectively burned when a user unstakes, but how that happens depends on which unstaking path is used. Let’s explain it with clear math and flow.
Two ways to unstake an LST on Solana
1️⃣ Native unstake (burn → wait → receive SOL)
This is the protocol-level unstake.
2️⃣ Instant unstake (sell on DEX)
This is a market trade, not real unstaking.
I’ll explain both.
1️⃣ Native unstake (LST is burned)
Starting state
From our earlier example:
Total SOL in pool = 180 SOL
Total LST supply = 150 LST
Exchange rate:
1 LST = 180 / 150 = 1.20 SOL
User A holds:
100 LST
User A unstakes 20 LST
Protocol calculates how much SOL this represents:
20 LST × 1.20 = 24 SOL
What happens internally:
✅ 20 LST are burned
❌ They no longer exist
But SOL is still staked, so:
User must wait for the Solana unstake cooldown (≈ 2–3 days)
Pool state after burn (but before withdrawal)
Total LST = 130 LST
SOL is reserved for User A (24 SOL)
Remaining SOL backing active LST = 156 SOL
Exchange rate stays the same:
156 / 130 = 1.20 SOL
👉 Burning keeps everyone else undiluted
After cooldown completes
User A receives:
24 SOL
System is now consistent.
2️⃣ Instant unstake (selling LST on a DEX)
Here, nothing is burned.
User:
Sells 20 LST on a DEX
Gets SOL from another trader
What changes?
Total SOL in protocol = unchanged
Total LST supply = unchanged
Ownership of LST shifts to someone else
📉 Price may be:
Slightly lower than intrinsic value (liquidity fee)
Or higher (demand)
But protocol math is unaffected.
Side-by-side summary
Action LST burned? SOL leaves pool? Wait time
Native unstake ✅ Yes ✅ Yes ~2–3 days
DEX sell ❌ No ❌ No Instant
Why burning is necessary
If LST were not burned:
SOL would leave the pool
Same LST would represent less SOL
Remaining holders would be diluted ❌
Burning keeps this formula correct:
LST value = Total SOL / Total LST
Final mental model (very important)
Think of LST as shares of a vault:
Deposit SOL → mint shares
Rewards → vault grows
Unstake → burn shares → withdraw vault assets
pack, unpack, serialize or deserialize code using arrayref
use {
crate::instruction::MAX_SIGNERS,
arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs},
num_enum::TryFromPrimitive,
solana_program_error::ProgramError,
solana_program_option::COption,
solana_program_pack::{IsInitialized, Pack, Sealed},
solana_pubkey::{Pubkey, PUBKEY_BYTES},
};
/// Mint data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Mint {
/// Optional authority used to mint new tokens. The mint authority may only
/// be provided during mint creation. If no mint authority is present
/// then the mint has a fixed supply and no further tokens may be
/// minted.
pub mint_authority: COption<Pubkey>,
/// Total supply of tokens.
pub supply: u64,
/// Number of base 10 digits to the right of the decimal place.
pub decimals: u8,
/// Is `true` if this structure has been initialized
pub is_initialized: bool,
/// Optional authority to freeze token accounts.
pub freeze_authority: COption<Pubkey>,
}
impl Sealed for Mint {}
impl IsInitialized for Mint {
fn is_initialized(&self) -> bool {
self.is_initialized
}
}
impl Pack for Mint {
const LEN: usize = 82;
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let src = array_ref![src, 0, 82];
let (mint_authority, supply, decimals, is_initialized, freeze_authority) =
array_refs![src, 36, 8, 1, 1, 36];
let mint_authority = unpack_coption_key(mint_authority)?;
let supply = u64::from_le_bytes(*supply);
let decimals = decimals[0];
let is_initialized = match is_initialized {
[0] => false,
[1] => true,
_ => return Err(ProgramError::InvalidAccountData),
};
let freeze_authority = unpack_coption_key(freeze_authority)?;
Ok(Mint {
mint_authority,
supply,
decimals,
is_initialized,
freeze_authority,
})
}
fn pack_into_slice(&self, dst: &mut [u8]) {
let dst = array_mut_ref![dst, 0, 82];
let (
mint_authority_dst,
supply_dst,
decimals_dst,
is_initialized_dst,
freeze_authority_dst,
) = mut_array_refs![dst, 36, 8, 1, 1, 36];
let &Mint {
ref mint_authority,
supply,
decimals,
is_initialized,
ref freeze_authority,
} = self;
pack_coption_key(mint_authority, mint_authority_dst);
*supply_dst = supply.to_le_bytes();
decimals_dst[0] = decimals;
is_initialized_dst[0] = is_initialized as u8;
pack_coption_key(freeze_authority, freeze_authority_dst);
}
}
/// Account data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Account {
/// The mint associated with this account
pub mint: Pubkey,
/// The owner of this account.
pub owner: Pubkey,
/// The amount of tokens this account holds.
pub amount: u64,
/// If `delegate` is `Some` then `delegated_amount` represents
/// the amount authorized by the delegate
pub delegate: COption<Pubkey>,
/// The account's state
pub state: AccountState,
/// If `is_native.is_some`, this is a native token, and the value logs the
/// rent-exempt reserve. An Account is required to be rent-exempt, so
/// the value is used by the Processor to ensure that wrapped SOL
/// accounts do not drop below this threshold.
pub is_native: COption<u64>,
/// The amount delegated
pub delegated_amount: u64,
/// Optional authority to close the account.
pub close_authority: COption<Pubkey>,
}
impl Account {
/// Checks if account is frozen
pub fn is_frozen(&self) -> bool {
self.state == AccountState::Frozen
}
/// Checks if account is native
pub fn is_native(&self) -> bool {
self.is_native.is_some()
}
/// Checks if a token Account's owner is the `system_program` or the
/// incinerator
pub fn is_owned_by_system_program_or_incinerator(&self) -> bool {
solana_sdk_ids::system_program::check_id(&self.owner)
|| solana_sdk_ids::incinerator::check_id(&self.owner)
}
}
impl Sealed for Account {}
impl IsInitialized for Account {
fn is_initialized(&self) -> bool {
self.state != AccountState::Uninitialized
}
}
impl Pack for Account {
const LEN: usize = 165;
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let src = array_ref![src, 0, 165];
let (mint, owner, amount, delegate, state, is_native, delegated_amount, close_authority) =
array_refs![src, 32, 32, 8, 36, 1, 12, 8, 36];
Ok(Account {
mint: Pubkey::new_from_array(*mint),
owner: Pubkey::new_from_array(*owner),
amount: u64::from_le_bytes(*amount),
delegate: unpack_coption_key(delegate)?,
state: AccountState::try_from_primitive(state[0])
.or(Err(ProgramError::InvalidAccountData))?,
is_native: unpack_coption_u64(is_native)?,
delegated_amount: u64::from_le_bytes(*delegated_amount),
close_authority: unpack_coption_key(close_authority)?,
})
}
fn pack_into_slice(&self, dst: &mut [u8]) {
let dst = array_mut_ref![dst, 0, 165];
let (
mint_dst,
owner_dst,
amount_dst,
delegate_dst,
state_dst,
is_native_dst,
delegated_amount_dst,
close_authority_dst,
) = mut_array_refs![dst, 32, 32, 8, 36, 1, 12, 8, 36];
let &Account {
ref mint,
ref owner,
amount,
ref delegate,
state,
ref is_native,
delegated_amount,
ref close_authority,
} = self;
mint_dst.copy_from_slice(mint.as_ref());
owner_dst.copy_from_slice(owner.as_ref());
*amount_dst = amount.to_le_bytes();
pack_coption_key(delegate, delegate_dst);
state_dst[0] = state as u8;
pack_coption_u64(is_native, is_native_dst);
*delegated_amount_dst = delegated_amount.to_le_bytes();
pack_coption_key(close_authority, close_authority_dst);
}
}