Skip to main content
The Stock Orderbook suite consists of a single deployable contract (StockCustody) with external dependencies on StockToken, USDC, a LiquidationRouter, and optionally a lending market. This page covers the contract’s architecture, state layout, and complete function reference.

Architecture

StockCustody maintains an internal ledger of user balances using a keccak256(user, token) key scheme. Deposits move tokens from the user into the contract and credit the internal balance. Withdrawals debit the internal balance and transfer tokens out. Trade settlements move balances between users internally without any on-chain token transfer. For compliance-checked tokens (security tokens), the contract calls ISecurityToken.canTransfer on deposit and withdrawal. Internal transfers during trade settlement skip this check entirely - compliance validation is the matching engine’s responsibility.
                      +------------------+
                      |   StockCustody   |
                      |  (this contract) |
                      +--------+---------+
                               |
          +--------------------+--------------------+
          |                    |                    |
+---------v--------+  +-------v--------+  +--------v---------+
|   StockToken     |  |     USDC       |  | LiquidationRouter|
| (ISecurityToken) |  |    (IERC20)    |  |   (external)     |
+--------+---------+  +----------------+  +--------+---------+
         |                                         |
+--------v---------+                     +---------v---------+
| IdentityRegistry |                     |   LendingMarket   |
|   (indirect)     |                     | (ILendingMarket)  |
+------------------+                     +-------------------+
The IdentityRegistry is never called directly by StockCustody. It is referenced indirectly through StockToken’s canTransfer validation during deposit and withdrawal of compliance-checked tokens.

StockCustody

UUPS-upgradeable custody contract with internal ledger, trade settlement, liquidation handling, and emergency controls.

Initialization

function initialize(
    address _admin,              // Contract administrator (cannot be zero)
    address _operator,           // Settlement operator (zero defaults to admin)
    address _liquidationRouter,  // Liquidation router address (cannot be zero)
    address _usdc                // USDC token address (cannot be zero)
) external initializer
The initializer sets up the admin, operator, liquidation router, and USDC address. If _operator is address(0), it defaults to the admin address.

State Variables

VariableTypeDescription
adminaddressContract administrator
pendingAdminaddressPending admin during two-step transfer
settlementOperatoraddressOperator authorized for settlements
liquidationRouteraddressExternal router for liquidation flows
usdcaddressUSDC settlement token address
withdrawalsPausedboolGlobal withdrawal pause flag
pendingLiquidationCountuint256Count of unsettled liquidations
_balancesmapping(bytes32 => uint256)Total balance per user-token pair
_lockedBalancesmapping(bytes32 => uint256)Locked balance per user-token pair
_processedSettlementsmapping(bytes32 => bool)Deduplication for settlement IDs
_supportedTokensmapping(address => bool)Registered token whitelist
_complianceCheckedmapping(address => bool)Tokens requiring canTransfer validation
_frozenUsersmapping(address => bool)Per-user withdrawal freeze
liquidationsmapping(uint256 => LiquidationInfo)Liquidation records

Access Control

Admin Management

Admin transfer is a two-step process to prevent accidental transfer to an incorrect address.
function transferAdmin(address newAdmin) external onlyAdmin
function acceptAdmin() external  // only callable by pendingAdmin
function updateOperator(address newOperator) external onlyAdmin

Token Configuration

function addSupportedToken(address token, bool complianceChecked) external onlyAdmin
function removeSupportedToken(address token) external onlyAdmin
The complianceChecked flag determines whether the token requires ISecurityToken.canTransfer validation on deposit and withdrawal. Set to true for security tokens, false for USDC and other standard ERC-20s. Removing a token from the supported list does not lock user funds. Users can still withdraw unsupported tokens. New deposits and trade settlements for the removed token are blocked.

Deposit and Withdrawal

function deposit(address token, uint256 amount) external nonReentrant tokenSupported(token)
Transfers tokens from the caller into custody. For compliance-checked tokens, validates canTransfer(caller, custody, amount) before transfer. The caller must have approved the custody contract for the deposit amount.
function withdraw(address token, uint256 amount) external nonReentrant
Withdraws available (unlocked) tokens from custody. Blocked if the user is frozen or withdrawals are globally paused. For compliance-checked tokens, validates canTransfer(custody, caller, amount). Allows withdrawal of unsupported tokens to prevent fund lockout after token removal.
function checkTransferability(
    address token, address from, address to, uint256 amount
) public view returns (bool canDo, uint8 code, string memory reason)
Pre-checks whether a compliance-checked token transfer would succeed. Returns success for non-compliance-checked tokens. Use this off-chain before deposit or withdrawal to avoid wasted gas.

Balance Locking

The operator locks user balances when orders are matched, before settlement.
function lockBalance(address user, address token, uint256 amount, bytes32 lockId)
    external onlyOperator tokenSupported(token)
Locks amount from the user’s available balance (total minus already locked). Reverts with InsufficientBalance if available balance is insufficient.
function unlockBalance(address user, address token, uint256 amount, bytes32 lockId)
    external onlyOperator tokenSupported(token)
Unlocks previously locked tokens. Reverts with InsufficientLockedBalance if the locked balance is less than amount.

Trade Settlement

function settleTrade(
    bytes32 settlementId,
    address maker, address taker,
    address baseToken, address quoteToken,
    uint256 baseAmount, uint256 quoteAmount,
    bool makerIsBuyer
) external onlyOperator nonReentrant
Atomically transfers locked base tokens from seller to buyer and locked quote tokens from buyer to seller. Both parties must have sufficient locked balances. The settlementId must be unique - reuse reverts with SettlementAlreadyProcessed.
function batchSettleTrades(
    bytes32 batchId,
    bytes32[] calldata settlementIds,
    address[] calldata makers,
    address[] calldata takers,
    address[] calldata baseTokens,
    address[] calldata quoteTokens,
    uint256[] calldata baseAmounts,
    uint256[] calldata quoteAmounts,
    bool[] calldata makerIsBuyers
) external onlyOperator nonReentrant
Settles multiple trades in one transaction. Skips already-processed settlement IDs. Includes a gas guard that stops processing if remaining gas drops below 80,000 per iteration. Emits BatchSettled(batchId, settledCount, totalSubmitted) so callers can detect partial completion.
The batchSettleTrades function takes 9 calldata array parameters, which exceeds the legacy compilation pipeline’s stack limit. The Solidity compiler must use viaIR: true. See Integration for compiler configuration.
function settlePriorityTrade(
    bytes32 settlementId,
    address liquidatedUser, address buyer,
    address baseToken, address quoteToken,
    uint256 baseAmount, uint256 quoteAmount
) external onlyOperator nonReentrant
Priority settlement for liquidated positions. The liquidated user’s tokens must be locked first. Use freezeUser() before locking to prevent withdrawal front-running between the lock and settlement steps. Emits both PrioritySettlement and TradeSettled.

Liquidation

Receiving Collateral

function receiveLiquidatedCollateral(
    address token, uint256 amount, uint256 debtOwed,
    address borrower, address lendingMarket, uint256 liquidationId
) external onlyLiquidationRouter nonReentrant
Called by the liquidation router to deliver seized collateral into custody. Tokens are pulled from the router (which must have approved the custody contract) and credited to an internal liquidation pool keyed to address(this).

External Sale Settlement

function settleLiquidation(
    uint256 liquidationId, uint256 usdcReceived, address tokenRecipient
) external onlyOperator nonReentrant
Settles after the operator sells tokens through an external venue. Stock tokens transfer to tokenRecipient. USDC is pulled from the operator and sent to the liquidation router. The lending market receives a receiveLiquidationProceeds callback. If the callback reverts, settlement still completes - off-chain systems must reconcile via events.

Internal Buyer Settlement

function settleLiquidationWithBuyer(
    uint256 liquidationId, address buyer,
    uint256 tokenAmount, uint256 usdcAmount
) external onlyOperator nonReentrant
Matches liquidation tokens with a buyer who has USDC in custody. Tokens move from the liquidation pool to the buyer’s custody balance. USDC is deducted from the buyer’s internal balance and transferred on-chain to the router. Supports partial fills - the liquidation remains open until the full amount is matched. The lending market is notified via callback.

Emergency Controls

function freezeUser(address user) external onlyOperator
function unfreezeUser(address user) external onlyOperator
Per-user withdrawal freeze. Used to prevent front-running during liquidation or for compliance holds. Does not affect the user’s ability to have their balances settled by the operator.
function pauseWithdrawals() external onlyAdmin
function unpauseWithdrawals() external onlyAdmin
Global withdrawal circuit breaker. Blocks all withdrawals for all users.

Stock Split Reconciliation

function reconcileAfterSplit(
    address token, address[] calldata users,
    uint256 numerator, uint256 denominator
) external onlyAdmin
Adjusts internal balances after a stock token split. Both total balances and locked balances are multiplied by numerator / denominator using floor division. The liquidation pool balance for the token is also adjusted. Dust from rounding stays unallocated in the contract’s ERC-20 balance. Only forward splits are accepted (numerator must exceed denominator).

View Functions

FunctionReturns
getBalance(user, token)Total balance (locked + available)
getLockedBalance(user, token)Locked balance
getAvailableBalance(user, token)Available balance (total minus locked)
isSettlementProcessed(settlementId)Whether a settlement ID has been used
isTokenSupported(token)Whether a token is registered
isComplianceChecked(token)Whether a token requires compliance validation
isUserFrozen(user)Whether a user’s withdrawals are frozen
getLiquidation(liquidationId)Full liquidation record (token, amount, debtOwed, borrower, lendingMarket, timestamp, settled)

Events

EventParametersWhen Emitted
Depositeduser, token, amount, newBalanceUser deposits tokens
Withdrawnuser, token, amountUser withdraws tokens
BalanceLockeduser, token, amount, lockIdOperator locks balance
BalanceUnlockeduser, token, amount, lockIdOperator unlocks balance
TradeSettledsettlementId, maker, taker, baseToken, quoteToken, baseAmount, quoteAmountTrade settlement completes
BatchSettledbatchId, settledCount, totalSubmittedBatch settlement completes
PrioritySettlementsettlementId, liquidatedUser, tokenPriority liquidation trade settled
LiquidationReceivedliquidationId, token, amount, debtOwed, borrower, lendingMarketCollateral received from router
LiquidationSettledliquidationId, usdcReceived, tokenRecipientExternal liquidation sale settled
LiquidationSoldToBuyerliquidationId, buyer, tokenAmount, usdcAmount, fullyFilledInternal buyer fills liquidation
OperatorUpdatedoldOperator, newOperatorOperator address changed
TokenSupportedtoken, supported, complianceCheckedToken added or removed
LiquidationRouterUpdatedoldRouter, newRouterRouter address changed
UsdcAddressUpdatedoldUsdc, newUsdcUSDC address changed
AdminTransferInitiatedcurrentAdmin, pendingAdminAdmin transfer started
AdminTransferCompletedoldAdmin, newAdminAdmin transfer accepted
UserFrozenuser, operatorUser withdrawals frozen
UserUnfrozenuser, operatorUser withdrawals unfrozen
WithdrawalsPausedoperatorGlobal withdrawal pause
WithdrawalsUnpausedoperatorGlobal withdrawal unpause
BalanceReconcileduser, token, oldBalance, newBalanceBalance adjusted after stock split

Errors

ErrorTrigger
UnauthorizedCaller lacks required role
InsufficientBalanceAvailable balance less than requested amount
InsufficientLockedBalanceLocked balance less than transfer/unlock amount
ZeroAmountAmount parameter is 0
ZeroAddressAddress parameter is address(0)
SettlementAlreadyProcessedSettlement ID reused
InvalidSettlementBatch array lengths mismatch or empty
TransferFailedERC-20 transfer returned false
TokenNotSupportedToken not registered via addSupportedToken
TransferRestricted(code, reason)Security token canTransfer rejected
LiquidationNotFoundLiquidation ID does not exist
LiquidationAlreadySettledLiquidation already fully settled
LiquidationAmountExceededRequested token amount exceeds remaining liquidation amount
BuyerInsufficientUsdcBuyer’s USDC custody balance too low
UserIsFrozenFrozen user attempts withdrawal
WithdrawalsArePausedWithdrawal attempted during global pause
InvalidBatchSizeBatch settlement called with empty array
AdminTransferNotPendingacceptAdmin called by non-pending address

ILendingMarket

Callback interface that lending market contracts must implement to receive liquidation settlement notifications.
interface ILendingMarket {
    function receiveLiquidationProceeds(uint256 liquidationId, uint256 amount) external;
}
Called by StockCustody when a liquidation is settled (both settleLiquidation and settleLiquidationWithBuyer). If the callback reverts, settlement still completes. Off-chain systems must reconcile via LiquidationSettled or LiquidationSoldToBuyer events.