Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
.cursor/cli.json

backend/testcandledata/target/
fuzz/target/
/node_modules
53 changes: 35 additions & 18 deletions contracts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,36 @@ Move package `strike`, edition `2024`. The Sui framework dependency is pinned to
`testnet-v1.75.1` — the same release as the `sui` CLI in `flake.nix` and the
backend `sui-sdk`. Bump all three together, never one alone.

| File | Module | Contents |
| ------------------------ | ------------------- | ------------------------------------------------------------ |
| `sources/units.move` | `strike::units` | `Price`/`Size`/`Leverage`/`UsdcAmount` fixed-point newtypes |
| `sources/risk.move` | `strike::risk` | Margin/liquidation formulas; ALL u128 scaling arithmetic |
| `sources/constants.move` | `strike::constants` | `FLOAT_SCALING` (10^6, USDC's 6 decimals), margin rate |
| `sources/margin.move` | `strike::strike` | `MarginAccount`: USDC deposits/withdrawals, owner checks |
| `sources/order.move` | `strike::order` | `Side` enum + `match_side!`, `OrderId`, `Order` struct |
| `sources/orderbook.move` | `strike::orderbook` | CLOB: matching, cancellation, liquidation sweep, events |
| `sources/pool.move` | `strike::pool` | `Pool`: vault + orderbook + oracle, entry points for trading |
| `sources/vault.move` | `strike::vault` | Pooled USDC collateral |
| `sources/oracle.move` | `strike::oracle` | Price oracle object |
| `tests/` | `strike::*_tests` | One `#[test_only]` module per source module |
| File | Module | Contents |
| ------------------------ | ------------------- | -------------------------------------------------------------- |
| `sources/units.move` | `strike::units` | Fixed-point newtypes + `FLOAT_SCALING` (10^6, USDC's decimals) |
| `sources/risk.move` | `strike::risk` | Margin/liquidation/funding formulas; ALL u128 scaling math |
| `sources/margin.move` | `strike::strike` | `MarginAccount`: USDC deposits/withdrawals, owner checks |
| `sources/order.move` | `strike::order` | `Side` enum + `match_side!`, `OrderId`, `Order` struct |
| `sources/orderbook.move` | `strike::orderbook` | CLOB: matching, cancellation, liquidation sweep, funding |
| `sources/pool.move` | `strike::pool` | `Pool`: vault + orderbook + oracle, entry points, cadence |
| `sources/vault.move` | `strike::vault` | Pooled USDC collateral |
| `sources/oracle.move` | `strike::oracle` | Price oracle object |
| `tests/` | `strike::*_tests` | One `#[test_only]` module per source module |

## Module Organization

Package by domain, never by kind. A module is one domain concept with its data,
its operations, and its constants together:

- **No grab-bag modules.** `types`, `constants`, `utils`, `helpers`, `common`
are banned names and banned concepts — they scale into dumping grounds where
nothing can be found. This package already dissolved both a `types` and a
`constants` module.
- **Constants live with the code that owns their meaning.** `FLOAT_SCALING` is
the unit system's, so it lives in `units`; the funding rate cap is applied
inside `risk::funding_rate_bps`, so it lives in `risk`; the funding interval
and default margin rate parameterize `Pool`, so they live in `pool`. Placement
follows the consumer that defines the semantics, not the syntactic category
"constant".
- **The test for a new item's home:** which module's doc comment would have to
explain it? That module owns it. If no existing module's domain covers it, the
item is a new domain — give it a new, domain-named module.

Build and test from `contracts/`, inside the dev shell:

Expand All @@ -42,12 +60,11 @@ blindness unrepresentable. Domain logic operates on domain types end to end:
The `side.is_bid()` projection is reserved for event payload projection; using
it for domain branching is a violation.
- **`Price`, `Size`, `Leverage`, `UsdcAmount` (`strike::units`), never bare
`u64`.** All four are fixed-point values scaled by
`constants::float_scaling()` (or USDC base units, which share the 10^6 scale —
`units.move` documents the coupling). They are NOT interchangeable:
multiplying two scaled values double-scales, and mixing units silently
corrupts margin math. A bare `u64` crossing a function boundary for any of
these quantities is a bug.
`u64`.** All four are fixed-point values scaled by `units::float_scaling()`
(or USDC base units, which share the 10^6 scale — `units.move` documents the
coupling). They are NOT interchangeable: multiplying two scaled values
double-scales, and mixing units silently corrupts margin math. A bare `u64`
crossing a function boundary for any of these quantities is a bug.
- **`OrderId` (`strike::order`) is the cancellation key.** Unlike
`(account, price)` it stays unique when one account rests several orders at
the same price level. Never key order lookup on anything else.
Expand Down
31 changes: 0 additions & 31 deletions contracts/sources/constants.move

This file was deleted.

77 changes: 26 additions & 51 deletions contracts/sources/margin.move
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@ use sui::coin::{Self, Coin};
use sui::event;
use usdc::usdc::USDC;

// Error constants
// === Errors ===

const ENotOwner: u64 = 1;
const EInsufficientBalance: u64 = 2;

// === Structs ===

/// MarginAccount has `key` but deliberately NOT `store`: without `store`,
/// `transfer::public_transfer` and embedding in other modules' structs are
/// impossible, so the account can only move via this module's `keep`. This
/// keeps the account bound to `owner`, which `deposit`/`withdraw` verify
/// against the transaction sender.
public struct MarginAccount has key, store {
public struct MarginAccount has key {
id: UID,
owner: address,
balance: Balance<USDC>,
}

// === Events ===

public enum MarginAccountEventKind has copy, drop {
Creation,
Deposit { amount: u64 },
Expand All @@ -31,6 +36,8 @@ public struct MarginAccountEvent has copy, drop {
kind: MarginAccountEventKind,
}

// === Public Functions ===

/// Create an empty margin account owned by the sender. Place it with
/// `keep` (the type lacks `store`, so nothing else can move it).
public fun new(ctx: &mut TxContext): MarginAccount {
Expand Down Expand Up @@ -80,60 +87,21 @@ public fun new_with_deposit(
margin_account
}

/// Transfer the account to the transaction sender. The only way to place a
/// MarginAccount at an address: the struct lacks `store`, so external code
/// cannot call `transfer::public_transfer` on it.
public fun keep(margin_account: MarginAccount, ctx: &TxContext) {
transfer::transfer(margin_account, tx_context::sender(ctx));
}

/// Add the full value of `coin` (USDC base units) to the account.
/// Aborts with `ENotOwner` unless the sender owns the account.
public fun deposit(
margin_account: &mut MarginAccount,
coin: Coin<USDC>,
ctx: &mut TxContext,
) {
// Verify that the sender is the owner of the margin account
assert!(tx_context::sender(ctx) == margin_account.owner, ENotOwner);

// Get the deposit amount for the event
let deposit_amount = coin::value(&coin);

// Convert Coin to Balance and add it to the margin account's balance
let deposit_balance = coin::into_balance(coin);
balance::join(&mut margin_account.balance, deposit_balance);

// Emit deposit event
event::emit(MarginAccountEvent {
margin_account_id: object::uid_to_inner(&margin_account.id),
kind: MarginAccountEventKind::Deposit { amount: deposit_amount },
});
}

public fun withdraw<T>(
margin_account: &mut MarginAccount,
withdraw_amount: u64,
ctx: &mut TxContext,
): Balance<T> {
assert!(tx_context::sender(ctx) == margin_account.owner, ENotOwner);

let key = BalanceKey<T> {};

let key_exists = margin_account.balances.contains(key);
assert!(key_exists, EInsufficientBalance);

event::emit(MarginAccountEvent {
margin_account_id: object::uid_to_inner(&margin_account.id),
kind: MarginAccountEventKind::Withdrawal { withdraw_amount },
});

let acc_balance: &mut Balance<T> = &mut margin_account.balances[key];
let acc_value = acc_balance.value();
assert!(acc_value >= withdraw_amount, EInsufficientBalance);
if (withdraw_amount == acc_value) {
margin_account.balances.remove(key)
} else {
acc_balance.split(withdraw_amount)
}
}

public fun deposit(margin_account: &mut MarginAccount, coin: Coin<USDC>) {
// TODO: make verification of account owner

assert!(verify_owner(margin_account, tx_context::sender(ctx)), ENotOwner);
let deposit_amount = coin::value(&coin);
let deposit_balance = coin::into_balance(coin);

Expand All @@ -153,7 +121,7 @@ public fun withdraw(
withdraw_amount: u64,
ctx: &mut TxContext,
): Coin<USDC> {
assert!(tx_context::sender(ctx) == margin_account.owner, ENotOwner);
assert!(verify_owner(margin_account, tx_context::sender(ctx)), ENotOwner);
assert!(
balance::value(&margin_account.balance) >= withdraw_amount,
EInsufficientBalance,
Expand All @@ -173,6 +141,8 @@ public fun withdraw(
withdraw_coin
}

// === View Functions ===

/// Current balance in USDC base units.
public fun balance(margin_account: &MarginAccount): u64 {
balance::value(&margin_account.balance)
Expand All @@ -183,8 +153,13 @@ public fun owner(margin_account: &MarginAccount): address {
margin_account.owner
}

// === Package Functions ===

/// Whether `sender` is the account owner; every mutating entry point
/// gates on this.
public fun verify_owner(margin_account: &MarginAccount, sender: address): bool {
public(package) fun verify_owner(
margin_account: &MarginAccount,
sender: address,
): bool {
margin_account.owner == sender
}
15 changes: 13 additions & 2 deletions contracts/sources/oracle.move
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module strike::oracle;
use strike::units::{Self, Price};
use sui::event;

// === Structs ===

/// Price feed for a pool. Writes are gated by the pool's `PriceCap` (see
/// `pool::update_price`); this module only enforces the data shape.
public struct Oracle has key, store {
Expand All @@ -11,11 +13,15 @@ public struct Oracle has key, store {
last_update_time: u64,
}

// === Events ===

public struct PriceUpdate has copy, drop {
price: u64,
timestamp: u64,
}

// === Package Functions ===

public(package) fun new(ctx: &mut TxContext): Oracle {
Oracle {
id: object::new(ctx),
Expand All @@ -39,10 +45,15 @@ public(package) fun update_price(
});
}

public(package) fun get_price(oracle: &Oracle): Price {
// === View Functions ===

/// The most recently written price; zero only before the first update
/// (`pool::new` seeds it at creation).
public(package) fun price(oracle: &Oracle): Price {
oracle.price
}

public(package) fun get_last_update_time(oracle: &Oracle): u64 {
/// Epoch-milliseconds timestamp of the last `update_price`.
public(package) fun last_update_time(oracle: &Oracle): u64 {
oracle.last_update_time
}
6 changes: 6 additions & 0 deletions contracts/sources/order.move
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module strike::order;

use strike::units::{Self, Price, Size, Leverage, UsdcAmount};

// === Side ===

/// Which side of the book an order is on. Bid = buy/long, Ask = sell/short.
/// Replaces `is_bid: bool` at call sites: `order::bid()` reads where a bare
/// `true` did not. Variants are module-internal (Move enums), so consumers
Expand Down Expand Up @@ -32,6 +34,8 @@ public fun is_bid(side: Side): bool {
}
}

// === OrderId ===

/// Identifier unique within one orderbook; assigned sequentially when an
/// order is placed. The cancellation key: unlike (account, price), it
/// stays unique when an account places several orders at one price level.
Expand All @@ -55,6 +59,8 @@ public fun order_id_eq(id: OrderId, other: OrderId): bool {

public use fun order_id_eq as OrderId.eq;

// === Order ===

public struct Order has drop, store {
/// Assigned by `orderbook::place_limit_order`; zero until placed.
order_id: OrderId,
Expand Down
Loading
Loading