From 316c9b5d3c16e3a267e633bb1bac74a1af355ca2 Mon Sep 17 00:00:00 2001 From: ldback-aftermath Date: Thu, 26 Mar 2026 16:27:32 +0100 Subject: [PATCH 1/4] feat(af-iperps): add TWAP SDK types --- crates/af-iperps/src/errors.rs | 22 ++++ crates/af-iperps/src/event_instance.rs | 9 ++ crates/af-iperps/src/lib.rs | 136 +++++++++++++++++++++++++ 3 files changed, 167 insertions(+) diff --git a/crates/af-iperps/src/errors.rs b/crates/af-iperps/src/errors.rs index 7c3a8861..5e1329eb 100644 --- a/crates/af-iperps/src/errors.rs +++ b/crates/af-iperps/src/errors.rs @@ -87,6 +87,8 @@ module perpetuals::errors { const InvalidExpirationTimestamp: u64 = 23; /// Stop order gas cost provided is not enough const NotEnoughGasForStopOrder: u64 = 24; + /// TWAP order gas cost provided is not enough + const NotEnoughGasForTWAPOrder: u64 = 25; /// Invalid account trying to perform an action on a StopOrderTicket const InvalidAccountForStopOrder: u64 = 26; /// Invalid executor trying to execute the StopOrderTicket @@ -126,6 +128,26 @@ module perpetuals::errors { const NoOpenInterestToSocializeBadDebt: u64 = 40; /// Bad debt amount is greater than max allowed threshold const BadDebtAboveThreshold: u64 = 41; + /// TWAP order is past its allowed start or end execution timestamp + const TWAPOrderTicketExpired: u64 = 43; + /// Amount executed in one TWAP execution is outside the allowed range + const TWAPOrderAmountUncertaintyViolated: u64 = 44; + /// Current timestamp is too early for the next TWAP execution + const TWAPOrderExecutionGapViolated: u64 = 45; + /// The TWAP order has already been fully executed + const TWAPOrderFullyExecuted: u64 = 46; + /// TWAP order is being executed after the retry deadline has passed + const TWAPOrderExecutedAfterRetryTime: u64 = 47; + /// TWAP order is not in a terminal state required for finalization + const TWAPOrderCannotBeFinalized: u64 = 48; + /// Invalid account trying to perform an action on a TWAPOrderTicket + const TWAPOrderInvalidAccount: u64 = 49; + /// Invalid executor trying to perform an action on a TWAPOrderTicket + const TWAPOrderInvalidExecutor: u64 = 50; + /// TWAP order is being edited while it is being executed + const TWAPOrderCannotEditExecutingOrder: u64 = 51; + /// Invalid split between execution gas pool and finalization gas + const TWAPOrderInvalidGasSplit: u64 = 52; // Market --------------------------------------------------------------- diff --git a/crates/af-iperps/src/event_instance.rs b/crates/af-iperps/src/event_instance.rs index e9e5873c..239b3f7e 100644 --- a/crates/af-iperps/src/event_instance.rs +++ b/crates/af-iperps/src/event_instance.rs @@ -60,6 +60,7 @@ event_instance!(EventInstance { AddedIntegratorConfig, AllocatedCollateral, CanceledOrder, + CanceledTWAPOrderTicket, ClosedMarket, ClosedPositionAtSettlementPrices, CreatedAccount, @@ -71,24 +72,31 @@ event_instance!(EventInstance { CreatedPosition, CreatedPositionFeesProposal, CreatedStopOrderTicket, + CreatedTWAPOrderTicket, DeallocatedCollateral, DeletedMarginRatiosProposal, DeletedPositionFeesProposal, DeletedStopOrderTicket, + DeletedTWAPOrderTicket, DepositedCollateral, DonatedToInsuranceFund, EditedStopOrderTicketDetails, EditedStopOrderTicketExecutors, + EditedTWAPOrderTicketDetails, + EditedTWAPOrderTicketExecutors, ExecutedStopOrderTicket, + ExecutedTWAPOrderTicket, FilledMakerOrder, FilledMakerOrders, FilledTakerOrder, + FinalizedTWAPOrderTicket, LiquidatedPosition, PaidIntegratorFees, PausedMarket, PerformedADL, PerformedLiquidation, PostedOrder, + ProcessedTWAPOrderTicket, RegisteredCollateralInfo, RegisteredMarketInfo, RejectedPositionFeesProposal, @@ -126,6 +134,7 @@ event_instance!(EventInstance { UpdatedSpreadTwap, UpdatedSpreadTwapParameters, UpdatedStopOrderMistCost, + UpdatedTWAPOrderMistCost, WithdrewCollateral, WithdrewFees, WithdrewFromIntegratorVault, diff --git a/crates/af-iperps/src/lib.rs b/crates/af-iperps/src/lib.rs index 10fbc083..5298c017 100644 --- a/crates/af-iperps/src/lib.rs +++ b/crates/af-iperps/src/lib.rs @@ -24,6 +24,7 @@ pub mod stop_order_helpers; pub use self::market::{MarketParams, MarketState}; pub use self::orderbook::Order; pub use self::position::Position; +pub use self::twap_orders_details::TWAPOrderDetails; // Convenient aliases since these types will never exist onchain with a type argument other than an // OTW. @@ -35,6 +36,8 @@ pub type Account = self::account::Account; pub type AccountTypeTag = self::account::AccountTypeTag; pub type StopOrderTicket = self::stop_orders::StopOrderTicket; pub type StopOrderTicketTypetag = self::stop_orders::StopOrderTicketTypeTag; +pub type TWAPOrderTicket = self::twap_orders::TWAPOrderTicket; +pub type TWAPOrderTicketTypetag = self::twap_orders::TWAPOrderTicketTypeTag; pub type ClearingHouse = self::clearing_house::ClearingHouse; pub type ClearingHouseTypeTag = self::clearing_house::ClearingHouseTypeTag; pub type Vault = self::clearing_house::Vault; @@ -276,6 +279,77 @@ sui_pkg_sdk!(perpetuals { } } + module twap_orders_details { + /// The details to be hashed for the `encrypted_details` argument of + /// `create_twap_order_ticket`. + struct TWAPOrderDetails has drop { + clearing_house_id: ID, + /// Exclusive deadline for the first valid TWAP execution attempt. + start_expire_timestamp: Option, + /// Exclusive deadline for any TWAP execution attempt. + end_expire_timestamp: Option, + /// Expected time between two consecutive valid TWAP execution attempts. + execution_gap_ms: u64, + /// Maximum amount by which a valid attempt may happen earlier than + /// `execution_gap_ms`. + execution_time_uncertainty_ms: u64, + /// Target amount for one TWAP execution before uncertainty and remainder + /// adjustments. + one_execution_amount: u64, + /// Maximum additional delay after the nominal execution gap before the TWAP + /// becomes spoiled. + time_for_retry_ms: u64, + /// Maximum deviation allowed between the caller-requested amount and + /// `one_execution_amount`. + amount_uncertainty: u64, + /// Maximum allowed amount for one execution after backlog adjustments. + max_one_execution_amount: u64, + side: bool, + size: u64, + max_slippage_bps: u64, + reduce_only: bool, + salt: vector + } + } + + module twap_orders { + /// Object that allows off-chain executors to process a TWAP order in multiple + /// executions until it is finalized or canceled. + struct TWAPOrderTicket has key, store { + id: UID, + /// Addresses allowed to execute the order on behalf of the user. + executors: vector
, + /// Address that funded the ticket gas and must receive unearned execution gas + /// back. + refund_address: address, + /// Gas coin that must be provided by the user to cover the whole TWAP + /// lifecycle. + gas: Balance, + /// Portion of `gas` reserved for chunk executions. + execution_gas_budget: u64, + /// Portion of `execution_gas_budget` already paid out. + paid_execution_gas: u64, + /// Portion of `gas` reserved for delete/finalize. + finalization_gas: u64, + /// User account id. + account_id: u64, + /// Hash of the off-chain order details. See `TWAPOrderDetails`. + encrypted_details: vector, + + /// Amount of the order that has already been executed. + processed_amount: u64, + /// Amount of the TWAP target that has already been scheduled into + /// sub-orders. + scheduled_amount: u64, + /// Timestamp of the last valid execution attempt. + last_attempt_timestamp_ms: u64, + /// Timestamp anchoring spoilage checks. + retry_anchor_timestamp_ms: u64, + /// Timestamp of the last successful fill. + last_execution_timestamp_ms: u64, + } + } + module events { struct CreatedAccount has copy, drop { account_obj_id: ID, @@ -602,6 +676,63 @@ sui_pkg_sdk!(perpetuals { executors: vector
} + struct CreatedTWAPOrderTicket has copy, drop { + ticket_id: ID, + account_id: u64, + executors: vector
, + gas: u64, + encrypted_details: vector + } + + struct ProcessedTWAPOrderTicket has copy, drop { + ticket_id: ID, + account_id: u64, + execution_amount: u64, + filled_amount: u64, + remainder: u64, + processed_amount: u64, + last_execution_timestamp_ms: u64, + } + + struct FinalizedTWAPOrderTicket has copy, drop { + ticket_id: ID, + account_id: u64, + executor: address, + executed: bool, + deallocated_collateral: u64, + } + + struct CanceledTWAPOrderTicket has copy, drop { + ticket_id: ID, + account_id: u64, + sender: address, + deallocated_collateral: u64, + } + + struct ExecutedTWAPOrderTicket has copy, drop { + ticket_id: ID, + account_id: u64, + executor: address + } + + struct DeletedTWAPOrderTicket has copy, drop { + ticket_id: ID, + account_id: u64, + executor: address + } + + struct EditedTWAPOrderTicketDetails has copy, drop { + ticket_id: ID, + account_id: u64, + encrypted_details: vector + } + + struct EditedTWAPOrderTicketExecutors has copy, drop { + ticket_id: ID, + account_id: u64, + executors: vector
+ } + struct CreatedMarginRatiosProposal has copy, drop { ch_id: ID, margin_ratio_initial: IFixed, @@ -745,6 +876,10 @@ sui_pkg_sdk!(perpetuals { stop_order_mist_cost: u64 } + struct UpdatedTWAPOrderMistCost has copy, drop { + twap_order_mist_cost: u64 + } + struct DonatedToInsuranceFund has copy, drop { sender: address, ch_id: ID, @@ -1138,6 +1273,7 @@ sui_pkg_sdk!(perpetuals { /// Config that stores useful info for the protocol struct Config has store { stop_order_mist_cost: u64, + twap_order_mist_cost: u64, } } }); From 04a425c8a45bce03cb0e3ba27a68649f8cf8e077 Mon Sep 17 00:00:00 2001 From: ldback-aftermath Date: Fri, 27 Mar 2026 10:49:40 +0100 Subject: [PATCH 2/4] fix(af-iperps): remove mist_cost field for twap from --- crates/af-iperps/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/af-iperps/src/lib.rs b/crates/af-iperps/src/lib.rs index 5298c017..b94b5a0a 100644 --- a/crates/af-iperps/src/lib.rs +++ b/crates/af-iperps/src/lib.rs @@ -1273,7 +1273,6 @@ sui_pkg_sdk!(perpetuals { /// Config that stores useful info for the protocol struct Config has store { stop_order_mist_cost: u64, - twap_order_mist_cost: u64, } } }); From c024df478052769cd49b9ed6dd006424df0a0a45 Mon Sep 17 00:00:00 2001 From: ldback-aftermath Date: Fri, 27 Mar 2026 13:04:08 +0100 Subject: [PATCH 3/4] refactor(af-iperps): remove extra event --- crates/af-iperps/src/event_instance.rs | 1 - crates/af-iperps/src/lib.rs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/crates/af-iperps/src/event_instance.rs b/crates/af-iperps/src/event_instance.rs index 239b3f7e..bdcd3d0e 100644 --- a/crates/af-iperps/src/event_instance.rs +++ b/crates/af-iperps/src/event_instance.rs @@ -134,7 +134,6 @@ event_instance!(EventInstance { UpdatedSpreadTwap, UpdatedSpreadTwapParameters, UpdatedStopOrderMistCost, - UpdatedTWAPOrderMistCost, WithdrewCollateral, WithdrewFees, WithdrewFromIntegratorVault, diff --git a/crates/af-iperps/src/lib.rs b/crates/af-iperps/src/lib.rs index b94b5a0a..d36320e5 100644 --- a/crates/af-iperps/src/lib.rs +++ b/crates/af-iperps/src/lib.rs @@ -876,10 +876,6 @@ sui_pkg_sdk!(perpetuals { stop_order_mist_cost: u64 } - struct UpdatedTWAPOrderMistCost has copy, drop { - twap_order_mist_cost: u64 - } - struct DonatedToInsuranceFund has copy, drop { sender: address, ch_id: ID, From dd128ad97745eb7e622886404c482d1123330814 Mon Sep 17 00:00:00 2001 From: ldback-aftermath Date: Thu, 7 May 2026 15:58:04 +0200 Subject: [PATCH 4/4] fix(af-iperps): align TWAP SDK types with on-chain module --- crates/af-iperps/src/event_instance.rs | 1 - crates/af-iperps/src/lib.rs | 54 ++++++++---------- crates/af-iperps/src/twap_order_helpers.rs | 66 ++++++++++++++++++++++ 3 files changed, 89 insertions(+), 32 deletions(-) create mode 100644 crates/af-iperps/src/twap_order_helpers.rs diff --git a/crates/af-iperps/src/event_instance.rs b/crates/af-iperps/src/event_instance.rs index bdcd3d0e..99ef87c5 100644 --- a/crates/af-iperps/src/event_instance.rs +++ b/crates/af-iperps/src/event_instance.rs @@ -85,7 +85,6 @@ event_instance!(EventInstance { EditedTWAPOrderTicketDetails, EditedTWAPOrderTicketExecutors, ExecutedStopOrderTicket, - ExecutedTWAPOrderTicket, FilledMakerOrder, FilledMakerOrders, FilledTakerOrder, diff --git a/crates/af-iperps/src/lib.rs b/crates/af-iperps/src/lib.rs index d36320e5..ef66bc84 100644 --- a/crates/af-iperps/src/lib.rs +++ b/crates/af-iperps/src/lib.rs @@ -20,11 +20,13 @@ pub mod order_helpers; pub mod order_id; #[cfg(feature = "stop-orders")] pub mod stop_order_helpers; +#[cfg(feature = "stop-orders")] +pub mod twap_order_helpers; pub use self::market::{MarketParams, MarketState}; pub use self::orderbook::Order; pub use self::position::Position; -pub use self::twap_orders_details::TWAPOrderDetails; +pub use self::twap_orders::TWAPOrderDetails; // Convenient aliases since these types will never exist onchain with a type argument other than an // OTW. @@ -279,63 +281,57 @@ sui_pkg_sdk!(perpetuals { } } - module twap_orders_details { + module twap_orders { /// The details to be hashed for the `encrypted_details` argument of /// `create_twap_order_ticket`. struct TWAPOrderDetails has drop { - clearing_house_id: ID, /// Exclusive deadline for the first valid TWAP execution attempt. - start_expire_timestamp: Option, + first_run_expire_timestamp: Option, /// Exclusive deadline for any TWAP execution attempt. - end_expire_timestamp: Option, + expire_timestamp: Option, /// Expected time between two consecutive valid TWAP execution attempts. execution_gap_ms: u64, /// Maximum amount by which a valid attempt may happen earlier than /// `execution_gap_ms`. execution_time_uncertainty_ms: u64, - /// Target amount for one TWAP execution before uncertainty and remainder - /// adjustments. - one_execution_amount: u64, + /// Amount of chunks the TWAP order is split into. + chunks_amount: u64, + /// Maximum size of the final fresh tail, expressed in basis points of + /// one execution amount, that may be merged into the previous chunk. + small_tail_merge_threshold_bps: u64, /// Maximum additional delay after the nominal execution gap before the TWAP /// becomes spoiled. time_for_retry_ms: u64, /// Maximum deviation allowed between the caller-requested amount and - /// `one_execution_amount`. - amount_uncertainty: u64, - /// Maximum allowed amount for one execution after backlog adjustments. - max_one_execution_amount: u64, + /// one execution amount, expressed in basis points. + amount_uncertainty_bps: u64, + /// Maximum allowed amount for one execution after backlog adjustments, + /// expressed in basis points of the total order size. + max_one_execution_amount_bps: u64, side: bool, size: u64, max_slippage_bps: u64, reduce_only: bool, salt: vector } - } - module twap_orders { /// Object that allows off-chain executors to process a TWAP order in multiple /// executions until it is finalized or canceled. struct TWAPOrderTicket has key, store { id: UID, + /// Clearing house for which the TWAP order is placed. + clearing_house_id: ID, /// Addresses allowed to execute the order on behalf of the user. executors: vector
, - /// Address that funded the ticket gas and must receive unearned execution gas - /// back. - refund_address: address, /// Gas coin that must be provided by the user to cover the whole TWAP /// lifecycle. gas: Balance, - /// Portion of `gas` reserved for chunk executions. - execution_gas_budget: u64, - /// Portion of `execution_gas_budget` already paid out. - paid_execution_gas: u64, - /// Portion of `gas` reserved for delete/finalize. - finalization_gas: u64, + /// Total gas budget for the entire TWAP execution. + gas_execution_budget: u64, /// User account id. account_id: u64, /// Hash of the off-chain order details. See `TWAPOrderDetails`. encrypted_details: vector, - /// Amount of the order that has already been executed. processed_amount: u64, /// Amount of the TWAP target that has already been scheduled into @@ -347,6 +343,8 @@ sui_pkg_sdk!(perpetuals { retry_anchor_timestamp_ms: u64, /// Timestamp of the last successful fill. last_execution_timestamp_ms: u64, + /// Portion of `gas_execution_budget` already paid out. + paid_execution_gas: u64, } } @@ -698,7 +696,6 @@ sui_pkg_sdk!(perpetuals { ticket_id: ID, account_id: u64, executor: address, - executed: bool, deallocated_collateral: u64, } @@ -707,12 +704,7 @@ sui_pkg_sdk!(perpetuals { account_id: u64, sender: address, deallocated_collateral: u64, - } - - struct ExecutedTWAPOrderTicket has copy, drop { - ticket_id: ID, - account_id: u64, - executor: address + partial_fill: bool, } struct DeletedTWAPOrderTicket has copy, drop { diff --git a/crates/af-iperps/src/twap_order_helpers.rs b/crates/af-iperps/src/twap_order_helpers.rs new file mode 100644 index 00000000..01d2059f --- /dev/null +++ b/crates/af-iperps/src/twap_order_helpers.rs @@ -0,0 +1,66 @@ +//! Helpers for TWAP orders. + +use fastcrypto::hash::{Blake2b256, HashFunction}; +use serde::Serialize; + +pub trait TWAPOrderTicketDetails { + /// Pure transaction input to use when calling `create_twap_order_ticket`. + fn encrypted_details(&self) -> Result, sui_sdk_types::bcs::Error> + where + Self: Serialize, + { + Ok(Blake2b256::digest(sui_sdk_types::bcs::ToBcs::to_bcs(&self)?).to_vec()) + } +} + +/// The details to be hashed for the `encrypted_details` argument of +/// `create_twap_order_ticket`. +#[derive(Debug, Serialize)] +pub struct TWAPDetails { + pub first_run_expire_timestamp: Option, + pub expire_timestamp: Option, + pub execution_gap_ms: u64, + pub execution_time_uncertainty_ms: u64, + pub chunks_amount: u64, + pub small_tail_merge_threshold_bps: u64, + pub time_for_retry_ms: u64, + pub amount_uncertainty_bps: u64, + pub max_one_execution_amount_bps: u64, + pub side: bool, + pub size: u64, + pub max_slippage_bps: u64, + pub reduce_only: bool, + pub salt: Vec, +} + +impl TWAPOrderTicketDetails for TWAPDetails {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypted_details_hashes_bcs_payload_including_salt() { + let details = TWAPDetails { + first_run_expire_timestamp: Some(1), + expire_timestamp: Some(2), + execution_gap_ms: 3, + execution_time_uncertainty_ms: 4, + chunks_amount: 5, + small_tail_merge_threshold_bps: 6, + time_for_retry_ms: 7, + amount_uncertainty_bps: 8, + max_one_execution_amount_bps: 9, + side: true, + size: 10, + max_slippage_bps: 11, + reduce_only: false, + salt: vec![12; 32], + }; + + let expected = Blake2b256::digest(sui_sdk_types::bcs::ToBcs::to_bcs(&details).unwrap()); + let actual = details.encrypted_details().unwrap(); + + assert_eq!(actual, expected.to_vec()); + } +}