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.
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
_operator is address(0), it defaults to the admin address.
State Variables
| Variable | Type | Description |
|---|---|---|
admin | address | Contract administrator |
pendingAdmin | address | Pending admin during two-step transfer |
settlementOperator | address | Operator authorized for settlements |
liquidationRouter | address | External router for liquidation flows |
usdc | address | USDC settlement token address |
withdrawalsPaused | bool | Global withdrawal pause flag |
pendingLiquidationCount | uint256 | Count of unsettled liquidations |
_balances | mapping(bytes32 => uint256) | Total balance per user-token pair |
_lockedBalances | mapping(bytes32 => uint256) | Locked balance per user-token pair |
_processedSettlements | mapping(bytes32 => bool) | Deduplication for settlement IDs |
_supportedTokens | mapping(address => bool) | Registered token whitelist |
_complianceChecked | mapping(address => bool) | Tokens requiring canTransfer validation |
_frozenUsers | mapping(address => bool) | Per-user withdrawal freeze |
liquidations | mapping(uint256 => LiquidationInfo) | Liquidation records |
Access Control
Admin Management
Admin transfer is a two-step process to prevent accidental transfer to an incorrect address.Token Configuration
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
canTransfer(caller, custody, amount) before transfer. The caller must have approved the custody contract for the deposit amount.
canTransfer(custody, caller, amount). Allows withdrawal of unsupported tokens to prevent fund lockout after token removal.
Balance Locking
The operator locks user balances when orders are matched, before settlement.amount from the user’s available balance (total minus already locked). Reverts with InsufficientBalance if available balance is insufficient.
InsufficientLockedBalance if the locked balance is less than amount.
Trade Settlement
settlementId must be unique - reuse reverts with SettlementAlreadyProcessed.
BatchSettled(batchId, settledCount, totalSubmitted) so callers can detect partial completion.
freezeUser() before locking to prevent withdrawal front-running between the lock and settlement steps. Emits both PrioritySettlement and TradeSettled.
Liquidation
Receiving Collateral
address(this).
External Sale Settlement
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
Emergency Controls
Stock Split Reconciliation
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
| Function | Returns |
|---|---|
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
| Event | Parameters | When Emitted |
|---|---|---|
Deposited | user, token, amount, newBalance | User deposits tokens |
Withdrawn | user, token, amount | User withdraws tokens |
BalanceLocked | user, token, amount, lockId | Operator locks balance |
BalanceUnlocked | user, token, amount, lockId | Operator unlocks balance |
TradeSettled | settlementId, maker, taker, baseToken, quoteToken, baseAmount, quoteAmount | Trade settlement completes |
BatchSettled | batchId, settledCount, totalSubmitted | Batch settlement completes |
PrioritySettlement | settlementId, liquidatedUser, token | Priority liquidation trade settled |
LiquidationReceived | liquidationId, token, amount, debtOwed, borrower, lendingMarket | Collateral received from router |
LiquidationSettled | liquidationId, usdcReceived, tokenRecipient | External liquidation sale settled |
LiquidationSoldToBuyer | liquidationId, buyer, tokenAmount, usdcAmount, fullyFilled | Internal buyer fills liquidation |
OperatorUpdated | oldOperator, newOperator | Operator address changed |
TokenSupported | token, supported, complianceChecked | Token added or removed |
LiquidationRouterUpdated | oldRouter, newRouter | Router address changed |
UsdcAddressUpdated | oldUsdc, newUsdc | USDC address changed |
AdminTransferInitiated | currentAdmin, pendingAdmin | Admin transfer started |
AdminTransferCompleted | oldAdmin, newAdmin | Admin transfer accepted |
UserFrozen | user, operator | User withdrawals frozen |
UserUnfrozen | user, operator | User withdrawals unfrozen |
WithdrawalsPaused | operator | Global withdrawal pause |
WithdrawalsUnpaused | operator | Global withdrawal unpause |
BalanceReconciled | user, token, oldBalance, newBalance | Balance adjusted after stock split |
Errors
| Error | Trigger |
|---|---|
Unauthorized | Caller lacks required role |
InsufficientBalance | Available balance less than requested amount |
InsufficientLockedBalance | Locked balance less than transfer/unlock amount |
ZeroAmount | Amount parameter is 0 |
ZeroAddress | Address parameter is address(0) |
SettlementAlreadyProcessed | Settlement ID reused |
InvalidSettlement | Batch array lengths mismatch or empty |
TransferFailed | ERC-20 transfer returned false |
TokenNotSupported | Token not registered via addSupportedToken |
TransferRestricted(code, reason) | Security token canTransfer rejected |
LiquidationNotFound | Liquidation ID does not exist |
LiquidationAlreadySettled | Liquidation already fully settled |
LiquidationAmountExceeded | Requested token amount exceeds remaining liquidation amount |
BuyerInsufficientUsdc | Buyer’s USDC custody balance too low |
UserIsFrozen | Frozen user attempts withdrawal |
WithdrawalsArePaused | Withdrawal attempted during global pause |
InvalidBatchSize | Batch settlement called with empty array |
AdminTransferNotPending | acceptAdmin called by non-pending address |
ILendingMarket
Callback interface that lending market contracts must implement to receive liquidation settlement notifications.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.