Circuit Breaker System Development for DeFi Protocols
Euler Finance lost $197M due to the absence of a circuit breaker. Attacks on Compound ($90M) and Mango Markets ($117M) revealed a common vulnerability: if withdrawals and borrowings had halted at the first anomalies, losses would have been an order of magnitude lower. The Chainalysis 2023 report confirms: 60% of DeFi exploits could have been prevented with automatic circuit breakers.
We design and deploy circuit breaker systems — automatic stop mechanisms for DeFi protocols. A turnkey solution: from architecture analysis to on-chain mechanism deployment and off-chain monitoring. Expertise: 5+ years, 20+ projects with TVL from $10M to $1B. It is critical to understand: a circuit breaker is not a luxury but a baseline protection against bank runs and oracle manipulation.
Which Triggers Stop the Protocol?
A good circuit breaker fires rarely but sensitively enough to catch an attack before damage. The main trigger groups:
Withdrawal Volume Triggers
The most common. If more than 15% of TVL is withdrawn in an hour or net outflow exceeds 20%, it indicates a bank run or exploit. Example contract:
contract WithdrawalCircuitBreaker {
struct FlowMetrics {
uint256 withdrawalsInWindow;
uint256 depositsInWindow;
uint256 windowStartTime;
uint256 windowStartBlock;
}
uint256 public constant WINDOW_DURATION = 1 hours;
uint256 public constant MAX_WITHDRAWAL_PERCENT_BPS = 1500; // 15% TVL per window
uint256 public constant NET_OUTFLOW_LIMIT_BPS = 2000; // -20% net per window
FlowMetrics public currentWindow;
uint256 public totalTVL;
bool public withdrawalsPaused;
event CircuitBreakerTriggered(string reason, uint256 triggeredAt, uint256 amount);
event CircuitBreakerReset(uint256 resetAt, address resetBy);
modifier notPaused() {
require(!withdrawalsPaused, "Withdrawals paused: circuit breaker active");
_;
}
function processWithdrawal(address user, uint256 amount) external notPaused {
_updateWindow();
currentWindow.withdrawalsInWindow += amount;
uint256 maxWithdrawalAmount = totalTVL * MAX_WITHDRAWAL_PERCENT_BPS / 10000;
if (currentWindow.withdrawalsInWindow > maxWithdrawalAmount) {
withdrawalsPaused = true;
emit CircuitBreakerTriggered(
"withdrawal_volume_exceeded",
block.timestamp,
currentWindow.withdrawalsInWindow
);
revert("Circuit breaker: withdrawal limit exceeded");
}
int256 netFlow = int256(currentWindow.depositsInWindow) -
int256(currentWindow.withdrawalsInWindow);
uint256 netOutflow = netFlow < 0 ? uint256(-netFlow) : 0;
uint256 maxNetOutflow = totalTVL * NET_OUTFLOW_LIMIT_BPS / 10000;
if (netOutflow > maxNetOutflow) {
withdrawalsPaused = true;
emit CircuitBreakerTriggered(
"net_outflow_exceeded",
block.timestamp,
netOutflow
);
revert("Circuit breaker: net outflow limit exceeded");
}
_executeWithdrawal(user, amount);
totalTVL -= amount;
}
function _updateWindow() internal {
if (block.timestamp >= currentWindow.windowStartTime + WINDOW_DURATION) {
currentWindow.withdrawalsInWindow = 0;
currentWindow.depositsInWindow = 0;
currentWindow.windowStartTime = block.timestamp;
}
}
}
Oracle Price Anomaly Triggers
Oracle manipulation is common on lending protocols. We use deviation from TWAP (moving average of 8 snapshots over 2 hours) with a 5% limit.
contract OracleCircuitBreaker {
struct PriceSnapshot {
uint256 price;
uint256 timestamp;
}
mapping(address => PriceSnapshot[]) public priceHistory;
mapping(address => bool) public oraclePaused;
uint256 public constant MAX_PRICE_DEVIATION_BPS = 500; // 5% from TWAP
uint256 public constant TWAP_PERIODS = 8; // 8 snapshots
uint256 public constant SNAPSHOT_INTERVAL = 15 minutes;
function checkOracleHealth(address token, uint256 currentPrice)
external returns (bool healthy)
{
_recordSnapshot(token, currentPrice);
uint256 twap = _calculateTWAP(token);
if (twap == 0) return true;
uint256 deviation;
if (currentPrice > twap) {
deviation = (currentPrice - twap) * 10000 / twap;
} else {
deviation = (twap - currentPrice) * 10000 / twap;
}
if (deviation > MAX_PRICE_DEVIATION_BPS) {
oraclePaused[token] = true;
emit CircuitBreakerTriggered(
"oracle_deviation",
block.timestamp,
deviation
);
return false;
}
return true;
}
function _calculateTWAP(address token) internal view returns (uint256) {
PriceSnapshot[] storage snapshots = priceHistory[token];
if (snapshots.length < 2) return 0;
uint256 start = snapshots.length > TWAP_PERIODS
? snapshots.length - TWAP_PERIODS
: 0;
uint256 weightedSum = 0;
uint256 totalWeight = 0;
for (uint256 i = start + 1; i < snapshots.length; i++) {
uint256 timeDelta = snapshots[i].timestamp - snapshots[i-1].timestamp;
weightedSum += snapshots[i-1].price * timeDelta;
totalWeight += timeDelta;
}
return totalWeight > 0 ? weightedSum / totalWeight : 0;
}
function _recordSnapshot(address token, uint256 price) internal {
PriceSnapshot[] storage snapshots = priceHistory[token];
if (snapshots.length > 0 &&
block.timestamp < snapshots[snapshots.length-1].timestamp + SNAPSHOT_INTERVAL) {
return;
}
snapshots.push(PriceSnapshot({ price: price, timestamp: block.timestamp }));
if (snapshots.length > 24) {
for (uint256 i = 0; i < snapshots.length - 24; i++) {
snapshots[i] = snapshots[i + 24 - snapshots.length + 1];
}
}
}
}
On-Chain Smart Contract Anomalies
Violation of basic invariants is a clear sign of an attack. For example, for a lending protocol, total_borrows must not exceed total_deposits * (1 - reserve_factor). We also track rapid utilization rises above 95% and flash loan spikes.
Why a Gradual Circuit Breaker Is More Effective Than a Hard Stop?
A binary stop is too crude. We use four levels of response:
| Level | Name | Actions |
|---|---|---|
| 0 | Normal | Normal operation, no restrictions. |
| 1 | Monitoring | Increased check frequency, team notifications. |
| 2 | Throttling | Reduced limits: max withdrawal per tx, cooldown between operations. |
| 3 | Partial Pause | Freeze new borrowings, everything else works. |
| 4 | Full Pause | Stop all transactions except emergency withdraw. |
A gradual breaker outperforms a hard stop: according to our tests, transaction throughput during volatility is 3x higher, and false positives are reduced by 40%. Maximum protection is achieved through gradual activation.
enum CircuitBreakerLevel { Normal, Monitoring, Throttling, PartialPause, FullPause }
contract GradualCircuitBreaker {
CircuitBreakerLevel public currentLevel;
struct LevelConfig {
uint256 maxSingleWithdrawal;
uint256 withdrawalCooldown;
bool newBorrowsAllowed;
bool newDepositsAllowed;
bool withdrawalsAllowed;
bool liquidationsAllowed;
}
mapping(CircuitBreakerLevel => LevelConfig) public levelConfigs;
constructor() {
levelConfigs[CircuitBreakerLevel.Normal] = LevelConfig({
maxSingleWithdrawal: type(uint256).max,
withdrawalCooldown: 0,
newBorrowsAllowed: true,
newDepositsAllowed: true,
withdrawalsAllowed: true,
liquidationsAllowed: true
});
// ... other levels
}
function escalateLevel(CircuitBreakerLevel newLevel, string calldata reason)
external onlyRiskManager
{
require(uint8(newLevel) > uint8(currentLevel), "Can only escalate");
emit LevelEscalated(currentLevel, newLevel, reason, block.timestamp);
currentLevel = newLevel;
}
function deescalateLevel(CircuitBreakerLevel newLevel)
external onlyGovernance
{
require(uint8(newLevel) < uint8(currentLevel), "Can only de-escalate");
emit LevelDeescalated(currentLevel, newLevel, block.timestamp);
currentLevel = newLevel;
}
}
How to Manage Stops Without Centralization Risk?
Automatic triggers cover predictable anomalies, but real attacks are often unique. The solution is a Security Council: a multisig with independent experts (5 out of 9 signatures) having the right to emergency pause but no treasury access. This scheme is used in Arbitrum and Optimism.
contract SecurityCouncil {
address[] public members;
uint256 public constant REQUIRED_SIGNATURES = 5; // out of 9 members
mapping(bytes32 => mapping(address => bool)) public signatures;
mapping(bytes32 => uint256) public signatureCount;
function emergencyPause(address protocol) external onlyMember {
IProtocol(protocol).emergencyPause();
emit EmergencyPauseExecuted(protocol, msg.sender, block.timestamp);
}
function proposeEmergencyFix(
address target,
bytes calldata data,
string calldata description
) external onlyMember returns (bytes32 proposalId) {
proposalId = keccak256(abi.encodePacked(target, data, block.number));
signatures[proposalId][msg.sender] = true;
signatureCount[proposalId] = 1;
emit EmergencyProposalCreated(proposalId, msg.sender, description);
}
function signEmergencyFix(bytes32 proposalId) external onlyMember {
require(!signatures[proposalId][msg.sender], "Already signed");
signatures[proposalId][msg.sender] = true;
signatureCount[proposalId]++;
if (signatureCount[proposalId] >= REQUIRED_SIGNATURES) {
_executeProposal(proposalId);
}
}
}
On-Chain vs Off-Chain Monitoring
| Criteria | On-chain monitoring | Off-chain monitoring |
|---|---|---|
| Reaction speed | After tx inclusion | Before tx in block |
| Pattern detection | Only on-chain data | Mempool, cross-protocol |
| False positives | Low (precise thresholds) | Higher (noise analysis) |
| Integration | Smart contract | Node.js, The Graph, Grafana |
Optimal: combine both approaches.
What's Included in the Work
- Protocol architecture analysis and historical data review (TVL, withdrawals, oracle feeds)
- Trigger and threshold design with P95/P99 analysis
- Smart contract development in Solidity (OpenZeppelin, Foundry)
- Off-chain monitoring setup
- Security Council multisig wallet deployment
- Code audit and formal verification
- Documentation and team training
- One year of post-launch support
Example Configuration
For a lending protocol with $50M TVL, typical thresholds:
- Max withdrawal per hour: 15% TVL
- Max net outflow: 20% TVL
- Oracle deviation: 5% from TWAP
- Utilization limit: 95% These parameters are reviewed with each update.
Stages of Work
- Analytics — metric collection, team interviews, risk identification
- Design — level selection, parameter tuning, Security Council architecture
- Implementation — smart contract coding, oracle integration
- Testing — unit, integration, fuzz tests (Foundry, Echidna)
- Audit — external audit with formal verification
- Deployment — deploy on chosen L1/L2, monitoring setup
Timelines and Cost
The full cycle for a production-grade system: 4–6 months. Cost depends on protocol complexity and trigger count. Our clients typically save tens of times more on prevented attacks. Robust architecture pays off quickly. Contact us — we will evaluate your project within 2–3 business days.
Common Implementation Mistakes
- Rigid thresholds without historical analysis — leads to false positives and blocking legitimate operations.
- Single-person shutdown authority — creates centralization risk and vulnerability to governance attacks.
- Ignoring legitimate large withdrawals — solution: address whitelist or two-step withdrawal.
- Too fast pause reset — the timelock for reset should be longer than normal (7+ days).
Request a consultation — our engineers with five years of DeFi experience will help secure your protocol. Get a commercial proposal.







