/ synthesizer / program / src / resources / perpetuals.delta
perpetuals.delta
  1  // Copyright (c) 2025 ALPHA/DELTA Network
  2  // This file is part of the DeltaVM library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  /**********************************************************************************************************************/
 17  // DELTA Perpetuals Program
 18  //
 19  // Provides leveraged perpetual futures trading:
 20  // - Leverage limits:
 21  //   - Fiat pairs (EUR, GBP, JPY): 50x maximum
 22  //   - Major crypto (BTC, ETH): 20x maximum
 23  //   - Alternative crypto: 10x maximum
 24  // - Funding rate settlement every 8 hours
 25  // - Funding calculation: (Mark Price - Index Price) / Index Price × Funding Interval
 26  // - Maintenance margin rates:
 27  //   - Fiat: 1.5%
 28  //   - Major crypto: 3%
 29  //   - Alt crypto: 5%
 30  // - Liquidation engine with insurance fund and ADL (Auto-Deleveraging)
 31  /**********************************************************************************************************************/
 32  
 33  program delta.perpetuals;
 34  
 35  /**********************************************************************************************************************/
 36  // STRUCTS
 37  /**********************************************************************************************************************/
 38  
 39  // A perpetual futures position.
 40  struct position:
 41      // Position ID (unique).
 42      position_id as u128;
 43      // Trader's address.
 44      trader as address;
 45      // Market ID (e.g., "BTC-PERP").
 46      market_id as field;
 47      // Position side: 0 = Long, 1 = Short.
 48      side as u8;
 49      // Position size in base currency.
 50      size as u128;
 51      // Average entry price (8 decimals).
 52      entry_price as u128;
 53      // Current margin deposited (in sAX).
 54      margin as u128;
 55      // Leverage used (1-50x, stored as 10x = 100).
 56      leverage as u16;
 57      // Accumulated funding (positive = paid, negative = received).
 58      accumulated_funding as i128;
 59      // Block height when position was opened.
 60      opened_at as u32;
 61      // Block height of last modification.
 62      updated_at as u32;
 63      // Status: 0 = Open, 1 = Closed, 2 = Liquidated.
 64      status as u8;
 65  
 66  // Perpetual market configuration.
 67  struct perp_market:
 68      // Underlying asset ID.
 69      underlying as field;
 70      // Maximum allowed leverage (10x = 100, 50x = 500).
 71      max_leverage as u16;
 72      // Initial margin requirement in basis points (e.g., 500 = 5%).
 73      initial_margin_bps as u16;
 74      // Maintenance margin requirement in basis points.
 75      maintenance_margin_bps as u16;
 76      // Asset class: 0 = Fiat, 1 = Major Crypto, 2 = Alt Crypto.
 77      asset_class as u8;
 78      // Whether market is active.
 79      is_active as boolean;
 80      // Minimum position size.
 81      min_position_size as u128;
 82  
 83  // Funding rate data.
 84  struct funding_rate:
 85      // Current funding rate in basis points (can be negative).
 86      rate_bps as i16;
 87      // Mark price (TWAP of order book).
 88      mark_price as u128;
 89      // Index price (oracle price).
 90      index_price as u128;
 91      // Block height of last settlement.
 92      last_settlement_block as u32;
 93      // Accumulated funding since launch.
 94      cumulative_funding as i128;
 95  
 96  // Insurance fund state.
 97  struct insurance_fund:
 98      // Total balance in sAX.
 99      balance as u128;
100      // Total liquidations covered.
101      liquidations_covered as u64;
102      // Total shortfall covered.
103      shortfall_covered as u128;
104  
105  /**********************************************************************************************************************/
106  // MAPPINGS
107  /**********************************************************************************************************************/
108  
109  // Positions by position ID.
110  mapping positions:
111      key as u128.public;
112      value as position.public;
113  
114  // User's active position per market (user_address_hash + market_id -> position_id).
115  mapping user_positions:
116      key as field.public;
117      value as u128.public;
118  
119  // Market configurations.
120  mapping perp_markets:
121      key as field.public;
122      value as perp_market.public;
123  
124  // Funding rates per market.
125  mapping funding_rates:
126      key as field.public;
127      value as funding_rate.public;
128  
129  // Insurance fund state.
130  mapping insurance:
131      key as u8.public;
132      value as insurance_fund.public;
133  
134  // Open interest per market (long side).
135  mapping open_interest_long:
136      key as field.public;
137      value as u128.public;
138  
139  // Open interest per market (short side).
140  mapping open_interest_short:
141      key as field.public;
142      value as u128.public;
143  
144  // Position counter.
145  mapping counters:
146      key as u8.public;
147      value as u128.public;
148  
149  // Liquidation counter.
150  mapping liquidation_counter:
151      key as u8.public;
152      value as u64.public;
153  
154  // Admin address.
155  mapping admin:
156      key as u8.public;
157      value as address.public;
158  
159  /**********************************************************************************************************************/
160  // FUNCTIONS
161  /**********************************************************************************************************************/
162  
163  // Open a new perpetual position.
164  function open_position:
165      // Input the market ID.
166      input r0 as field.public;
167      // Input the side (0 = Long, 1 = Short).
168      input r1 as u8.public;
169      // Input the leverage (e.g., 100 = 10x, 500 = 50x).
170      input r2 as u16.public;
171      // Input the margin amount in sAX.
172      input r3 as u128.public;
173  
174      // Validate side.
175      lte r1 1u8 into r4;
176      assert.eq r4 true;
177  
178      // Validate leverage (at least 1x = 10).
179      gte r2 10u16 into r5;
180      assert.eq r5 true;
181  
182      // Validate margin is positive.
183      gt r3 0u128 into r6;
184      assert.eq r6 true;
185  
186      // Execute position opening.
187      async open_position self.caller r0 r1 r2 r3 into r7;
188      output r7 as delta.perpetuals/open_position.future;
189  
190  finalize open_position:
191      // Input the caller (trader).
192      input r0 as address.public;
193      // Input the market ID.
194      input r1 as field.public;
195      // Input the side.
196      input r2 as u8.public;
197      // Input the leverage.
198      input r3 as u16.public;
199      // Input the margin.
200      input r4 as u128.public;
201  
202      // Get market config.
203      get perp_markets[r1] into r5;
204  
205      // Ensure market is active.
206      assert.eq r5.is_active true;
207  
208      // Validate leverage against market max.
209      lte r3 r5.max_leverage into r6;
210      assert.eq r6 true;
211  
212      // Calculate position size: margin × leverage / 10.
213      cast r3 into r7 as u128;
214      mul r4 r7 into r8;
215      div r8 10u128 into r9; // Position size (notional)
216  
217      // Check minimum position size.
218      gte r9 r5.min_position_size into r10;
219      assert.eq r10 true;
220  
221      // Get current oracle price (from delta.oracle via external call).
222      // For now, we'll use a placeholder - in production this would call delta.oracle::get_price.
223      // Assume price is passed or fetched externally.
224      // Using a constant for demonstration.
225      // In production: get delta.oracle/price_feeds[r1] into current_price;
226  
227      // Get next position ID.
228      get.or_use counters[0u8] 1u128 into r11;
229  
230      // Calculate entry price (would be from oracle).
231      // Placeholder: using 50000_00000000 (50,000 with 8 decimals).
232      // In production, this comes from the matching engine or oracle.
233  
234      // Construct the position.
235      cast r11 r0 r1 r2 r9 50000_00000000u128 r4 r3 0i128 block.height block.height 0u8 into r12 as position;
236  
237      // Store the position.
238      set r12 into positions[r11];
239  
240      // Increment position counter.
241      add r11 1u128 into r13;
242      set r13 into counters[0u8];
243  
244      // Update open interest.
245      is.eq r2 0u8 into r14; // Is long?
246      branch.eq r14 true to update_long_oi;
247  
248      // Update short OI.
249      get.or_use open_interest_short[r1] 0u128 into r15;
250      add r15 r9 into r16;
251      set r16 into open_interest_short[r1];
252      branch.eq true true to end;
253  
254      position update_long_oi;
255      get.or_use open_interest_long[r1] 0u128 into r17;
256      add r17 r9 into r18;
257      set r18 into open_interest_long[r1];
258  
259      position end;
260  
261  /**********************************************************************************************************************/
262  
263  // Close a position (fully or partially).
264  function close_position:
265      // Input the position ID.
266      input r0 as u128.public;
267      // Input the amount to close (0 = close all).
268      input r1 as u128.public;
269  
270      async close_position self.caller r0 r1 into r2;
271      output r2 as delta.perpetuals/close_position.future;
272  
273  finalize close_position:
274      // Input the caller.
275      input r0 as address.public;
276      // Input the position ID.
277      input r1 as u128.public;
278      // Input the close amount.
279      input r2 as u128.public;
280  
281      // Get the position.
282      get positions[r1] into r3;
283  
284      // Verify caller owns the position.
285      assert.eq r0 r3.trader;
286  
287      // Verify position is open.
288      assert.eq r3.status 0u8;
289  
290      // Determine close amount.
291      is.eq r2 0u128 into r4;
292      ternary r4 r3.size r2 into r5; // Close all if 0, else close specified
293  
294      // Ensure close amount doesn't exceed position size.
295      lte r5 r3.size into r6;
296      assert.eq r6 true;
297  
298      // Calculate remaining size.
299      sub r3.size r5 into r7;
300  
301      // Calculate PnL (simplified - would need current price).
302      // PnL = (current_price - entry_price) × size for longs
303      // PnL = (entry_price - current_price) × size for shorts
304      // Placeholder: using entry_price as current_price (no PnL).
305  
306      // Update open interest.
307      is.eq r3.side 0u8 into r8;
308      branch.eq r8 true to update_long_oi_close;
309  
310      // Update short OI.
311      get open_interest_short[r3.market_id] into r9;
312      sub r9 r5 into r10;
313      set r10 into open_interest_short[r3.market_id];
314      branch.eq true true to update_position;
315  
316      position update_long_oi_close;
317      get open_interest_long[r3.market_id] into r11;
318      sub r11 r5 into r12;
319      set r12 into open_interest_long[r3.market_id];
320  
321      position update_position;
322  
323      // Update or close position.
324      is.eq r7 0u128 into r13; // Is fully closed?
325      ternary r13 1u8 0u8 into r14; // Status: 1 = Closed, 0 = Open
326  
327      // Calculate new margin (proportional).
328      mul r3.margin r7 into r15;
329      div r15 r3.size into r16;
330  
331      cast r3.position_id r3.trader r3.market_id r3.side r7 r3.entry_price r16 r3.leverage r3.accumulated_funding r3.opened_at block.height r14 into r17 as position;
332      set r17 into positions[r1];
333  
334      // TODO: Return margin + PnL to user.
335  
336  /**********************************************************************************************************************/
337  
338  // Add margin to an existing position.
339  function add_margin:
340      // Input the position ID.
341      input r0 as u128.public;
342      // Input the amount to add.
343      input r1 as u128.public;
344  
345      // Validate amount.
346      gt r1 0u128 into r2;
347      assert.eq r2 true;
348  
349      async add_margin self.caller r0 r1 into r3;
350      output r3 as delta.perpetuals/add_margin.future;
351  
352  finalize add_margin:
353      // Input the caller.
354      input r0 as address.public;
355      // Input the position ID.
356      input r1 as u128.public;
357      // Input the amount.
358      input r2 as u128.public;
359  
360      // Get the position.
361      get positions[r1] into r3;
362  
363      // Verify caller owns the position.
364      assert.eq r0 r3.trader;
365  
366      // Verify position is open.
367      assert.eq r3.status 0u8;
368  
369      // Add margin.
370      add r3.margin r2 into r4;
371  
372      // Update position.
373      cast r3.position_id r3.trader r3.market_id r3.side r3.size r3.entry_price r4 r3.leverage r3.accumulated_funding r3.opened_at block.height r3.status into r5 as position;
374      set r5 into positions[r1];
375  
376      // TODO: Transfer margin from user's balance.
377  
378  /**********************************************************************************************************************/
379  
380  // Settle funding rate for a market. Called periodically (every 8 hours).
381  function settle_funding:
382      // Input the market ID.
383      input r0 as field.public;
384  
385      async settle_funding r0 into r1;
386      output r1 as delta.perpetuals/settle_funding.future;
387  
388  finalize settle_funding:
389      // Input the market ID.
390      input r0 as field.public;
391  
392      // Get current funding rate.
393      cast 0i16 0u128 0u128 0u32 0i128 into r1 as funding_rate;
394      get.or_use funding_rates[r0] r1 into r2;
395  
396      // Check if enough time has passed (8 hours = ~2880 blocks at 10s).
397      sub block.height r2.last_settlement_block into r3;
398      gte r3 2880u32 into r4;
399      assert.eq r4 true;
400  
401      // Get oracle price (index price).
402      // In production: call delta.oracle::get_price(r0).
403      // Placeholder value.
404  
405      // Calculate funding rate.
406      // rate = (mark_price - index_price) / index_price
407      // For now, using a simplified calculation.
408      // In production, mark_price comes from TWAP of order book.
409  
410      // Get open interest to calculate funding payments.
411      get.or_use open_interest_long[r0] 0u128 into r5;
412      get.or_use open_interest_short[r0] 0u128 into r6;
413  
414      // Funding is paid from longs to shorts when rate is positive,
415      // and from shorts to longs when rate is negative.
416  
417      // Update cumulative funding.
418      cast r2.rate_bps into r7 as i128;
419      add r2.cumulative_funding r7 into r8;
420  
421      // Store updated funding rate.
422      cast r2.rate_bps r2.mark_price r2.index_price block.height r8 into r9 as funding_rate;
423      set r9 into funding_rates[r0];
424  
425      // Note: Individual position funding is calculated on-demand
426      // when positions are modified or closed.
427  
428  /**********************************************************************************************************************/
429  
430  // Liquidate an undercollateralized position.
431  function liquidate:
432      // Input the position ID to liquidate.
433      input r0 as u128.public;
434  
435      async liquidate self.caller r0 into r1;
436      output r1 as delta.perpetuals/liquidate.future;
437  
438  finalize liquidate:
439      // Input the liquidator (caller).
440      input r0 as address.public;
441      // Input the position ID.
442      input r1 as u128.public;
443  
444      // Get the position.
445      get positions[r1] into r2;
446  
447      // Verify position is open.
448      assert.eq r2.status 0u8;
449  
450      // Get market config for maintenance margin.
451      get perp_markets[r2.market_id] into r3;
452  
453      // Calculate maintenance margin required.
454      // maintenance_margin = position_size × entry_price × maintenance_margin_bps / 10000.
455      mul r2.size r2.entry_price into r4;
456      div r4 100000000u128 into r5; // Adjust for decimals
457      cast r3.maintenance_margin_bps into r6 as u128;
458      mul r5 r6 into r7;
459      div r7 10000u128 into r8; // Required margin
460  
461      // Get current margin ratio.
462      // In production, would calculate PnL and adjust margin.
463      // For now, checking if margin < maintenance_margin.
464      lt r2.margin r8 into r9;
465      assert.eq r9 true; // Position must be liquidatable
466  
467      // Mark position as liquidated.
468      cast r2.position_id r2.trader r2.market_id r2.side 0u128 r2.entry_price 0u128 r2.leverage r2.accumulated_funding r2.opened_at block.height 2u8 into r10 as position;
469      set r10 into positions[r1];
470  
471      // Update open interest.
472      is.eq r2.side 0u8 into r11;
473      branch.eq r11 true to update_long_oi_liq;
474  
475      get open_interest_short[r2.market_id] into r12;
476      sub r12 r2.size into r13;
477      set r13 into open_interest_short[r2.market_id];
478      branch.eq true true to update_insurance;
479  
480      position update_long_oi_liq;
481      get open_interest_long[r2.market_id] into r14;
482      sub r14 r2.size into r15;
483      set r15 into open_interest_long[r2.market_id];
484  
485      position update_insurance;
486  
487      // Update insurance fund and liquidation count.
488      cast 0u128 0u64 0u128 into r16 as insurance_fund;
489      get.or_use insurance[0u8] r16 into r17;
490  
491      add r17.liquidations_covered 1u64 into r18;
492  
493      // In production: calculate shortfall and use insurance fund.
494      // For now, just increment counter.
495      cast r17.balance r18 r17.shortfall_covered into r19 as insurance_fund;
496      set r19 into insurance[0u8];
497  
498      // TODO: Pay liquidation bonus to liquidator.
499  
500  /**********************************************************************************************************************/
501  
502  // Register a new perpetual market (admin only).
503  function register_perp_market:
504      // Input the market ID.
505      input r0 as field.public;
506      // Input the underlying asset.
507      input r1 as field.public;
508      // Input the max leverage.
509      input r2 as u16.public;
510      // Input the initial margin bps.
511      input r3 as u16.public;
512      // Input the maintenance margin bps.
513      input r4 as u16.public;
514      // Input the asset class.
515      input r5 as u8.public;
516      // Input the minimum position size.
517      input r6 as u128.public;
518  
519      async register_perp_market self.caller r0 r1 r2 r3 r4 r5 r6 into r7;
520      output r7 as delta.perpetuals/register_perp_market.future;
521  
522  finalize register_perp_market:
523      // Input the caller.
524      input r0 as address.public;
525      // Input the market ID.
526      input r1 as field.public;
527      // Input the underlying.
528      input r2 as field.public;
529      // Input the max leverage.
530      input r3 as u16.public;
531      // Input the initial margin bps.
532      input r4 as u16.public;
533      // Input the maintenance margin bps.
534      input r5 as u16.public;
535      // Input the asset class.
536      input r6 as u8.public;
537      // Input the min position size.
538      input r7 as u128.public;
539  
540      // Verify caller is admin.
541      get admin[0u8] into r8;
542      assert.eq r0 r8;
543  
544      // Construct market config.
545      cast r2 r3 r4 r5 r6 true r7 into r9 as perp_market;
546  
547      // Store market.
548      set r9 into perp_markets[r1];
549  
550      // Initialize funding rate.
551      cast 0i16 0u128 0u128 block.height 0i128 into r10 as funding_rate;
552      set r10 into funding_rates[r1];
553  
554  /**********************************************************************************************************************/
555  
556  // Initialize the perpetuals contract.
557  function initialize:
558      // Input the admin address.
559      input r0 as address.public;
560  
561      async initialize self.caller r0 into r1;
562      output r1 as delta.perpetuals/initialize.future;
563  
564  finalize initialize:
565      // Input the caller.
566      input r0 as address.public;
567      // Input the admin.
568      input r1 as address.public;
569  
570      // Check if already initialized.
571      contains admin[0u8] into r2;
572      assert.eq r2 false;
573  
574      // Set admin.
575      set r1 into admin[0u8];
576  
577      // Initialize counters.
578      set 1u128 into counters[0u8];
579  
580      // Initialize insurance fund.
581      cast 0u128 0u64 0u128 into r3 as insurance_fund;
582      set r3 into insurance[0u8];
583  
584  /**********************************************************************************************************************/
585  
586  // Contribute to insurance fund.
587  function contribute_insurance:
588      // Input the amount to contribute.
589      input r0 as u128.public;
590  
591      // Validate amount.
592      gt r0 0u128 into r1;
593      assert.eq r1 true;
594  
595      async contribute_insurance self.caller r0 into r2;
596      output r2 as delta.perpetuals/contribute_insurance.future;
597  
598  finalize contribute_insurance:
599      // Input the caller.
600      input r0 as address.public;
601      // Input the amount.
602      input r1 as u128.public;
603  
604      // Get current insurance fund.
605      cast 0u128 0u64 0u128 into r2 as insurance_fund;
606      get.or_use insurance[0u8] r2 into r3;
607  
608      // Add contribution.
609      add r3.balance r1 into r4;
610  
611      cast r4 r3.liquidations_covered r3.shortfall_covered into r5 as insurance_fund;
612      set r5 into insurance[0u8];
613  
614      // TODO: Transfer funds from contributor.
615  
616  /**********************************************************************************************************************/
617  
618  // Get funding rate for a market.
619  function get_funding_rate:
620      // Input the market ID.
621      input r0 as field.public;
622  
623      async get_funding_rate r0 into r1;
624      output r1 as delta.perpetuals/get_funding_rate.future;
625  
626  finalize get_funding_rate:
627      // Input the market ID.
628      input r0 as field.public;
629  
630      // Get funding rate (fails if market doesn't exist).
631      get funding_rates[r0] into r1;
632  
633      // Note: ADL doesn't support return values from finalize.
634      // Clients query funding_rates mapping directly.
635  
636  /**********************************************************************************************************************/