This page covers operational patterns, security considerations, and common pitfalls when running CommodityCustody in production.
Key Management
Deploy the admin as a multi-sig. The admin key controls upgrades, token registration, USDC address changes, and router changes. A compromised admin key can upgrade the contract to an arbitrary implementation. Use a Gnosis Safe with a minimum 2-of-3 threshold. Consider adding a timelock between upgrade proposal and execution at the proxy level.
Run the operator as a dedicated hot wallet with monitoring. The operator key can settle trades and move internal balances between users. A compromised operator cannot withdraw tokens directly, but can manipulate internal balances through fake settlements. Implement real-time monitoring on all settlement events and alert on unexpected balance movements.
Separate admin and operator keys. Although the onlyOperator modifier permits admin access as a fallback, production deployments should use distinct keys for each role. This limits blast radius if either key is compromised and keeps audit trails unambiguous.
Operator Security
The operator’s freezeUser capability should be used judiciously. An attacker with operator access could freeze all users, effectively halting the entire market. Rate-limit freeze operations in your operator service and alert on bulk freezes.
Settlement IDs are the primary deduplication mechanism. The operator service must guarantee unique settlement IDs. If the operator replays a settlement ID, the transaction reverts with SettlementAlreadyProcessed, which is safe but wastes gas. Use deterministic ID generation (e.g., hash of trade details) rather than sequential counters to avoid collisions across operator restarts.
Balance Integrity
The contract’s internal ledger must always be backed by actual ERC-20 balances held by the contract. This invariant holds as long as all entry points are used correctly, but there are edge cases to be aware of.
Do not send tokens directly to the contract address. Tokens sent via transfer instead of deposit will increase the contract’s ERC-20 balance without crediting any internal balance. These tokens become permanently unrecoverable unless an upgrade adds a sweep function.
Monitor the settleLiquidationWithBuyer flow. This function deducts from the buyer’s internal USDC balance and transfers actual USDC from the contract to the router. The invariant holds because the buyer deposited real USDC via deposit(). If any code path ever credits internal USDC without an actual deposit, the contract’s real balance could fall below the sum of internal balances. No such path exists in the current contract, but flag this for audit awareness during any upgrades that modify balance logic.
Commodity Token Compliance Configuration
This is the single most important operational concern unique to CommodityCustody. Unlike StockCustody, which explicitly checks compliance via canTransfer, CommodityCustody delegates all compliance enforcement to the commodity token itself. This means the custody contract must remain authorized under whatever transfer restriction mode the commodity token uses.
Monitor TransferRestrictionModeUpdated events. If a commodity token issuer changes the transfer restriction mode (e.g., from NONE to WHITELIST, or from BLACKLIST to KYC), the custody contract’s authorization may break silently. All deposits and withdrawals for that token will start reverting with the commodity token’s error codes, not CommodityCustody errors. Build automated monitoring that detects mode changes and immediately verifies the custody contract’s status under the new mode.
Maintain whitelist status proactively. In WHITELIST mode, the custody contract must be explicitly whitelisted via setWhitelist(custodyAddress, true) on the commodity token. If the issuer removes the whitelist entry (accidentally or intentionally), all deposits and withdrawals break. Include custody contract whitelist status in your health checks.
Handle KYC requirements carefully. If kycRequired is enabled on a commodity token, the custody contract’s address must be verified in the IdentityRegistry. The KYC verification has expiry timestamps - if the custody contract’s KYC expires, deposits and withdrawals will fail. Set long expiry windows and monitor KYCStatusChanged events for the custody contract’s address.
Three layers of freeze. A user’s ability to withdraw from custody depends on three independent freeze states: the custody-level freeze (freezeUser), the commodity token-level freeze (frozenAccounts), and the identity registry-level freeze (freezeAddress). All three must be clear for a withdrawal to succeed. Your compliance dashboard should display all three states together.
Withdrawal Protection Strategy
Two independent custody-level withdrawal protection layers serve different purposes:
Per-user freeze (freezeUser) blocks a specific user’s withdrawals. Use this during liquidation to prevent front-running between the lock and settlement steps. The recommended flow is: freeze user, lock balance, settle, unfreeze. Keep the freeze duration as short as possible.
Global pause (pauseWithdrawals) acts as an emergency circuit breaker. Use this for contract-wide incidents: discovered vulnerabilities, oracle failures, or chain-level issues. The pause blocks all withdrawals for all users but does not affect trade settlements or liquidation flows. This means the operator can continue settling pending trades during a withdrawal pause.
Additionally, the commodity token’s own pause mechanism provides a third layer. If the commodity token is paused, all deposits and withdrawals of that token fail regardless of the custody contract’s state. Coordinate with the commodity token issuer before pausing either system.
Compliance Boundary
Trade settlements within custody do not validate on-chain compliance. Internal transfers are purely ledger operations. The off-chain matching engine carries the full regulatory burden.
Your matching engine must verify both counterparties are eligible before creating a match:
- Both parties must have active KYC status in the identity registry (if the commodity token requires KYC).
- Both parties must not be blacklisted (if the commodity token uses blacklist mode).
- Both parties must be whitelisted (if the commodity token uses whitelist mode).
- Neither party should be frozen at the token level or registry level.
If a user’s compliance status changes between order placement and settlement, the matching engine should cancel the order rather than settling. Build a compliance status cache in your matching engine and subscribe to the relevant events from both the commodity token and identity registry for real-time updates.
Liquidation Router Coordination
The liquidation router is a separate contract that bridges between lending markets and custody. It must approve the custody contract to pull collateral tokens when calling receiveLiquidatedCollateral. If the router’s approval is insufficient or revoked, collateral delivery will fail.
When updating the router address via setLiquidationRouter, ensure the new router has approved the custody contract before switching. A misconfigured router blocks all new liquidation flows. Pending liquidations received from the old router can still be settled.
The settleLiquidation function transfers commodity tokens to an external buyer via the token’s transfer(). This on-chain transfer triggers the commodity token’s compliance checks. If the external buyer is not authorized under the token’s restriction mode, the settlement will revert. Ensure external buyers are pre-verified before attempting external liquidation settlement.
Front-Running Mitigation
On L1 Ethereum, the priority trade flow (freeze, lock, settle, unfreeze) is vulnerable to front-running between steps. A user watching the mempool could withdraw tokens between the freeze transaction being submitted and being mined.
Mitigate this by deploying on an L2 with a sequencer (where transaction ordering is deterministic) or by using a private mempool service (e.g., Flashbots Protect) for liquidation transactions. On L2s with sub-second block times, the window is negligible.
Batch Settlement Sizing
The batchSettleTrades gas guard stops processing at 80,000 remaining gas per iteration. A single trade settlement within a batch costs approximately 40,000-60,000 gas depending on storage slot warmth. On chains with higher block gas limits (L2s), you can fit larger batches.
Size batches conservatively at first and monitor BatchSettled events. If settledCount is consistently less than totalSubmitted, your batches are too large for the available block gas. Reduce batch size or split into multiple transactions.
Always check the return event and retry unsettled trades. The gas guard is a safety mechanism, not an error.
Upgrade Safety
The contract includes a uint256[50] private __gap storage reservation. When adding state variables in future upgrades, reduce the gap size accordingly. Never insert variables before existing state declarations.
Before upgrading:
- Deploy the new implementation to a test environment and run your full test suite against the existing proxy state.
- Verify storage layout compatibility using OpenZeppelin’s upgrade safety tooling.
- Have the upgrade reviewed by your security auditor.
- Execute through the admin multi-sig with appropriate governance approval.
A bricked proxy from an incompatible storage layout permanently locks all custodied tokens. Storage validation before every upgrade is not optional.
Token Removal
Removing a token via removeSupportedToken blocks new deposits and trade settlements for that token. Users with existing balances can still withdraw. Do not remove a token while there are pending (locked) balances or open liquidations for that token. Clear all pending activity before removing.
Monitoring and Alerting
At minimum, monitor these events in production:
| Event | Alert priority | Action |
|---|
BatchSettled where settledCount < totalSubmitted | High | Retry remaining settlements |
LiquidationReceived | High | Operator must match or sell tokens |
UserFrozen | Medium | Track freeze duration, alert if extended |
WithdrawalsPaused | High | Investigate root cause, communicate to users |
AdminTransferInitiated | High | Verify legitimacy of admin change |
OperatorUpdated | High | Verify new operator key is secured |
LiquidationRouterUpdated | Medium | Verify new router approvals |
TokenSupported with supported=false | Medium | Verify no pending activity for removed token |
Additionally, monitor these commodity token events that directly impact custody operations:
| Event | Alert priority | Action |
|---|
TransferRestrictionModeUpdated | Critical | Re-verify custody contract authorization immediately |
WhitelistUpdated (custody address removed) | Critical | Deposits/withdrawals broken, re-whitelist |
AccountFrozen (custody address) | Critical | All deposits/withdrawals for this token broken |
Paused | High | All deposits/withdrawals for this token blocked |
Track the pendingLiquidationCount state variable. If it grows without corresponding settlement events, liquidations are stalling and require operator intervention.