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..99ef87c5 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,30 @@ event_instance!(EventInstance { CreatedPosition, CreatedPositionFeesProposal, CreatedStopOrderTicket, + CreatedTWAPOrderTicket, DeallocatedCollateral, DeletedMarginRatiosProposal, DeletedPositionFeesProposal, DeletedStopOrderTicket, + DeletedTWAPOrderTicket, DepositedCollateral, DonatedToInsuranceFund, EditedStopOrderTicketDetails, EditedStopOrderTicketExecutors, + EditedTWAPOrderTicketDetails, + EditedTWAPOrderTicketExecutors, ExecutedStopOrderTicket, FilledMakerOrder, FilledMakerOrders, FilledTakerOrder, + FinalizedTWAPOrderTicket, LiquidatedPosition, PaidIntegratorFees, PausedMarket, PerformedADL, PerformedLiquidation, PostedOrder, + ProcessedTWAPOrderTicket, RegisteredCollateralInfo, RegisteredMarketInfo, RejectedPositionFeesProposal, diff --git a/crates/af-iperps/src/lib.rs b/crates/af-iperps/src/lib.rs index 10fbc083..ef66bc84 100644 --- a/crates/af-iperps/src/lib.rs +++ b/crates/af-iperps/src/lib.rs @@ -20,10 +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::TWAPOrderDetails; // Convenient aliases since these types will never exist onchain with a type argument other than an // OTW. @@ -35,6 +38,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 +281,73 @@ sui_pkg_sdk!(perpetuals { } } + module twap_orders { + /// The details to be hashed for the `encrypted_details` argument of + /// `create_twap_order_ticket`. + struct TWAPOrderDetails has drop { + /// Exclusive deadline for the first valid TWAP execution attempt. + first_run_expire_timestamp: Option, + /// Exclusive deadline for any TWAP execution attempt. + 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, + /// 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, 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 + } + + /// 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
, + /// Gas coin that must be provided by the user to cover the whole TWAP + /// lifecycle. + gas: Balance, + /// 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 + /// 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, + /// Portion of `gas_execution_budget` already paid out. + paid_execution_gas: u64, + } + } + module events { struct CreatedAccount has copy, drop { account_obj_id: ID, @@ -602,6 +674,57 @@ 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, + deallocated_collateral: u64, + } + + struct CanceledTWAPOrderTicket has copy, drop { + ticket_id: ID, + account_id: u64, + sender: address, + deallocated_collateral: u64, + partial_fill: bool, + } + + 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, 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()); + } +}