Skip to content

Commit 77dcae9

Browse files
committed
Cap predict NAV index sizes
1 parent 827692f commit 77dcae9

5 files changed

Lines changed: 167 additions & 16 deletions

File tree

packages/predict/sources/constants.move

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ public(package) macro fun max_plp_price(): u64 { 100_000_000_000 }
6969
/// full-pool flush.
7070
public(package) macro fun max_live_expiry_markets(): u64 { 24 }
7171

72+
/// Maximum finite payout-tree boundary nodes one expiry market may carry into NAV.
73+
public(package) macro fun max_payout_tree_nodes(): u64 { 1_000 }
74+
75+
/// Maximum active leveraged orders one expiry market may carry into NAV.
76+
public(package) macro fun max_active_leveraged_orders(): u64 { 5_000 }
77+
7278
// === Time Constants ===
7379

7480
/// Milliseconds in one minute.

packages/predict/sources/strike_exposure/index/liquidation_book.move

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const EActiveOrderAlreadyExists: u64 = 0;
2222
const EActiveOrderNotFound: u64 = 1;
2323
const ELiquidatedOrderAlreadyExists: u64 = 2;
2424
const ELiquidatedOrderNotFound: u64 = 3;
25+
const EMaxActiveLeveragedOrders: u64 = 4;
2526

2627
const PAGE_CAPACITY: u64 = 64;
2728

@@ -135,6 +136,10 @@ public(package) fun insert_order(book: &mut LiquidationBook, order: &Order) {
135136

136137
let order_id = order.id();
137138
assert!(!book.liquidated_orders.contains(order_id), ELiquidatedOrderAlreadyExists);
139+
assert!(
140+
book.active_order_count < constants::max_active_leveraged_orders!(),
141+
EMaxActiveLeveragedOrders,
142+
);
138143
book.insert_active_order_id(order_id);
139144
}
140145

packages/predict/sources/strike_exposure/index/strike_payout_tree.move

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ use fixed_math::math;
2020
use sui::{bcs, hash::blake2b256, table::{Self, Table}};
2121

2222
const EInsufficientPayoutTerms: u64 = 0;
23+
const EMaxPayoutTreeNodes: u64 = 1;
2324

2425
/// Sparse payout-liability tree keyed by finite strike tick.
2526
public struct StrikePayoutTree has store {
2627
root: Option<u64>,
2728
nodes: Table<u64, PayoutNode>,
29+
node_count: u64,
2830
base: PayoutTerms,
2931
}
3032

@@ -138,6 +140,7 @@ public(package) fun new(ctx: &mut TxContext): StrikePayoutTree {
138140
StrikePayoutTree {
139141
root: option::none(),
140142
nodes: table::new(ctx),
143+
node_count: 0,
141144
base: payout_terms(0, 0),
142145
}
143146
}
@@ -150,12 +153,27 @@ public(package) fun insert_range(
150153
quantity: u64,
151154
floor_shares: u64,
152155
) {
153-
tree.apply_range(
154-
lower_tick,
155-
higher_tick,
156-
payout_terms(quantity, floor_shares),
157-
true,
156+
let terms = payout_terms(quantity, floor_shares);
157+
if (terms.quantity == 0 && terms.floor_shares == 0) return;
158+
159+
let mut new_nodes = 0;
160+
if (lower_tick != 0 && !tree.nodes.contains(lower_tick)) {
161+
new_nodes = new_nodes + 1;
162+
};
163+
if (
164+
higher_tick != constants::pos_inf_tick!()
165+
&& higher_tick != lower_tick
166+
&& !tree.nodes.contains(higher_tick)
167+
) {
168+
new_nodes = new_nodes + 1;
169+
};
170+
171+
assert!(
172+
tree.node_count + new_nodes <= constants::max_payout_tree_nodes!(),
173+
EMaxPayoutTreeNodes,
158174
);
175+
176+
tree.apply_range(lower_tick, higher_tick, terms, true);
159177
}
160178

161179
/// Remove interval payout terms for the order tick range `(lower_tick, higher_tick]`.
@@ -181,7 +199,12 @@ public(package) fun debug_contains_node(tree: &StrikePayoutTree, tick: u64): boo
181199

182200
#[test_only]
183201
public(package) fun debug_node_count(tree: &StrikePayoutTree): u64 {
184-
debug_node_count_subtree(&tree.nodes, tree.root)
202+
tree.node_count
203+
}
204+
205+
#[test_only]
206+
public(package) fun debug_set_node_count(tree: &mut StrikePayoutTree, node_count: u64) {
207+
tree.node_count = node_count;
185208
}
186209

187210
fun apply_range(
@@ -212,6 +235,7 @@ fun apply_boundary_delta(
212235
is_start: bool,
213236
add: bool,
214237
) {
238+
let had_node = tree.nodes.contains(tick);
215239
let new_root = apply_at(
216240
&mut tree.nodes,
217241
tree.root,
@@ -221,6 +245,13 @@ fun apply_boundary_delta(
221245
add,
222246
);
223247
tree.root = new_root;
248+
249+
let has_node = tree.nodes.contains(tick);
250+
if (!had_node && has_node) {
251+
tree.node_count = tree.node_count + 1;
252+
} else if (had_node && !has_node) {
253+
tree.node_count = tree.node_count - 1;
254+
};
224255
}
225256

226257
fun apply_at(
@@ -548,16 +579,6 @@ fun is_empty_node(node: PayoutNode): bool {
548579
is_zero_terms(node.local_start) && is_zero_terms(node.local_end)
549580
}
550581

551-
#[test_only]
552-
fun debug_node_count_subtree(nodes: &Table<u64, PayoutNode>, root: Option<u64>): u64 {
553-
if (root.is_none()) return 0;
554-
let tick = *root.borrow();
555-
let node = nodes[tick];
556-
1
557-
+ debug_node_count_subtree(nodes, node.left)
558-
+ debug_node_count_subtree(nodes, node.right)
559-
}
560-
561582
fun apply_terms_delta(value: &mut PayoutTerms, delta: PayoutTerms, add: bool) {
562583
if (add) {
563584
value.quantity = value.quantity + delta.quantity;

packages/predict/tests/strike_exposure/index/liquidation_book_tests.move

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,47 @@ fun mark_liquidated_removes_active_and_records_tombstone() {
116116
destroy(book);
117117
}
118118

119+
#[test, expected_failure(abort_code = liquidation_book::EMaxActiveLeveragedOrders)]
120+
fun insert_above_active_leveraged_order_cap_aborts() {
121+
let ctx = &mut tx_context::dummy();
122+
let mut book = liquidation_book::new(ctx);
123+
insert_sequential_orders(&mut book, constants::max_active_leveraged_orders!());
124+
125+
book.insert_order(&leveraged_order_with_sequence(constants::max_active_leveraged_orders!()));
126+
abort 999
127+
}
128+
129+
#[test]
130+
fun one_x_orders_do_not_count_toward_active_leveraged_order_cap() {
131+
let ctx = &mut tx_context::dummy();
132+
let mut book = liquidation_book::new(ctx);
133+
insert_sequential_orders(&mut book, constants::max_active_leveraged_orders!());
134+
135+
let one_x = one_x_order_with_sequence(constants::max_active_leveraged_orders!());
136+
book.insert_order(&one_x);
137+
138+
assert!(!book.contains_active_order(&one_x));
139+
let candidates = book.select_liquidation_candidates(constants::max_active_leveraged_orders!());
140+
assert_eq!(candidates.length(), constants::max_active_leveraged_orders!());
141+
destroy(book);
142+
}
143+
144+
#[test]
145+
fun removing_active_leveraged_order_frees_cap_slot() {
146+
let ctx = &mut tx_context::dummy();
147+
let mut book = liquidation_book::new(ctx);
148+
insert_sequential_orders(&mut book, constants::max_active_leveraged_orders!());
149+
150+
let removed = leveraged_order_with_sequence(0);
151+
book.remove_order(&removed);
152+
let replacement = leveraged_order_with_sequence(constants::max_active_leveraged_orders!());
153+
book.insert_order(&replacement);
154+
155+
assert!(!book.contains_active_order(&removed));
156+
assert!(book.contains_active_order(&replacement));
157+
destroy(book);
158+
}
159+
119160
#[test]
120161
fun head_candidate_prefers_higher_floor_for_equal_quantity() {
121162
let ctx = &mut tx_context::dummy();
@@ -233,6 +274,16 @@ fun leveraged_order_with_sequence(sequence: u64): Order {
233274
)
234275
}
235276

277+
fun one_x_order_with_sequence(sequence: u64): Order {
278+
order::new_from_ticks(
279+
LOWER_TICK,
280+
HIGHER_TICK,
281+
0,
282+
constants::position_lot_size!(),
283+
sequence,
284+
)
285+
}
286+
236287
fun insert_sequential_orders(
237288
book: &mut liquidation_book::LiquidationBook,
238289
count: u64,

packages/predict/tests/strike_exposure/index/strike_payout_tree_tests.move

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,64 @@ fun insert_with_both_terms_zero_is_no_op() {
255255
destroy(tree);
256256
}
257257

258+
#[test, expected_failure(abort_code = strike_payout_tree::EMaxPayoutTreeNodes)]
259+
fun insert_new_boundary_above_node_cap_aborts() {
260+
let ctx = &mut tx_context::dummy();
261+
let mut tree = new_tree(ctx);
262+
seed_single_boundary_at_node_cap(&mut tree);
263+
264+
insert_range(
265+
&mut tree,
266+
2,
267+
constants::pos_inf_tick!(),
268+
1,
269+
0,
270+
);
271+
abort 999
272+
}
273+
274+
#[test, expected_failure(abort_code = strike_payout_tree::EMaxPayoutTreeNodes)]
275+
fun insert_finite_range_requiring_two_new_boundaries_above_node_cap_aborts() {
276+
let ctx = &mut tx_context::dummy();
277+
let mut tree = new_tree(ctx);
278+
seed_single_boundary_one_slot_below_node_cap(&mut tree);
279+
280+
insert_range(&mut tree, 2, 3, 1, 0);
281+
abort 999
282+
}
283+
284+
#[test]
285+
fun insert_existing_boundary_at_node_cap_succeeds() {
286+
let ctx = &mut tx_context::dummy();
287+
let mut tree = new_tree(ctx);
288+
seed_single_boundary_at_node_cap(&mut tree);
289+
290+
insert_range(&mut tree, 1, constants::pos_inf_tick!(), 1, 0);
291+
292+
assert_eq!(tree.debug_node_count(), constants::max_payout_tree_nodes!());
293+
destroy(tree);
294+
}
295+
296+
#[test]
297+
fun removing_boundary_below_node_cap_allows_new_boundary() {
298+
let ctx = &mut tx_context::dummy();
299+
let mut tree = new_tree(ctx);
300+
seed_single_boundary_at_node_cap(&mut tree);
301+
302+
remove_range(&mut tree, 1, constants::pos_inf_tick!(), 1, 0);
303+
assert_eq!(tree.debug_node_count(), constants::max_payout_tree_nodes!() - 1);
304+
305+
insert_range(
306+
&mut tree,
307+
2,
308+
constants::pos_inf_tick!(),
309+
1,
310+
0,
311+
);
312+
assert_eq!(tree.debug_node_count(), constants::max_payout_tree_nodes!());
313+
destroy(tree);
314+
}
315+
258316
// === remove_range ===
259317

260318
#[test]
@@ -423,3 +481,13 @@ fun settled_liability_nets_floor_shares() {
423481
assert_reserve_terms(&tree, 30, 30);
424482
destroy(tree);
425483
}
484+
485+
fun seed_single_boundary_at_node_cap(tree: &mut StrikePayoutTree) {
486+
insert_range(tree, 1, constants::pos_inf_tick!(), 1, 0);
487+
tree.debug_set_node_count(constants::max_payout_tree_nodes!());
488+
}
489+
490+
fun seed_single_boundary_one_slot_below_node_cap(tree: &mut StrikePayoutTree) {
491+
insert_range(tree, 1, constants::pos_inf_tick!(), 1, 0);
492+
tree.debug_set_node_count(constants::max_payout_tree_nodes!() - 1);
493+
}

0 commit comments

Comments
 (0)