Risk Engine Design for Derivatives DEXs: Liquidation and Solvency
Build robust risk engines for derivatives DEXs. Implement liquidation systems, solvency monitoring, and protocol protection mechanisms.
The risk engine is the heart of any derivatives exchange. It monitors every position, calculates real-time risk metrics, and protects the protocol from insolvency. A well-designed risk engine can mean the difference between a thriving exchange and catastrophic failure.
This comprehensive guide covers the architecture, implementation, and best practices for building production-grade risk management systems for perpetual DEXs.
Risk Engine Architecture
Core Components
contract RiskEngine {
struct PositionRisk {
bytes32 positionId;
address trader;
uint256 marginRatio;
int256 unrealizedPnL;
uint256 liquidationPrice;
uint256 notionalValue;
bool isLiquidatable;
RiskTier tier;
}
enum RiskTier {
SAFE, // > 20% margin
CAUTION, // 10-20% margin
WARNING, // 6-10% margin
LIQUIDATABLE // < 5% margin
}
// Risk parameters
uint256 public initialMarginRatio = 1000; // 10%
uint256 public maintenanceMarginRatio = 500; // 5%
uint256 public liquidationFee = 250; // 2.5%
uint256 public maxLeverage = 50; // 50x
// Protocol safety
uint256 public insuranceFundBalance;
uint256 public badDebt;
uint256 public lastHealthCheck;
mapping(bytes32 => PositionRisk) public positionRisks;
mapping(address => bool) public liquidators;
mapping(address => uint256) public traderTotalExposure;
event PositionRiskUpdated(
bytes32 indexed positionId,
RiskTier tier,
uint256 marginRatio
);
event LiquidationExecuted(
bytes32 indexed positionId,
address indexed liquidator,
uint256 liquidationPrice,
uint256 collateralSeized
);
modifier onlyLiquidator() {
require(liquidators[msg.sender], "Not authorized liquidator");
_;
}
// Update risk metrics for a position
function updatePositionRisk(bytes32 positionId) external {
Position storage position = positions[positionId];
// Get current market data
uint256 markPrice = oracle.getMarkPrice(position.market);
uint256 indexPrice = oracle.getIndexPrice(position.market);
// Calculate PnL
int256 unrealizedPnL = calculateUnrealizedPnL(position, markPrice);
// Calculate margin ratio
uint256 notional = (position.size * markPrice) / 1e18;
int256 equity = int256(position.collateral) + unrealizedPnL - position.fundingDebt;
uint256 marginRatio;
if (equity <= 0) {
marginRatio = 0;
} else {
marginRatio = (uint256(equity) * 10000) / notional;
}
// Determine risk tier
RiskTier tier = determineRiskTier(marginRatio);
// Calculate liquidation price
uint256 liqPrice = calculateLiquidationPrice(
position.isLong,
position.entryPrice,
position.leverage,
maintenanceMarginRatio
);
// Store risk data
positionRisks[positionId] = PositionRisk({
positionId: positionId,
trader: position.trader,
marginRatio: marginRatio,
unrealizedPnL: unrealizedPnL,
liquidationPrice: liqPrice,
notionalValue: notional,
isLiquidatable: tier == RiskTier.LIQUIDATABLE,
tier: tier
});
emit PositionRiskUpdated(positionId, tier, marginRatio);
}
function determineRiskTier(uint256 marginRatio) public view returns (RiskTier) {
if (marginRatio >= 2000) return RiskTier.SAFE;
if (marginRatio >= 1000) return RiskTier.CAUTION;
if (marginRatio >= 600) return RiskTier.WARNING;
return RiskTier.LIQUIDATABLE;
}
}
Liquidation Engine
Full Liquidation
contract LiquidationEngine {
struct Liquidation {
bytes32 positionId;
address liquidator;
uint256 liquidationPrice;
uint256 collateralSeized;
uint256 debtRepaid;
uint256 liquidatorReward;
uint256 insuranceContribution;
uint256 timestamp;
}
uint256 public constant LIQUIDATION_FEE = 250; // 2.5%
uint256 public constant LIQUIDATOR_REWARD = 125; // 1.25%
uint256 public constant INSURANCE_CONTRIBUTION = 125; // 1.25%
uint256 public constant MAX_SLIPPAGE = 200; // 2%
mapping(bytes32 => Liquidation) public liquidations;
uint256 public totalLiquidations;
uint256 public totalBadDebt;
event LiquidationExecuted(
bytes32 indexed positionId,
address indexed liquidator,
uint256 collateralSeized,
uint256 badDebt
);
function liquidate(bytes32 positionId) external onlyLiquidator {
PositionRisk memory risk = riskEngine.getPositionRisk(positionId);
require(risk.isLiquidatable, "Position not liquidatable");
Position storage position = positions[positionId];
uint256 liquidationPrice = oracle.getMarkPrice(position.market);
// Verify price is within acceptable slippage
require(
isPriceValidForLiquidation(positionId, liquidationPrice),
"Price slippage too high"
);
// Calculate position value and debt
uint256 positionValue = (position.size * liquidationPrice) / 1e18;
int256 unrealizedPnL = calculateUnrealizedPnL(position, liquidationPrice);
int256 equity = int256(position.collateral) + unrealizedPnL - position.fundingDebt;
// Calculate liquidation amounts
uint256 collateralSeized;
uint256 badDebt = 0;
if (equity > 0) {
collateralSeized = uint256(equity);
// Distribute seized collateral
uint256 liquidatorReward = (collateralSeized * LIQUIDATOR_REWARD) / 10000;
uint256 insuranceContribution = (collateralSeized * INSURANCE_CONTRIBUTION) / 10000;
uint256 protocolFee = collateralSeized - liquidatorReward - insuranceContribution;
// Transfers
collateralToken.transfer(msg.sender, liquidatorReward);
collateralToken.transfer(insuranceFund, insuranceContribution);
collateralToken.transfer(treasury, protocolFee);
} else {
// Bad debt scenario
badDebt = uint256(-equity);
collateralSeized = 0;
totalBadDebt += badDebt;
// Cover bad debt from insurance fund
require(
insuranceFundBalance >= badDebt,
"Insufficient insurance funds"
);
insuranceFundBalance -= badDebt;
}
// Close position
_closePosition(positionId);
// Record liquidation
bytes32 liquidationId = keccak256(abi.encodePacked(
positionId,
block.timestamp,
msg.sender
));
liquidations[liquidationId] = Liquidation({
positionId: positionId,
liquidator: msg.sender,
liquidationPrice: liquidationPrice,
collateralSeized: collateralSeized,
debtRepaid: badDebt,
liquidatorReward: liquidatorReward,
insuranceContribution: insuranceContribution,
timestamp: block.timestamp
});
totalLiquidations++;
emit LiquidationExecuted(positionId, msg.sender, collateralSeized, badDebt);
}
function isPriceValidForLiquidation(
bytes32 positionId,
uint256 liquidationPrice
) internal view returns (bool) {
Position storage position = positions[positionId];
uint256 twapPrice = oracle.getTWAP(position.market, 15 minutes);
uint256 deviation = liquidationPrice > twapPrice
? ((liquidationPrice - twapPrice) * 10000) / twapPrice
: ((twapPrice - liquidationPrice) * 10000) / twapPrice;
return deviation <= MAX_SLIPPAGE;
}
}
Partial Liquidation
contract PartialLiquidation {
uint256 public constant PARTIAL_LIQUIDATION_PERCENT = 25; // Close 25% at a time
function partialLiquidate(bytes32 positionId) external onlyLiquidator {
PositionRisk memory risk = riskEngine.getPositionRisk(positionId);
require(
risk.tier == RiskTier.WARNING || risk.tier == RiskTier.LIQUIDATABLE,
"Position not at risk"
);
Position storage position = positions[positionId];
uint256 closeSize = (position.size * PARTIAL_LIQUIDATION_PERCENT) / 100;
uint256 liquidationPrice = oracle.getMarkPrice(position.market);
// Calculate PnL on closed portion
int256 closedPnL = calculatePnLForSize(position, closeSize, liquidationPrice);
// Calculate fees
uint256 closeNotional = (closeSize * liquidationPrice) / 1e18;
uint256 fee = (closeNotional * LIQUIDATION_FEE) / 10000;
// Update position
position.size -= closeSize;
position.collateral = closedPnL > 0
? position.collateral + uint256(closedPnL) - fee
: position.collateral - uint256(-closedPnL) - fee;
// Distribute fee
uint256 liquidatorReward = (fee * 80) / 100;
uint256 protocolFee = fee - liquidatorReward;
collateralToken.transfer(msg.sender, liquidatorReward);
collateralToken.transfer(treasury, protocolFee);
// Recalculate risk
riskEngine.updatePositionRisk(positionId);
emit PartialLiquidation(positionId, closeSize, fee);
}
}
Liquidation Queue and Priority
contract LiquidationQueue {
struct LiquidatablePosition {
bytes32 positionId;
uint256 marginRatio;
uint256 timestamp;
uint256 rewardBonus;
}
LiquidatablePosition[] public liquidationQueue;
mapping(bytes32 => uint256) public positionQueueIndex;
uint256 public constant QUEUE_PROCESSING_INTERVAL = 1 minutes;
uint256 public lastQueueProcessing;
function addToLiquidationQueue(bytes32 positionId) external {
PositionRisk memory risk = riskEngine.getPositionRisk(positionId);
require(risk.isLiquidatable, "Position not liquidatable");
uint256 index = liquidationQueue.length;
positionQueueIndex[positionId] = index;
liquidationQueue.push(LiquidatablePosition({
positionId: positionId,
marginRatio: risk.marginRatio,
timestamp: block.timestamp,
rewardBonus: calculateRewardBonus(risk.marginRatio)
}));
_sortQueueByPriority();
emit AddedToQueue(positionId, risk.marginRatio);
}
function calculateRewardBonus(uint256 marginRatio) internal pure returns (uint256) {
// Higher bonus for riskier positions
if (marginRatio < 100) return 500; // 5% bonus
if (marginRatio < 300) return 300; // 3% bonus
if (marginRatio < 500) return 100; // 1% bonus
return 0;
}
function _sortQueueByPriority() internal {
// Sort by margin ratio ascending (riskiest first)
for (uint i = 0; i < liquidationQueue.length; i++) {
for (uint j = i + 1; j < liquidationQueue.length; j++) {
if (liquidationQueue[j].marginRatio < liquidationQueue[i].marginRatio) {
LiquidatablePosition memory temp = liquidationQueue[i];
liquidationQueue[i] = liquidationQueue[j];
liquidationQueue[j] = temp;
positionQueueIndex[liquidationQueue[i].positionId] = i;
positionQueueIndex[liquidationQueue[j].positionId] = j;
}
}
}
}
function processQueue(uint256 count) external {
require(
block.timestamp >= lastQueueProcessing + QUEUE_PROCESSING_INTERVAL,
"Processing too frequent"
);
uint256 processed = 0;
for (uint i = 0; i < liquidationQueue.length && processed < count; i++) {
LiquidatablePosition memory pos = liquidationQueue[i];
// Check if still liquidatable
PositionRisk memory risk = riskEngine.getPositionRisk(pos.positionId);
if (risk.isLiquidatable) {
try liquidationEngine.liquidate(pos.positionId) {
processed++;
// Remove from queue
_removeFromQueue(i);
i--; // Adjust index
} catch {
// Skip if liquidation fails
continue;
}
} else {
// No longer liquidatable, remove
_removeFromQueue(i);
i--;
}
}
lastQueueProcessing = block.timestamp;
emit QueueProcessed(processed);
}
function _removeFromQueue(uint256 index) internal {
bytes32 positionId = liquidationQueue[index].positionId;
// Move last element to removed position
if (index < liquidationQueue.length - 1) {
liquidationQueue[index] = liquidationQueue[liquidationQueue.length - 1];
positionQueueIndex[liquidationQueue[index].positionId] = index;
}
liquidationQueue.pop();
delete positionQueueIndex[positionId];
}
}
Insurance Fund Management
contract InsuranceFund {
uint256 public balance;
uint256 public targetBalance;
uint256 public maxBalance;
uint256 public constant DEPOSIT_FEE_BPS = 5; // 0.05% of volume
uint256 public constant WITHDRAWAL_THRESHOLD = 95; // 95% of target
mapping(address => uint256) public contributorShares;
uint256 public totalShares;
event Deposit(address indexed source, uint256 amount);
event Withdrawal(address indexed recipient, uint256 amount);
event BadDebtCovered(uint256 amount);
function deposit(uint256 amount) external {
collateralToken.transferFrom(msg.sender, address(this), amount);
uint256 sharesToMint = totalShares == 0
? amount
: (amount * totalShares) / balance;
contributorShares[msg.sender] += sharesToMint;
totalShares += sharesToMint;
balance += amount;
emit Deposit(msg.sender, amount);
}
function coverBadDebt(uint256 amount) external onlyRiskEngine {
require(balance >= amount, "Insufficient insurance funds");
balance -= amount;
emit BadDebtCovered(amount);
}
function withdraw(uint256 shares) external {
require(
balance >= targetBalance * WITHDRAWAL_THRESHOLD / 100,
"Below withdrawal threshold"
);
uint256 amount = (shares * balance) / totalShares;
contributorShares[msg.sender] -= shares;
totalShares -= shares;
balance -= amount;
collateralToken.transfer(msg.sender, amount);
emit Withdrawal(msg.sender, amount);
}
function allocateTradingFees(uint256 feeAmount) external onlyExchange {
// Allocate portion of trading fees to insurance fund
uint256 insuranceAllocation = (feeAmount * 20) / 100; // 20% of fees
balance += insuranceAllocation;
emit Deposit(address(exchange), insuranceAllocation);
}
function rebalance() external onlyGovernance {
if (balance > maxBalance) {
// Excess goes to treasury
uint256 excess = balance - maxBalance;
balance = maxBalance;
collateralToken.transfer(treasury, excess);
}
}
}
Real-Time Risk Monitoring
contract RealTimeRiskMonitor {
struct SystemRiskMetrics {
uint256 totalOpenInterest;
uint256 totalCollateral;
int256 totalUnrealizedPnL;
uint256 insuranceFundRatio;
uint256 liquidationVolume24h;
uint256 badDebt24h;
uint256 systemHealthScore;
}
SystemRiskMetrics public currentMetrics;
uint256 public lastUpdateTime;
uint256 public constant UPDATE_INTERVAL = 5 minutes;
mapping(address => uint256) public assetExposure;
uint256 public maxSingleAssetExposure = 5000; // 50%
event SystemRiskAlert(string alertType, uint256 severity);
event CircuitBreakerTriggered(string reason);
function updateSystemMetrics() external {
require(
block.timestamp >= lastUpdateTime + UPDATE_INTERVAL,
"Update too frequent"
);
uint256 totalOI = 0;
uint256 totalCollat = 0;
int256 totalPnL = 0;
bytes32[] memory allPositions = getAllActivePositions();
for (uint i = 0; i < allPositions.length; i++) {
PositionRisk memory risk = riskEngine.getPositionRisk(allPositions[i]);
totalOI += risk.notionalValue;
totalCollat += risk.notionalValue * risk.marginRatio / 10000;
totalPnL += risk.unrealizedPnL;
}
uint256 insuranceRatio = insuranceFund.balance * 10000 / totalOI;
currentMetrics = SystemRiskMetrics({
totalOpenInterest: totalOI,
totalCollateral: totalCollat,
totalUnrealizedPnL: totalPnL,
insuranceFundRatio: insuranceRatio,
liquidationVolume24h: getLiquidationVolume24h(),
badDebt24h: getBadDebt24h(),
systemHealthScore: calculateHealthScore(totalOI, totalCollat, insuranceRatio)
});
lastUpdateTime = block.timestamp;
// Check risk thresholds
_checkRiskThresholds();
}
function calculateHealthScore(
uint256 totalOI,
uint256 totalCollat,
uint256 insuranceRatio
) internal pure returns (uint256) {
// Score from 0-10000
uint256 collateralRatio = totalOI > 0 ? (totalCollat * 10000) / totalOI : 0;
// Weight: collateral ratio 60%, insurance ratio 40%
uint256 score = (collateralRatio * 60 + insuranceRatio * 40) / 100;
return score;
}
function _checkRiskThresholds() internal {
// Check insurance fund adequacy
if (currentMetrics.insuranceFundRatio < 500) { // < 5%
emit SystemRiskAlert("LOW_INSURANCE", 3);
}
// Check concentration risk
for (uint i = 0; i < supportedAssets.length; i++) {
uint256 exposure = assetExposure[supportedAssets[i]];
uint256 exposureRatio = currentMetrics.totalOpenInterest > 0
? (exposure * 10000) / currentMetrics.totalOpenInterest
: 0;
if (exposureRatio > maxSingleAssetExposure) {
emit SystemRiskAlert("CONCENTRATION_RISK", 2);
}
}
// Check liquidation spike
if (currentMetrics.liquidationVolume24h > currentMetrics.totalOpenInterest / 10) {
// > 10% of OI liquidated in 24h
emit SystemRiskAlert("LIQUIDATION_SPIKE", 4);
_triggerCircuitBreaker("High liquidation volume");
}
// Check bad debt spike
if (currentMetrics.badDebt24h > insuranceFund.balance / 5) {
// > 20% of insurance fund
emit SystemRiskAlert("BAD_DEBT_SPIKE", 5);
_triggerCircuitBreaker("High bad debt");
}
// Check overall health
if (currentMetrics.systemHealthScore < 3000) {
emit SystemRiskAlert("POOR_SYSTEM_HEALTH", 5);
_triggerCircuitBreaker("Poor system health");
}
}
function _triggerCircuitBreaker(string memory reason) internal {
exchange.pauseTrading();
emit CircuitBreakerTriggered(reason);
}
}
Position Limits and Exposure Management
contract PositionLimits {
struct LimitConfig {
uint256 maxPositionSize;
uint256 maxLeverage;
uint256 maxNotionalPerAsset;
uint256 maxDrawdown;
}
LimitConfig public limits;
mapping(address => mapping(address => uint256)) public traderAssetExposure;
mapping(address => uint256) public traderTotalExposure;
mapping(address => int256) public traderMaxDrawdown;
function checkPositionLimits(
address trader,
address asset,
uint256 newSize,
uint256 leverage
) external view returns (bool, string memory) {
// Check leverage
if (leverage > limits.maxLeverage) {
return (false, "Exceeds max leverage");
}
// Check position size
if (newSize > limits.maxPositionSize) {
return (false, "Exceeds max position size");
}
// Check asset exposure
uint256 assetExposure = traderAssetExposure[trader][asset] + newSize;
if (assetExposure > limits.maxNotionalPerAsset) {
return (false, "Exceeds asset exposure limit");
}
// Check total exposure
uint256 totalExposure = traderTotalExposure[trader] + newSize;
if (totalExposure > limits.maxNotionalPerAsset * 2) {
return (false, "Exceeds total exposure limit");
}
return (true, "");
}
function updateExposure(
address trader,
address asset,
int256 sizeDelta,
int256 pnlDelta
) external {
if (sizeDelta > 0) {
traderAssetExposure[trader][asset] += uint256(sizeDelta);
traderTotalExposure[trader] += uint256(sizeDelta);
} else {
traderAssetExposure[trader][asset] -= uint256(-sizeDelta);
traderTotalExposure[trader] -= uint256(-sizeDelta);
}
// Track drawdown
if (pnlDelta < 0) {
traderMaxDrawdown[trader] += pnlDelta;
}
}
}
Best Practices
1. Oracle Resilience
contract MultiOracleRisk {
address[] public oracles;
uint256 public requiredConfirmations;
uint256 public maxDeviation;
function getConservativePrice() external view returns (uint256) {
uint256[] memory prices = new uint256[](oracles.length);
uint256 validCount = 0;
for (uint i = 0; i < oracles.length; i++) {
try IOracle(oracles[i]).getPrice() returns (uint256 price) {
prices[validCount] = price;
validCount++;
} catch {
continue;
}
}
require(validCount >= requiredConfirmations, "Insufficient oracle responses");
// Use median for robustness
return findMedian(prices, validCount);
}
}
2. Gradual Liquidation
- Start with partial liquidations
- Escalate to full liquidation only if necessary
- Give traders chance to add collateral
3. Liquidator Incentives
- Competitive rewards
- Gas subsidies for liquidators
- Priority queue for at-risk positions
4. Insurance Fund Growth
- Continuous fee allocation
- Protocol revenue sharing
- Emergency fundraising mechanisms
5. Monitoring and Alerts
- Real-time dashboards
- Automated risk reports
- Emergency response procedures
Conclusion
A robust risk engine is non-negotiable for derivatives DEXs. Key success factors:
- Fast Risk Updates: Sub-second position risk calculations
- Reliable Liquidations: Incentivize liquidators, minimize slippage
- Deep Insurance: Protect against tail risk events
- Proactive Monitoring: Catch issues before they become crises
- Graduated Response: Escalate from warnings to circuit breakers
The exchanges that survive extreme market conditions will be those that invested in sophisticated risk management from day one. Risk isn't overhead—it's the foundation of sustainable derivatives trading.