@@ -5,6 +5,7 @@ use mango::state::*;
5
5
use mango_common:: * ;
6
6
use serum_dex:: state:: OpenOrders ;
7
7
use solana_sdk:: pubkey:: Pubkey ;
8
+ use std:: collections:: HashMap ;
8
9
use std:: mem:: size_of;
9
10
use std:: str:: FromStr ;
10
11
@@ -18,6 +19,7 @@ struct Cli {
18
19
#[ derive( Args , Debug , Clone ) ]
19
20
struct EquityFromSnapshotArgs {
20
21
sqlite : String ,
22
+ late_changes : String ,
21
23
program : Pubkey ,
22
24
group : Pubkey ,
23
25
}
@@ -76,20 +78,6 @@ impl DataSource {
76
78
anyhow:: bail!( "no data found for pubkey {}" , address) ;
77
79
}
78
80
79
- /*
80
- fn program_account_list(&self, program: Pubkey) -> anyhow::Result<Vec<Pubkey>> {
81
- let mut stmt =
82
- self.conn.prepare_cached("SELECT DISTINCT pubkey FROM account WHERE owner = ?")?;
83
- let mut rows = stmt.query(rusqlite::params![program.as_ref()])?;
84
- let mut list = Vec::new();
85
- while let Some(row) = rows.next()? {
86
- let v: Vec<u8> = row.get(0)?;
87
- list.push(Pubkey::new(&v));
88
- }
89
- Ok(list)
90
- }
91
- */
92
-
93
81
fn mango_account_list (
94
82
& self ,
95
83
program : Pubkey ,
@@ -132,18 +120,80 @@ impl DataSource {
132
120
}
133
121
}
134
122
123
+ fn late_deposits_withdrawals ( filename : & str ) -> anyhow:: Result < Vec < ( Pubkey , Pubkey , usize , i64 ) > > {
124
+ // mango token index and decimals
125
+ let tokens: HashMap < & str , ( usize , i32 ) > = HashMap :: from ( [
126
+ ( "MNGO" , ( 0 , 6 ) ) ,
127
+ ( "BTC" , ( 1 , 6 ) ) ,
128
+ ( "ETH" , ( 2 , 6 ) ) ,
129
+ ( "SOL" , ( 3 , 9 ) ) ,
130
+ ( "USDT" , ( 4 , 6 ) ) ,
131
+ ( "SRM" , ( 5 , 6 ) ) ,
132
+ ( "RAY" , ( 6 , 6 ) ) ,
133
+ ( "FTT" , ( 8 , 6 ) ) ,
134
+ ( "MSOL" , ( 10 , 9 ) ) ,
135
+ ( "BNB" , ( 11 , 8 ) ) ,
136
+ ( "AVAX" , ( 12 , 8 ) ) ,
137
+ ( "GMT" , ( 14 , 9 ) ) ,
138
+ ( "USDC" , ( 15 , 6 ) ) ,
139
+ ] ) ;
140
+
141
+ let mut list = Vec :: new ( ) ;
142
+
143
+ use std:: io:: { BufRead , BufReader } ;
144
+ let file = std:: fs:: File :: open ( filename) ?;
145
+ for line in BufReader :: new ( file) . lines ( ) . skip ( 1 ) {
146
+ if let Ok ( line) = line {
147
+ let fields = line. split ( "\t " ) . collect :: < Vec < & str > > ( ) ;
148
+ assert_eq ! ( fields. len( ) , 19 ) ;
149
+ let account = Pubkey :: from_str ( fields[ 5 ] ) . unwrap ( ) ;
150
+ // skip attacker accounts
151
+ if fields[ 5 ] == "4ND8FVPjUGGjx9VuGFuJefDWpg3THb58c277hbVRnjNa"
152
+ || fields[ 5 ] == "CQvKSNnYtPTZfQRQ5jkHq8q2swJyRsdQLcFcj3EmKFfX"
153
+ {
154
+ continue ;
155
+ }
156
+ let owner = Pubkey :: from_str ( fields[ 6 ] ) . unwrap ( ) ;
157
+ let token = fields[ 7 ] ;
158
+ let side = fields[ 8 ] ;
159
+ let quantity = f64:: from_str ( & fields[ 9 ] . replace ( "," , "" ) ) . unwrap ( ) ;
160
+ let token_info = tokens. get ( token) . unwrap ( ) ;
161
+ let change = ( quantity
162
+ * 10f64 . powi ( token_info. 1 )
163
+ * ( if side == "Withdraw" {
164
+ -1f64
165
+ } else {
166
+ assert_eq ! ( side, "Deposit" ) ;
167
+ 1f64
168
+ } ) ) as i64 ;
169
+ list. push ( ( account, owner, token_info. 0 , change) ) ;
170
+ }
171
+ }
172
+ Ok ( list)
173
+ }
174
+
135
175
struct EquityFromSnapshot {
136
176
args : EquityFromSnapshotArgs ,
137
177
data : DataSource ,
138
178
group : MangoGroup ,
139
179
cache : MangoCache ,
140
180
}
141
181
182
+ fn cache_price ( cache : & MangoCache , index : usize ) -> I80F48 {
183
+ if index == QUOTE_INDEX {
184
+ I80F48 :: ONE
185
+ } else {
186
+ cache. price_cache [ index] . price
187
+ }
188
+ }
189
+
142
190
/// value of per-token equity in usd, ordered by mango group token index
143
191
type AccountTokenAmounts = [ i64 ; 16 ] ;
144
192
145
193
impl EquityFromSnapshot {
146
194
fn run ( args : EquityFromSnapshotArgs ) -> anyhow:: Result < ( ) > {
195
+ let late_changes = late_deposits_withdrawals ( & args. late_changes ) ?;
196
+
147
197
let data = DataSource :: new ( args. sqlite . clone ( ) ) ?;
148
198
149
199
let group = data. load_group ( args. group ) ?;
@@ -156,6 +206,8 @@ impl EquityFromSnapshot {
156
206
157
207
let mut account_equities: Vec < ( Pubkey , Pubkey , AccountTokenAmounts ) > =
158
208
Vec :: with_capacity ( account_addresses. len ( ) ) ;
209
+
210
+ // get the snapshot account equities
159
211
for account_address in account_addresses {
160
212
let equity_opt = ctx
161
213
. account_equity ( account_address)
@@ -167,6 +219,59 @@ impl EquityFromSnapshot {
167
219
account_equities. push ( ( account_address, owner, equity) ) ;
168
220
}
169
221
222
+ // apply the late deposits/withdrawals
223
+ for & ( address, owner, token_index, change_native) in late_changes. iter ( ) {
224
+ let change_usd =
225
+ ( I80F48 :: from ( change_native) * cache_price ( & cache, token_index) ) . to_num ( ) ;
226
+ // slow, but just ran a handful times
227
+ let account_opt = account_equities. iter_mut ( ) . find ( |( a, _, _) | a == & address) ;
228
+ if let Some ( ( _, _, equity) ) = account_opt {
229
+ equity[ token_index] += change_usd;
230
+ } else {
231
+ assert ! ( change_usd > 0 ) ;
232
+ let mut equity = AccountTokenAmounts :: default ( ) ;
233
+ equity[ token_index] = change_usd;
234
+ account_equities. push ( ( address, owner, equity) ) ;
235
+ }
236
+ }
237
+
238
+ // Some accounts already cached out on a MNGO PERP position that started to be valuable after the
239
+ // snapshot was taken, no reimbursements
240
+ {
241
+ let odd_accounts = [
242
+ "9A6YVfa66kBEeCLtt6wyqdmjpib7UrybA5mHr3X3kyvf" ,
243
+ "AEYWfmFVu1huajTkT3UUbvhCZx92kZXwgpWgrMtocnzv" ,
244
+ "AZVbGBJ1DU2RnZNhZ72fcpo191DX3k1uyqDiPiaWoF1q" ,
245
+ "C19JAyRLUjkTWmj9VpYu5eVVCbSXcbqqhyF5588ERSSf" ,
246
+ "C9rprN4zcP7Wx87UcbLarTEAGCmGiPZp8gaFXPhY9HYm" ,
247
+ ] ;
248
+ for odd_one in odd_accounts {
249
+ let address = Pubkey :: from_str ( odd_one) . unwrap ( ) ;
250
+ let ( _, _, equity) =
251
+ account_equities. iter_mut ( ) . find ( |( a, _, _) | a == & address) . unwrap ( ) ;
252
+ assert ! ( late_changes. iter( ) . any( |( a, _, _, c) | a == & address && * c < 0 ) ) ;
253
+ let total = equity. iter ( ) . sum :: < i64 > ( ) ;
254
+ assert ! ( total < 0 ) ;
255
+ assert ! ( total > -10_000_000_000 ) ; // none of these was bigger than 10000 USD
256
+ * equity = AccountTokenAmounts :: default ( ) ;
257
+ }
258
+ }
259
+
260
+ // Some accounts withdrew everything after the snapshot was taken. When doing that they
261
+ // probably withdrew a tiny bit more than their snapshot equity due to interest.
262
+ // These accounts have already cached out, no need to reimburse.
263
+ for ( address, _, equity) in account_equities. iter_mut ( ) {
264
+ let total = equity. iter ( ) . sum :: < i64 > ( ) ;
265
+ if total >= 0 {
266
+ continue ;
267
+ }
268
+ assert ! ( late_changes. iter( ) . any( |( a, _, _, c) | a == address && * c < 0 ) ) ;
269
+ assert ! ( equity. iter( ) . sum:: <i64 >( ) < 0 ) ;
270
+ // only up to -10 USD is expected, otherwise investigate manually!
271
+ assert ! ( equity. iter( ) . sum:: <i64 >( ) > -10_000_000 ) ;
272
+ * equity = AccountTokenAmounts :: default ( ) ;
273
+ }
274
+
170
275
let token_names: [ & str ; 16 ] = [
171
276
"MNGO" , "BTC" , "ETH" , "SOL" , "USDT" , "SRM" , "RAY" , "COPE" , "FTT" , "ADA" , "MSOL" , "BNB" ,
172
277
"AVAX" , "LUNA" , "GMT" , "USDC" ,
@@ -180,8 +285,6 @@ impl EquityFromSnapshot {
180
285
true ,
181
286
] ;
182
287
183
- //println!("{:?}", ctx.cache.price_cache.iter().map(|c| c.price).collect::<Vec<_>>());
184
-
185
288
// TODO: tentative numbers from "Repay bad Debt #2" proposal
186
289
let available_native_amounts: [ u64 ; 15 ] = [
187
290
32409565000000 ,
@@ -201,8 +304,11 @@ impl EquityFromSnapshot {
201
304
152843000000000 ,
202
305
] ;
203
306
307
+ // Token prices at time of reimbursement
308
+ // Note that user equity at snapshot time is computed from the prices from the
309
+ // mango cache in the snapshot.
204
310
let reimbursement_prices: [ I80F48 ; 16 ] = [
205
- // TODO: bad prices
311
+ // TODO: bad prices, must be updated when time comes!
206
312
I80F48 :: from_num ( 0.038725 ) ,
207
313
I80F48 :: from_num ( 19036.47 ) ,
208
314
I80F48 :: from_num ( 1280.639999999999997 ) ,
@@ -221,6 +327,7 @@ impl EquityFromSnapshot {
221
327
I80F48 :: ONE ,
222
328
] ;
223
329
330
+ // USD amounts in each token that can be used for reimbursement
224
331
let available_amounts: [ u64 ; 15 ] = available_native_amounts
225
332
. iter ( )
226
333
. zip ( reimbursement_prices. iter ( ) )
@@ -229,6 +336,7 @@ impl EquityFromSnapshot {
229
336
. try_into ( )
230
337
. unwrap ( ) ;
231
338
339
+ // Amounts each user should be reimbursed
232
340
let mut reimburse_amounts = account_equities. clone ( ) ;
233
341
234
342
// all the equity in unavailable tokens is just considered usdc
@@ -258,7 +366,7 @@ impl EquityFromSnapshot {
258
366
// resolve user's liabilities with their assets in a way that aims to bring the
259
367
// needed token amounts <= what's available
260
368
let mut reimburse_amounts = account_equities. clone ( ) ;
261
- for ( _ , _, equity) in reimburse_amounts. iter_mut ( ) {
369
+ for ( adr , _, equity) in reimburse_amounts. iter_mut ( ) {
262
370
for i in 0 ..16 {
263
371
let mut value = equity[ i] ;
264
372
// positive amounts get reimbursed
@@ -291,6 +399,8 @@ impl EquityFromSnapshot {
291
399
}
292
400
293
401
// All tokens fine? Try reducing some random one, starting with USDC
402
+ // (mSOL is last because it looks like we will have a lot of it and want
403
+ // to prefer giving it out to users that had it before)
294
404
for j in [ 15 , 14 , 13 , 12 , 11 , 9 , 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 , 0 , 10 ] {
295
405
if equity[ j] <= 0 {
296
406
continue ;
@@ -313,7 +423,7 @@ impl EquityFromSnapshot {
313
423
// now all reimburse_amounts are >= 0
314
424
315
425
// Do a pass where we scale down user reimbursement token amounts and instead
316
- // reimburse with USDC if there's not enough tokens
426
+ // reimburse with USDC if there's not enough tokens to give out
317
427
for i in 0 ..15 {
318
428
if reimburse_totals[ i] == 0 || reimburse_totals[ i] == available_amounts[ i] {
319
429
continue ;
@@ -346,6 +456,10 @@ impl EquityFromSnapshot {
346
456
}
347
457
348
458
// Do passes where we scale up token reimbursement amounts to try to fully utilize funds
459
+ //
460
+ // The idea here is that we have say 1000 SOL but only need 500 SOL to reimburse.
461
+ // To leave the DAO with fewer SOL at the end we prefer to give people who already
462
+ // had some SOL more of it (and compensate by giving them less of another token).
349
463
for _ in 0 ..100 {
350
464
for i in 0 ..15 {
351
465
if reimburse_totals[ i] == 0 || reimburse_totals[ i] == available_amounts[ i] {
@@ -387,7 +501,7 @@ impl EquityFromSnapshot {
387
501
}
388
502
}
389
503
390
- // double check that user equity is unchanged
504
+ // Double check that total user equity is unchanged
391
505
for ( ( _, ownerl, equity) , ( _, ownerr, reimburse) ) in
392
506
account_equities. iter ( ) . zip ( reimburse_amounts. iter ( ) )
393
507
{
@@ -432,7 +546,7 @@ impl EquityFromSnapshot {
432
546
account_address : Pubkey ,
433
547
) -> anyhow:: Result < Option < ( Pubkey , AccountTokenAmounts ) > > {
434
548
if account_address
435
- != Pubkey :: from_str ( & "3TXDBTHFwyKywjZj1vGdRVkrF5o4YZ1vZnMf3Hb9qALz " ) . unwrap ( )
549
+ != Pubkey :: from_str ( & "rwxRFn2S1DHkbA8wiCDxzMRMncgjUaa4LiJTagvLBr9 " ) . unwrap ( )
436
550
{
437
551
//return Ok(None);
438
552
}
0 commit comments