Skip to content

feat: enable trigger orders on Swift signed message flow#2171

Open
ChesterSim wants to merge 3 commits into
masterfrom
chester/be-254-enable-trigger-orders-on-swift
Open

feat: enable trigger orders on Swift signed message flow#2171
ChesterSim wants to merge 3 commits into
masterfrom
chester/be-254-enable-trigger-orders-on-swift

Conversation

@ChesterSim
Copy link
Copy Markdown
Contributor

@ChesterSim ChesterSim commented Mar 31, 2026

Summary

  • Enable OrderType::TriggerMarket and OrderType::TriggerLimit to be placed on-chain via the Swift signed message flow (placeSignedMsgTakerOrder)
  • Trigger orders are signed by the taker, sent to the Swift server, picked up by keeper bots, and placed on-chain without fill attempt — they sit until triggered
  • No SDK changes needed — existing signSignedMsgOrderParamsMessage and placeSignedMsgTakerOrder work with trigger order params as-is

Changes

Program (order_params.rs):

  • Extend has_valid_auction_params to accept trigger order types with no auction params (using existing is_trigger_order() helper)

Program (keeper.rs):

  • Add trigger_price validation for trigger orders in place_signed_msg_taker_order
  • Fix max_slot calculation to use unwrap_or(0) for trigger orders (prevents panic on missing auction_duration)

Tests:

  • Bankrun test: TriggerMarket order placed successfully via placeSignedMsgTakerOrder
  • Bankrun test: TriggerLimit order placed successfully via placeSignedMsgTakerOrder
  • Bankrun test: Trigger order without trigger_price is rejected with InvalidSignedMsgOrderParam

Test plan

  • All 17 bankrun tests pass (14 existing + 3 new)
  • Program compiles with cargo check
  • Verify on devnet with actual Swift server integration

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Trigger market and trigger limit orders supported in signed-message flows.
  • Bug Fixes

    • Signed-message trigger orders now require a trigger price.
    • Auction parameter handling for trigger orders updated to align with limit-order behavior.
  • Tests

    • Added tests covering trigger-market and trigger-limit happy paths and a trigger-market rejection case.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 15ecee67-763c-44b0-8791-f1b2a170e95d

📥 Commits

Reviewing files that changed from the base of the PR and between ebdafdc and b998dcd.

📒 Files selected for processing (1)
  • tests/placeAndMakeSignedMsgBankrun.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/placeAndMakeSignedMsgBankrun.ts

Walkthrough

Require trigger_price for trigger orders in place_signed_msg_taker_order; treat trigger orders like OrderType::Limit when computing max_slot (use auction_duration.unwrap_or(0)); extend has_valid_auction_params to accept all-auction-fields-none for trigger orders; add tests for trigger-market/trigger-limit happy paths and a missing-trigger_price rejection.

Changes

Cohort / File(s) Summary
Keeper + Order validation
programs/drift/src/instructions/keeper.rs, programs/drift/src/state/order_params.rs
Enforce that trigger orders include trigger_price; treat trigger orders like limit orders for max_slot by using auction_duration.unwrap_or(0); allow the "all auction fields are None" case for trigger orders in has_valid_auction_params.
Tests — trigger order flows
tests/placeAndMakeSignedMsgBankrun.ts
Add trigger-market and trigger-limit happy-path tests (verify open orders, correct orderType via isVariant, and SignedMsgOrderRecord event) and a trigger-market rejection test for missing triggerPrice (assert error contains 0x1890).
Tests — formatting
sdk/tests/user/test.ts
Cosmetic formatting changes: collapse multi-line BN assignments and getLeverageComponents calls into single-line expressions (no logic changes).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniffed the fields both near and far,
A trigger price now must be where you are,
Auctions and limits hop into the same frame,
Tests jump in to prove each honored name,
A cheerful rabbit pats the guarded market.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: enabling trigger orders within the Swift signed message flow for placing orders on-chain.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

…ned message flow

- Extend has_valid_auction_params to accept trigger order types with no auction params
- Add trigger_price validation in place_signed_msg_taker_order
- Fix max_slot calculation to use unwrap_or(0) for trigger orders
- Use existing is_trigger_order() helper for DRY enum checks
- Add bankrun tests for TriggerMarket, TriggerLimit, and rejection without trigger_price

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ChesterSim ChesterSim force-pushed the chester/be-254-enable-trigger-orders-on-swift branch from c4242ff to b9286a0 Compare March 31, 2026 08:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
programs/drift/src/instructions/keeper.rs (1)

836-843: ⚠️ Potential issue | 🟠 Major

Trigger orders without auction_duration silently fail to place due to zero TTL.

For trigger orders without auction parameters, unwrap_or(0) at line 842 sets max_slot == order_slot, creating zero time-to-live. At line 855, when max_slot < clock.slot (inevitable with async placement delays), the keeper silently returns Ok(()) without placing the order. This makes trigger order placement fragile across async flows like Swift→keeper delays.

💡 Proposed fix (add default TTL for trigger orders when auction duration is absent)
-    let max_slot = if matching_taker_order_params.order_type == OrderType::Limit
-        || matching_taker_order_params.is_trigger_order()
-    {
-        order_slot.safe_add(
-            matching_taker_order_params
-                .auction_duration
-                .unwrap_or(0)
-                .cast::<u64>()?,
-        )?
+    let max_slot = if matching_taker_order_params.order_type == OrderType::Limit
+        || matching_taker_order_params.is_trigger_order()
+    {
+        let ttl_slots: u64 = if matching_taker_order_params.is_trigger_order() {
+            matching_taker_order_params
+                .auction_duration
+                .unwrap_or(500)
+                .cast::<u64>()?
+        } else {
+            matching_taker_order_params
+                .auction_duration
+                .unwrap_or(0)
+                .cast::<u64>()?
+        };
+        order_slot.safe_add(ttl_slots)?
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@programs/drift/src/instructions/keeper.rs` around lines 836 - 843, The code
sets max_slot using matching_taker_order_params.auction_duration.unwrap_or(0),
which makes trigger orders have zero TTL and silently fail; change the logic in
the max_slot calculation (around matching_taker_order_params, order_slot, and
auction_duration) to supply a sensible default TTL when
matching_taker_order_params.is_trigger_order() is true and auction_duration is
None (e.g., use a DEFAULT_TRIGGER_TTL constant or a small nonzero slot delta) so
max_slot > order_slot and the keeper does not immediately return Ok(()) for
delayed placements.
🧹 Nitpick comments (1)
tests/placeAndMakeSignedMsgBankrun.ts (1)

1866-1943: Strengthen the rejection test with a state assertion.

After the expected failure, add an explicit check that no open order was created (Line 1935 catch path currently only checks error text).

✅ Suggested assertion addition
 		} catch (e) {
 			// Expected to fail with InvalidSignedMsgOrderParam
 			assert(e.toString().includes('0x1890') || e.toString().includes('InvalidSignedMsgOrderParam'),
 				`Expected InvalidSignedMsgOrderParam error, got: ${e.toString()}`);
 		}
+		await takerDriftClientUser.fetchAccounts();
+		assert(
+			takerDriftClient.getUser().getOpenOrders().length === 0,
+			'No trigger order should be placed when triggerPrice is missing'
+		);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/placeAndMakeSignedMsgBankrun.ts` around lines 1866 - 1943, The test
currently only asserts the error text when placeSignedMsgTakerOrder fails; after
the catch, fetch the taker state and assert no open order was created: call
takerDriftClient.fetchAccounts() and inspect takerDriftClient.getUserAccount()
(or getUserAccountPublicKey()) to ensure there is no order matching the test's
uuid/marketIndex (or that openOrders/orders length is unchanged/zero). Place
this assertion inside the catch path before unsubscribing to guarantee the
failed signed order did not produce any on‑chain open order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@programs/drift/src/instructions/keeper.rs`:
- Around line 836-843: The code sets max_slot using
matching_taker_order_params.auction_duration.unwrap_or(0), which makes trigger
orders have zero TTL and silently fail; change the logic in the max_slot
calculation (around matching_taker_order_params, order_slot, and
auction_duration) to supply a sensible default TTL when
matching_taker_order_params.is_trigger_order() is true and auction_duration is
None (e.g., use a DEFAULT_TRIGGER_TTL constant or a small nonzero slot delta) so
max_slot > order_slot and the keeper does not immediately return Ok(()) for
delayed placements.

---

Nitpick comments:
In `@tests/placeAndMakeSignedMsgBankrun.ts`:
- Around line 1866-1943: The test currently only asserts the error text when
placeSignedMsgTakerOrder fails; after the catch, fetch the taker state and
assert no open order was created: call takerDriftClient.fetchAccounts() and
inspect takerDriftClient.getUserAccount() (or getUserAccountPublicKey()) to
ensure there is no order matching the test's uuid/marketIndex (or that
openOrders/orders length is unchanged/zero). Place this assertion inside the
catch path before unsubscribing to guarantee the failed signed order did not
produce any on‑chain open order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8b36c7c1-e504-4cc3-a6b7-3d8983bd892e

📥 Commits

Reviewing files that changed from the base of the PR and between a75152b and c4242ff.

📒 Files selected for processing (3)
  • programs/drift/src/instructions/keeper.rs
  • programs/drift/src/state/order_params.rs
  • tests/placeAndMakeSignedMsgBankrun.ts

@ChesterSim
Copy link
Copy Markdown
Contributor Author

Code review

Found 1 issue:

  1. Trigger orders are silently dropped in production due to zero-duration max_slot (bug in max_slot calculation for trigger orders)

has_valid_auction_params() enforces that trigger orders must have auction_duration = None. As a result, in the max_slot calculation, unwrap_or(0) always resolves to 0, making max_slot = order_slot + 0 = order_slot. The subsequent guard then silently drops every trigger order placed by a keeper:

let max_slot = if matching_taker_order_params.order_type == OrderType::Limit
|| matching_taker_order_params.is_trigger_order()
{
order_slot.safe_add(
matching_taker_order_params
.auction_duration
.unwrap_or(0)
.cast::<u64>()?,
)?
} else {
order_slot.safe_add(
matching_taker_order_params
.auction_duration
.unwrap()
.cast::<u64>()?,
)?
};
// Dont place order if max slot already passed
if max_slot < clock.slot {
msg!(
"SignedMsg order max_slot {} < current slot {}",
max_slot,
clock.slot
);
return Ok(());
}

In production, the keeper reads the signed message and submits a transaction in a later slot, so clock.slot > order_slot and max_slot < clock.slot is always true — the order is discarded with Ok(()) before it is ever placed. The bankrun tests pass because all operations execute within the same slot (clock.slot == order_slot), so the < check is false and the order goes through.

A fix would be to use a separate expiry window for trigger orders (e.g., a fixed offset such as the existing 500-slot staleness threshold) rather than deriving max_slot from auction_duration, which is structurally None for all valid trigger orders.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant