Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d71c24a
feat: margin type config for override margin type behavior
LukasDeco Jan 29, 2026
39227c4
feat: tests for margin override
LukasDeco Jan 30, 2026
a16dc8f
feat: idl, anchor tests, more cargo tests
LukasDeco Jan 30, 2026
ea34060
feat: helpful comments
LukasDeco Feb 3, 2026
1cad8ef
fix: logic for user margin ratio evaluated on per position basis
LukasDeco Feb 5, 2026
f8ed48b
chore: use maintenance check on cross account place order req on iso …
LukasDeco Feb 9, 2026
033f9e0
feat: margin type config on transfer isolated + tests
LukasDeco Feb 9, 2026
f675af5
feat: transfer to isolated conditional margin overrides + fulfill_per…
LukasDeco Feb 10, 2026
9504a39
fix: recalc is high lev on each perp position calc margin req
LukasDeco Feb 10, 2026
f85cc94
fix: tests and build errors
LukasDeco Feb 11, 2026
6920c50
fix: cargo fmt
LukasDeco Feb 11, 2026
280bd1a
feat: additional test scenarios
LukasDeco Feb 12, 2026
9a98f9e
fix: using wrong margin type on fulfill perp order margin type config
LukasDeco Feb 12, 2026
0148ca2
fix: anchor tests wrong oracle prices on order margin checks
LukasDeco Feb 14, 2026
51089c9
fix: bug with custom margin ratios
LukasDeco Feb 14, 2026
62ad6e6
fix: incorrect comments on anchor tests w oracle prices
LukasDeco Feb 14, 2026
f353b5d
fix: another anchor test fix
LukasDeco Feb 16, 2026
ea85892
feat: isolated transfer anchor tests
LukasDeco Feb 16, 2026
c31ef21
fix: anchor tests broken for iso transfer
LukasDeco Feb 18, 2026
3ec549a
fix: fulfill order pr feedback how define is isolated flag
LukasDeco Feb 24, 2026
cc8ea34
fix: perp order fulfillment maker is isolated flag
LukasDeco Mar 3, 2026
88f7a74
fix: tests for maker_fills map
LukasDeco Mar 3, 2026
6cab857
Merge branch 'master' into lukas/be-67-isolated-longterm-solution-run…
LukasDeco Mar 16, 2026
351bded
Merge branch 'master' into lukas/be-67-isolated-longterm-solution-run…
LukasDeco Mar 28, 2026
3f145d9
fix: install ts later version for ci
LukasDeco Mar 29, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ jobs:
run: yarn

- name: install typescript
run: npm install typescript -g
run: npm install typescript@5.4.5 -g

- name: install mocha
run: |
Expand Down Expand Up @@ -180,7 +180,7 @@ jobs:
- name: Install ts-mocha and typescript
run: |
npm install -g ts-mocha
npm install -g typescript
npm install [email protected] -g
- name: Run tests
env:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ cd sdk/ && yarn && yarn build && cd ..

## Running Rust Test

For running cargo tests, you'll need version 1.70. You'll also need Solana CLI version 1.16.27

```bash
rustup override set 1.70
cargo test
```

Expand All @@ -60,11 +63,13 @@ bash test-scripts/run-anchor-tests.sh
We've provided a devcontainer `Dockerfile` to help you spin up a dev environment with the correct versions of Rust, Solana, and Anchor for program development.

Build the container and tag it `drift-dev`:

```
cd .devcontainer && docker build -t drift-dev .
```

Open a shell to the container:

```
# Find the container ID first
docker ps
Expand All @@ -83,6 +88,7 @@ Alternatively use an extension provided by your IDE to make use of the dev conta
```

Use the dev container as you would a local build environment:

```
# build program
anchor build
Expand Down
13 changes: 11 additions & 2 deletions programs/drift/src/controller/isolated_position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::math::liquidation::is_isolated_margin_being_liquidated;
use crate::math::margin::{validate_spot_margin_trading, MarginRequirementType};
use crate::math::safe_math::SafeMath;
use crate::state::events::{DepositDirection, DepositExplanation, DepositRecord};
use crate::state::margin_calculation::MarginTypeConfig;
use crate::state::oracle_map::OracleMap;
use crate::state::perp_market::MarketStatus;
use crate::state::perp_market_map::PerpMarketMap;
Expand Down Expand Up @@ -234,7 +235,10 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>(
&perp_market_map,
&spot_market_map,
oracle_map,
MarginRequirementType::Initial,
MarginTypeConfig::CrossMarginOverride {
margin_requirement_type: MarginRequirementType::Initial,
default_margin_requirement_type: MarginRequirementType::Maintenance,
},
spot_market_index,
amount as u128,
user_stats,
Expand Down Expand Up @@ -298,7 +302,12 @@ pub fn transfer_isolated_perp_position_deposit<'c: 'info, 'info>(
&perp_market_map,
&spot_market_map,
oracle_map,
MarginRequirementType::Initial,
MarginTypeConfig::IsolatedPositionOverride {
margin_requirement_type: MarginRequirementType::Initial,
default_isolated_margin_requirement_type: MarginRequirementType::Maintenance,
cross_margin_requirement_type: MarginRequirementType::Maintenance,
market_index: perp_market_index,
},
0,
0,
user_stats,
Expand Down
70 changes: 57 additions & 13 deletions programs/drift/src/controller/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ use crate::state::events::{emit_stack, get_order_action_record, OrderActionRecor
use crate::state::events::{OrderAction, OrderActionExplanation};
use crate::state::fill_mode::FillMode;
use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod};
use crate::state::margin_calculation::{MarginCalculation, MarginContext};
use crate::state::margin_calculation::{MarginCalculation, MarginContext, MarginTypeConfig};
use crate::state::oracle::{OraclePriceData, StrictOraclePrice};
use crate::state::oracle_map::OracleMap;
use crate::state::order_params::{
Expand Down Expand Up @@ -379,12 +379,19 @@ pub fn place_perp_order(

// when orders are placed in bulk, only need to check margin on last place
if options.enforce_margin_check && !options.is_liquidation() {
// if isolated position, use the isolated margin calculation
let isolated_market_index = if user.perp_positions[position_index].is_isolated() {
Some(market_index)
} else {
None
};
meets_place_order_margin_requirement(
user,
perp_market_map,
spot_market_map,
oracle_map,
options.risk_increasing,
isolated_market_index,
)?;
}

Expand Down Expand Up @@ -1795,6 +1802,7 @@ fn fulfill_perp_order(

let user_order_position_decreasing =
determine_if_user_order_is_position_decreasing(user, market_index, user_order_index)?;
let user_is_isolated_position = user.get_perp_position(market_index)?.is_isolated();

let perp_market = perp_market_map.get_ref(&market_index)?;
let limit_price = fill_mode.get_limit_price(
Expand Down Expand Up @@ -1826,7 +1834,7 @@ fn fulfill_perp_order(

let mut base_asset_amount = 0_u64;
let mut quote_asset_amount = 0_u64;
let mut maker_fills: BTreeMap<Pubkey, i64> = BTreeMap::new();
let mut maker_fills: BTreeMap<Pubkey, (i64, bool)> = BTreeMap::new();
let maker_direction = user.orders[user_order_index].direction.opposite();
for fulfillment_method in fulfillment_methods.iter() {
if user.orders[user_order_index].status != OrderStatus::Open {
Expand Down Expand Up @@ -1891,6 +1899,8 @@ fn fulfill_perp_order(
}
PerpFulfillmentMethod::Match(maker_key, maker_order_index, maker_price) => {
let mut maker = makers_and_referrer.get_ref_mut(maker_key)?;
let maker_is_isolated_position =
maker.get_perp_position(market_index)?.is_isolated();
let mut maker_stats = if maker.authority == user.authority {
None
} else {
Expand Down Expand Up @@ -1939,6 +1949,7 @@ fn fulfill_perp_order(
maker_key,
maker_direction,
maker_fill_base_asset_amount,
maker_is_isolated_position,
)?;
}

Expand All @@ -1961,7 +1972,7 @@ fn fulfill_perp_order(
quote_asset_amount
)?;

let total_maker_fill = maker_fills.values().sum::<i64>();
let total_maker_fill = maker_fills.values().map(|(fill, _)| fill).sum::<i64>();

validate!(
total_maker_fill.unsigned_abs() <= base_asset_amount,
Expand All @@ -1979,13 +1990,29 @@ fn fulfill_perp_order(
-(base_asset_amount as i64)
};

let mut context = MarginContext::standard(if user_order_position_decreasing {
let margin_requirement_type = if user_order_position_decreasing {
MarginRequirementType::Maintenance
} else {
MarginRequirementType::Fill
})
.fuel_perp_delta(market_index, taker_base_asset_amount_delta)
.fuel_numerator(user, now);
};

let margin_type_config = if user_is_isolated_position {
MarginTypeConfig::IsolatedPositionOverride {
market_index,
margin_requirement_type,
default_isolated_margin_requirement_type: MarginRequirementType::Maintenance,
cross_margin_requirement_type: MarginRequirementType::Maintenance,
}
} else {
MarginTypeConfig::CrossMarginOverride {
margin_requirement_type,
default_margin_requirement_type: MarginRequirementType::Maintenance,
}
};

let mut context = MarginContext::standard_with_config(margin_type_config)
.fuel_perp_delta(market_index, taker_base_asset_amount_delta)
.fuel_numerator(user, now);

if oracle_stale_for_margin && !user_order_position_decreasing {
context = context.margin_ratio_override(MARGIN_PRECISION);
Expand Down Expand Up @@ -2033,7 +2060,7 @@ fn fulfill_perp_order(
}
}

for (maker_key, maker_base_asset_amount_filled) in maker_fills {
for (maker_key, (maker_base_asset_amount_filled, maker_is_isolated_position)) in maker_fills {
let mut maker = makers_and_referrer.get_ref_mut(&maker_key)?;

let maker_stats = if maker.authority == user.authority {
Expand All @@ -2048,7 +2075,21 @@ fn fulfill_perp_order(
market_index,
)?;

let mut context = MarginContext::standard(margin_type)
let margin_type_config = if maker_is_isolated_position {
MarginTypeConfig::IsolatedPositionOverride {
market_index,
margin_requirement_type: margin_type,
default_isolated_margin_requirement_type: MarginRequirementType::Maintenance,
cross_margin_requirement_type: MarginRequirementType::Maintenance,
}
} else {
MarginTypeConfig::CrossMarginOverride {
margin_requirement_type: margin_type,
default_margin_requirement_type: MarginRequirementType::Maintenance,
}
};

let mut context = MarginContext::standard_with_config(margin_type_config)
.fuel_perp_delta(market_index, -maker_base_asset_amount_filled)
.fuel_numerator(&maker, now);

Expand Down Expand Up @@ -2147,20 +2188,21 @@ fn get_referrer<'a>(

#[inline(always)]
fn update_maker_fills_map(
map: &mut BTreeMap<Pubkey, i64>,
map: &mut BTreeMap<Pubkey, (i64, bool)>,
maker_key: &Pubkey,
maker_direction: PositionDirection,
fill: u64,
is_isolated_position: bool,
) -> DriftResult {
let signed_fill = match maker_direction {
PositionDirection::Long => fill.cast::<i64>()?,
PositionDirection::Short => -fill.cast::<i64>()?,
};

if let Some(maker_filled) = map.get_mut(maker_key) {
*maker_filled = maker_filled.safe_add(signed_fill)?;
*maker_filled = (maker_filled.0.safe_add(signed_fill)?, is_isolated_position);
} else {
map.insert(*maker_key, signed_fill);
map.insert(*maker_key, (signed_fill, is_isolated_position));
}

Ok(())
Expand Down Expand Up @@ -3920,6 +3962,7 @@ pub fn place_spot_order(
spot_market_map,
oracle_map,
options.risk_increasing,
None, // no isolated positions for spot positions
)?;
}

Expand Down Expand Up @@ -4574,7 +4617,7 @@ fn fulfill_spot_order(

let mut base_asset_amount = 0_u64;
let mut quote_asset_amount = 0_u64;
let mut maker_fills: BTreeMap<Pubkey, i64> = BTreeMap::new();
let mut maker_fills: BTreeMap<Pubkey, (i64, bool)> = BTreeMap::new();
let maker_direction = user.orders[user_order_index].direction.opposite();
for fulfillment_method in fulfillment_methods.iter() {
if user.orders[user_order_index].status != OrderStatus::Open {
Expand Down Expand Up @@ -4616,6 +4659,7 @@ fn fulfill_spot_order(
maker_key,
maker_direction,
base_filled,
false,
)?;
}

Expand Down
49 changes: 40 additions & 9 deletions programs/drift/src/controller/orders/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13433,28 +13433,59 @@ mod update_maker_fills_map {

#[test]
fn test() {
let mut map: BTreeMap<Pubkey, i64> = BTreeMap::new();
let mut map: BTreeMap<Pubkey, (i64, bool)> = BTreeMap::new();

let maker_key = Pubkey::new_unique();
let fill = 100;
let direction = PositionDirection::Long;
update_maker_fills_map(&mut map, &maker_key, direction, fill).unwrap();
update_maker_fills_map(&mut map, &maker_key, direction, fill, false).unwrap();

assert_eq!(*map.get(&maker_key).unwrap(), fill as i64);
assert_eq!(map.get(&maker_key).unwrap().0, fill as i64);
assert_eq!(map.get(&maker_key).unwrap().1, false);

update_maker_fills_map(&mut map, &maker_key, direction, fill).unwrap();
update_maker_fills_map(&mut map, &maker_key, direction, fill, false).unwrap();

assert_eq!(*map.get(&maker_key).unwrap(), 2 * fill as i64);
assert_eq!(map.get(&maker_key).unwrap().0, 2 * fill as i64);
assert_eq!(map.get(&maker_key).unwrap().1, false);

let maker_key = Pubkey::new_unique();
let direction = PositionDirection::Short;
update_maker_fills_map(&mut map, &maker_key, direction, fill).unwrap();
update_maker_fills_map(&mut map, &maker_key, direction, fill, false).unwrap();

assert_eq!(*map.get(&maker_key).unwrap(), -(fill as i64));
assert_eq!(map.get(&maker_key).unwrap().0, -(fill as i64));
assert_eq!(map.get(&maker_key).unwrap().1, false);

update_maker_fills_map(&mut map, &maker_key, direction, fill).unwrap();
update_maker_fills_map(&mut map, &maker_key, direction, fill, false).unwrap();

assert_eq!(*map.get(&maker_key).unwrap(), -2 * fill as i64);
assert_eq!(map.get(&maker_key).unwrap().0, -2 * fill as i64);
assert_eq!(map.get(&maker_key).unwrap().1, false);
}

#[test]
fn test_isolated_position_true() {
let mut map: BTreeMap<Pubkey, (i64, bool)> = BTreeMap::new();

let fill = 100;

// Single insert with isolated_position true
let maker_key = Pubkey::new_unique();
update_maker_fills_map(&mut map, &maker_key, PositionDirection::Long, fill, true).unwrap();
assert_eq!(map.get(&maker_key).unwrap().0, fill as i64);
assert_eq!(map.get(&maker_key).unwrap().1, true);

// Merge: same maker_key, two updates both with true
update_maker_fills_map(&mut map, &maker_key, PositionDirection::Long, fill, true).unwrap();
assert_eq!(map.get(&maker_key).unwrap().0, 2 * fill as i64);
assert_eq!(map.get(&maker_key).unwrap().1, true);

// Last write wins: first false, then true -> final .1 is true
let maker_key2 = Pubkey::new_unique();
update_maker_fills_map(&mut map, &maker_key2, PositionDirection::Short, fill, false)
.unwrap();
update_maker_fills_map(&mut map, &maker_key2, PositionDirection::Short, fill, true)
.unwrap();
assert_eq!(map.get(&maker_key2).unwrap().0, -2 * fill as i64);
assert_eq!(map.get(&maker_key2).unwrap().1, true);
}
}

Expand Down
Loading
Loading