Prerequisites
Before deploying StockCustody, the following must be in place:IdentityRegistryUpgradeabledeployed and configured with KYC providers.- At least one
StockTokendeployed with the identity registry address. - USDC (or chosen settlement stablecoin) available on the target chain.
- A
LiquidationRouterdeployed, or a placeholder address if liquidation is not yet needed.
Compiler Configuration
ThebatchSettleTrades function takes 9 calldata array parameters plus batchId, which exceeds the EVM’s 16-slot stack limit in the legacy compilation pipeline. The Solidity compiler must use viaIR: true.
Deployment Sequence
Deploy the implementation contract, then deploy an ERC1967Proxy pointing to it and call
initialize.const { ethers, upgrades } = require("hardhat");
const StockCustody = await ethers.getContractFactory("StockCustody");
const custody = await upgrades.deployProxy(
StockCustody,
[
"0xADMIN_ADDRESS", // admin (multi-sig recommended)
"0xOPERATOR_ADDRESS", // settlement operator (or 0x0 to default to admin)
"0xLIQUIDATION_ROUTER", // router contract address
"0xUSDC_ADDRESS" // USDC token address
],
{ kind: "uups" }
);
await custody.deployed();
_admin_operator_liquidationRouter_usdcRegister each token the orderbook will trade. Set
complianceChecked to true for security tokens and false for standard ERC-20s.// Register stock token (compliance-checked)
await custody.addSupportedToken("0xSTOCK_TOKEN", true);
// Register USDC (standard ERC-20)
await custody.addSupportedToken("0xUSDC_ADDRESS", false);
The custody contract must be registered as an authorized contract on each StockToken. Without this, all deposits of compliance-checked tokens will fail because the custody contract address cannot pass KYC checks.
const stockToken = await ethers.getContractAt("StockToken", "0xSTOCK_TOKEN");
await stockToken.addAuthorizedContract(custody.address);
Deposit and Withdrawal
Users must deposit tokens into custody before they can place orders on the orderbook.Trade Settlement Flow
The matching engine (operator) follows a lock-then-settle pattern.const lockId = ethers.utils.id("lock-001");
// Lock maker's USDC (maker is buyer)
await custody.lockBalance(maker, usdc.address, quoteAmount, lockId);
// Lock taker's stock tokens (taker is seller)
await custody.lockBalance(taker, stockToken.address, baseAmount, lockId);
batchSettleTrades to settle multiple trades in one transaction. Size batches based on your target chain’s block gas limit. The gas guard stops processing if remaining gas drops below 80,000, so always check the BatchSettled event’s settledCount against totalSubmitted and retry any remaining settlements.
Liquidation Flows
Two paths exist for settling liquidated collateral received from a lending market.Path A: Internal Buyer
The buyer already has USDC in custody. Tokens move between internal balances and USDC is transferred on-chain to the router.getLiquidation(liquidationId) to see the remaining amount.
Path B: External Sale
The operator sells tokens through an external venue, then provides USDC proceeds.ILendingMarket.receiveLiquidationProceeds. If the callback reverts, settlement still completes. Monitor LiquidationSettled and LiquidationSoldToBuyer events for reconciliation.
Priority Trade Flow
For liquidating tokens already held in a user’s custody balance (not received via the router):Stock Split Reconciliation
After aStockToken.stockSplit() executes, internal custody balances must be updated to match the new token supply.
StockSplit event on StockToken to trigger reconciliation. The user list must be complete - any omitted user will have stale balances.
