Prerequisites
Before deploying CommodityCustody, the following must be in place:IdentityRegistryUpgradeabledeployed and configured with KYC providers.- At least one
CommodityTokenUpgradeabledeployed viaCommodityTokenFactoryUpgradeable. - 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 CommodityCustody = await ethers.getContractFactory("CommodityCustody");
const custody = await upgrades.deployProxy(
CommodityCustody,
[
"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. Both commodity tokens and USDC are registered the same way.
// Register commodity token
await custody.addSupportedToken("0xCOMMODITY_TOKEN");
// Register USDC
await custody.addSupportedToken("0xUSDC_ADDRESS");
The custody contract must be authorized on each commodity token for deposits and withdrawals to succeed. The required configuration depends on the token’s transfer restriction mode:
const commodityToken = await ethers.getContractAt(
"CommodityTokenUpgradeable", "0xCOMMODITY_TOKEN"
);
// WHITELIST mode: whitelist the custody contract
await commodityToken.setWhitelist(custody.address, true);
// BLACKLIST mode: ensure custody is not blacklisted (no action needed if clean)
// KYC required: verify the custody contract address in the identity registry
await identityRegistry.verifyIdentity(
custody.address,
keccak256("CUSTODY_CONTRACT"),
investorType,
softExpiry,
hardExpiry
);
Deposit and Withdrawal
Users must deposit tokens into custody before they can place orders on the orderbook.checkTransferability function on CommodityCustody. To pre-check whether a deposit or withdrawal will succeed, query the commodity token’s compliance state directly:
Trade Settlement Flow
The matching engine (operator) follows a lock-then-settle pattern. This is identical to the Stock Orderbook flow.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 commodity tokens (taker is seller)
await custody.lockBalance(taker, commodityToken.address, baseAmount, lockId);
Atomically transfer locked balances between the counterparties. These are internal ledger movements - no on-chain token transfers occur.
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. The commodity token enforces compliance during the on-chain transfer to the external buyer.ILendingMarket.receiveLiquidationProceeds. If the callback reverts, settlement still completes. Monitor LiquidationSettled and LiquidationSoldToBuyer events for reconciliation.
