To ensure our trade settlement latencies on-chain are minimal, we have opted for an owned object architecture i.e. when a trade takes place it only needs owned objects to fully execute on-chain. But since only the owner of the owned object can interact with an owned object on-chain, how can we allow users and Admin of the protocol ( holder of the Admin Cap object) to interact with this owned object? To overcome this problem, we create two data store objects on contract deployment Internal (more on this below) and External.

External Data Store (EDS)

The External data store is a publicly shared object with the following structure:

/// Represents the shared external data store
struct ExternalDataStore has key {
    /// ID of the data Storage object
    id: UID,
    /// The protocol version that storage supports
    version: u64,
    /// The id of the internal data store 
    internal_data_store: ID,
    /// Map of perpetuals
    perpetuals: Table<String, EDSPerpetual>,        
    /// Map of the operators (privileged accounts) that can perform certain privileged actions
    operators: Table<String, address>,
    /// The external/asset bank that stores user deposits
    asset_bank: AssetBank,
    /// An incremental counter to keep track of number of actions performed on shared objects
    /// such as deposit to AssetBanks, Updating perpetuals
    sequence_number: u128
 }

On genesis, an external data store object is created and shared publicly. Actions that are performed on External Data Store prior to their execution on Internal Data Store are listed below:

  1. Support Asset

    The assets supported as collateral or just for paying fees are created on the EDS by the admin of the protocol. When an asset is supported AssetSupportedEvent is emitted. An asset carries the following details on-chain:

        struct Asset has store, copy, drop {
            // symbol/name of the asset
            symbol: String,
            // The asset type `0x...package::module::struct`
            type: String,
            // the number of decimals the asset has
            decimals: u8,
            /// The discounted price percentage of the asset
            weight: u64,
            /// The current price of the asset
            price: u64,
            /// True if the asset can be used to open positions
            collateral: bool
        }
    
  2. Create Perpetual

    A new perpetual/market is created on chain by admin by invoking create_perpetual() method. It stores the new perpetual config on-chain and emits PerpetualUpdateEvent - Following perpetual details are stored on-chain:

    struct Perpetual has store, copy, drop {
            /// Unique address for this perpetual.
            id: address,
            /// Name of perpetual
            symbol: String,
            /// imr: the initial margin collateralization percentage
            imr: u64,
            /// mmr: the minimum collateralization percentage
            mmr: u64,
            /// the smallest decimal unit supported by asset for quantity
            step_size: u64,
            /// the smallest decimal unit supported by asset for price
            tick_size: u64,
            /// minimum quantity of asset that can be traded
            min_trade_qty: u64,
            /// maximum quantity of asset that can be traded
            max_trade_qty: u64,
            /// min price at which asset can be traded
            min_trade_price: u64,
            /// max price at which asset can be traded
            max_trade_price: u64,
            /// vector for maximum OI Open allowed for leverage. Indexes represent leverage
            max_notional_at_open: vector<u64>,
            ///  market take bound for long side ( 10% == 100000000000000000)
            mtb_long: u64,
            ///  market take bound for short side ( 10% == 100000000000000000)
            mtb_short: u64,
            /// default maker order fee 
            maker_fee: u64,
            /// default taker order fee 
            taker_fee: u64,
            /// max allowed funding rate
            max_funding_rate: u64,
            /// percentage of liquidation premium goes to insurance pool
            insurance_pool_ratio: u64,
            /// address of insurance pool
            insurance_pool: address,
            /// fee pool address
            fee_pool: address,
            /// is trading allowed
            trading_status: bool,
            /// trading start time
            trading_start_time: u64,
            /// delist status
            delist: bool,
            /// the price at which trades will be executed after delisting
            delisting_price: u64,
            /// is market only for isolated trades
            isolated_only: bool,
            /// Asset Information
            base_asset_symbol: String,
            base_asset_name: String,
            base_asset_decimals: u64,
            
            /// Order Limits
            max_limit_order_quantity: u64,
            max_market_order_quantity: u64,
            
            /// Default leverage for the market. This is not used on-chain and is only for off-chain
            default_leverage: u64,
    
            /// The oracle price of the perpetual
            oracle_price: u64,
            /// The current funding rate of the perp
            funding: FundingRate
        }
    
  3. Update Perpetual Config

    Each parameter of the perpetual/market such as step size, tick size, min/max trade price etc can be updated the Protocol’s admin. The admin can invoke the update_perpetual() method with the field/config of the perpetual that needs to be updated along with the new value. The new value of the perpetual is updated in the EDS and PerpetualUpdate event is emitted. The method includes not just the updated value but all the values/config for the perpetual. This to save ourself from writing separate event for each perpetual parameter and doing the same on chain-event-listener service (CEL) and off-chain margining engine. The off-chain will get the new perpetual update event and ask sequencer to make changes to the state of perpetual in IDS.

  4. Set Operator

    The admin can set these operators on-chain. All of these are stored in the EDS in operators table. The method OperatorUpdate event is emitted which will eventually be consumed by off-chain system and later on-chain to replicate the new operators on IDS.

  5. Deposit

    Deposit is the only action a user performs directly on-chain and it is performed on the EDS. The EDS carries an AssetBank object that stores all the supported assets and all the balances deposited by users.

    /// Represents the External bank that stores the margin/assets deposited by accounts
        /// The struct carries dynamic fields which are indexed using `asset_symbol` 
        /// that is used to store the balances of deposited coins
        struct AssetBank has key, store {
            id: UID,
            /// An incremental nonce to keep track of deposits made
            nonce: u128,
            /// Map of the assets currently supported by protocol indexed using asset name/symbol
            supported_assets: Table<String, SupportedAsset>,
            /// Map of all the pending deposits made to the bank that are yet not copied into the internal data store
            deposits: Table<u128, Deposit>
        }
    

    Any deposit being made is expected to be in the decimals of the input coin being deposited. Although the protocol only supports USDC token at the moment that has 6 decimals but we might be accepting SUI for collateral tomorrow that carries 9 decimals so the input amount to be deposited must be in 9 decimals. The protocol internally uses 9 decimals for all numeric representation so the input token amount is converted from its token decimals into 9 decimals before performing any operation on that amount.

    <aside> 💡

    For example the Min/Max deposit amount check. For USDC the min deposit amount is 1e9, meanwhile for SUI it might be 0.1e9 and since these min/max deposit quantities are always set in e9 format when an asset is being supported in support_asset call - we must convert all incoming deposited amounts into the e9 format before any comparison.

    </aside>

    The balance equal to coin_base_amount provided to deposit call is taken from the coin provided as input to the call and stored against a dynamic field ( The key is deposit.id). An entry of Deposit is created with the following struct

        /// Represents a deposit made to the asset bank
        /// The struct also stores a dynamic field by the name `coin` which stores the deposited coin
        /// The deposited coin stays inside this `Deposit` struct until the deposit is verified to be valid
        /// in which case the deposited coin moves to balance stored inside the Bank object as dynamic field
        /// or the deposit is tainted in which case the deposited coin is sent back to the depositor
        struct Deposit has key, store {
            // the id of the deposit
            id: UID,
            // The asset symbol that was deposited
            symbol: String,
            // The wallet that made the deposit
            from: address,
            // the account to which deposit was made
            account: address,
            // the amount deposited
            amount: u64,
        }
    

    and is stored against an ever increasing NONCE in bank.deposits table and the event AssetBankDeposit is emitted.

        struct AssetBankDeposit has copy, drop {
            eds_id: ID,
            // Name/symbol of the asset (USDC, SUI, etc.)
            asset: String,
            // the person who deposited the assets
            from: address,
            // the account to which assets were deposited
            to: address,
            // the deposited amount in e9
            amount: u64,
            nonce: u128,
            sequence_number: u128
        }
    

    Every deposit to asset bank event is consumed by an off-chain Anti Money Laundering Service (AML) and is verified to be legit before it is forward to off-chain Margining Engine (ME) for consumption. There are two possible scenarios: The AML flags the funds deposited to EDS as fraudulent/stolen or it finds the funds to be legit — in either case, it attaches an appropriate flag/status to the AssetBankDeposit event and forwards it to the off-chain Margining Engine (ME). Based on the flag that ME receives attached with the event, it either increases the user balance off-chain by the deposited amount or not.

    <aside> 💡

    If this AssetBankDeposit event is missed by CEL or there is a delay, the new balance of the user will never be known by the off-chain margining engine and this could cause the account to be liquidated first on off-chain and later on-chain as the off-chain never received the balance deposit event and never updated user balance in its memory and asked sequencer to update the account state on-chain.

    </aside>

For all the above actions, appropriate events are emitted. The chain-events-listener service is supposed to pick it up, run it through the off-chain margining engine which will emit an event that will be consumed by sequencer and the sequencer will perform the appropriate sync() method on-chain that will replicate the data from EDS to IDS.

<aside> 💡

Note: If the CEL misses any of the above event, the off-chain margining engine or the IDS will never know about that change.

</aside>

Internal Data Store (IDS)

The Internal data store ( or the IDS we refer it as ) is an owned object. An account called sequencer owns this object and only it can perform any actions (contract calls) on it. The IDS stores the user and perpetual states and all trades are executed against it. User assets are not deposited to the IDS, they are deposited to EDS and the IDS just comes to know about their quantity when off-chain margining engine requests sequencer to deposit user balance to IDS ( although the name of the function is deposit but in reality the assets always live in EDS and IDS) And the same is true for any updates to perpetual state. All actions performed on the IDS are verified and executed only if there is signature present for it or we have its record on-chain in EDS ( e.g. deposited balance of a user in EDS). Provided below is the structure of IDS

 /// Represents the internal data store of the protocol owned by the sequencer 
    struct InternalDataStore has key {
        /// ID of the data Storage object
        id: UID,
        /// The protocol version that storage supports
        version: u64,
        /// Current sequence hash updated after each tx execution
        sequence_hash: vector<u8>,
        /// Map of account address and their account balance
        accounts: Table<address, Account>,
        /// Map of perpetuals
        perpetuals: Table<String, Perpetual>,
        /// Map of the payload bytes executed till data. Used to prevent the replay of a tx
        hashes: Table<vector<u8>, u64>,
        /// Map of the orders filled quantities
        filled_orders: Table<vector<u8>, OrderFill>,
        /// Map of the assets currently supported by protocol indexed using asset name
        supported_assets: Table<String, Asset>,
        /// Map of special accounts that can perform certain actions on exchange
        operators: Table<String, address>,
        /// List of whitelisted wallets that can perform liquidations
        liquidators: vector<address>,
        /// the gas charges to be applied on traders
        gas_fee: u64,
        /// Incremental sequence number that increments with each action on IDS
        sequence_number: u128
    }

The sequencer (owner of the IDS) can perform the following actions on the IDS:

  1. Sync Perpetual

    As you might know by now by reading the docs above that only the admin of the protocol holding the AdminCap can create perpetuals. And these perpetuals are first created on the EDS by a direct contract call from the admin and then they are updated in our off-chain margining engine and only then the perpetuals are created or updated in the IDS. There is a single command for creating/updating perpetual in IDS called sync_perpetual - The method takes as input both the owned IDS and the shared EDS object and replicates the state of provided perpetual:string from EDS to IDS. If there is nothing to be replicated i.e. IDS perpetual is already in-sync with the EDS perpetual, the call reverts.