Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions starter-templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ They are more comprehensive than **building-blocks**, and can be adapted into yo
5. **Vault Harvester** — [`./vault-harvester`](./vault-harvester)
Automated DeFi vault harvesting — check if yield is profitable, then harvest and compound. Same cron → read → check → write pattern as the Keeper Bot, with a DeFi-specific profitability check.

6. **Circuit Breaker** — [`./circuit-breaker`](./circuit-breaker)
Monitor on-chain events for anomalies, automatically pause contracts when price deviation thresholds are breached. Dual-trigger pattern combining LogTrigger (event-driven) with Cron (periodic health checks).

> Each subdirectory includes its own README with template-specific steps and example logs.

## License
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
kind: starter-template
id: circuit-breaker-ts
projectDir: .
title: "Circuit Breaker (TypeScript)"
description: "Monitor on-chain events for anomalies, automatically pause contracts when thresholds are breached."
language: typescript
category: workflow
tags:
- circuit-breaker
- risk
- safety
- pause
- log-trigger
- cron
- on-chain-read
- on-chain-write
workflows:
- dir: my-workflow
postInit: |
A demo ProtocolWithBreaker contract is pre-deployed on Sepolia. See README.md for details.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.env
183 changes: 183 additions & 0 deletions starter-templates/circuit-breaker/circuit-breaker-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Circuit Breaker — CRE Starter Template (TypeScript)

Monitor on-chain events for anomalies, automatically pause contracts when thresholds are breached.

**⚠️ DISCLAIMER**

This template is an educational example to demonstrate how to interact with Chainlink systems, products, and services. It is provided **"AS IS"** and **"AS AVAILABLE"** without warranties of any kind, has **not** been audited, and may omit checks or error handling for clarity. **Do not use this code in production** without performing your own audits and applying best practices. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs generated due to errors in code.

---

## Overview

This template demonstrates the **dual-trigger circuit breaker** pattern using Chainlink CRE (Compute Runtime Environment). It combines a **LogTrigger** (react to price update events) with a **Cron trigger** (periodic health checks) to provide comprehensive protocol safety monitoring.

### Use Cases

- **DeFi protocol safety**: Price deviation > threshold -> pause deposits
- **Price deviation monitoring**: Oracle update deviates > 10% from last known -> pause trading
- **Bridge anomaly**: Unusual bridge volume -> pause bridge contract
- **Treasury drain**: Large unexpected outflows -> freeze treasury
- **NFT mint abuse**: Mint rate exceeds threshold -> pause minting

## Architecture

```
┌──────────────────────┐
│ Protocol Contract │
│ emits PriceUpdated │
└──────────┬───────────┘
│ events
v
┌─────────────────────────────────────────────────────────────────┐
│ CRE DON │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ LogTrigger │-->│ Decode │-->│ Anomaly Check │ │
│ │ (events) │ │ Event Data │ │ deviation > X%? │ │
│ └──────────────┘ └──────────────┘ └────────┬─────────┘ │
│ │ │
│ ┌──────────────┐ │ │
│ │ Cron │--> Health check ───────────────┤ │
│ │ (periodic) │ (read state, verify ok) │ │
│ └──────────────┘ │ │
│ ┌─────────v──────────┐ │
│ │ ANOMALY? │ │
│ │ YES -> Pause │ │
│ │ NO -> Log OK │ │
│ └─────────┬──────────┘ │
│ │ │
└──────────────────────────────────────────────────┼──────────────┘
┌──────────v──────────┐
│ KeystoneForwarder │
│ -> _processReport │
│ (pause protocol) │
└─────────────────────┘
```

## Components

### CRE Workflow (`my-workflow/`)

Two triggers work together:

**Trigger 0 — LogTrigger (event-driven):**
1. Fires when `ProtocolWithBreaker` emits a `PriceUpdated` event
2. Decodes the event (newPrice, oldPrice, timestamp) via generated typed bindings
3. Reads on-chain state (paused?, tripCount, threshold)
4. Calculates price deviation in basis points
5. If deviation > threshold: trips the circuit breaker (pauses protocol on-chain)

**Trigger 1 — Cron (periodic health check):**
1. Fires every 10 minutes (configurable)
2. Reads protocol state (paused, lastPrice, lastUpdate, tripCount)
3. Logs the current status for monitoring

### Consumer Contract (`contracts/evm/src/ProtocolWithBreaker.sol`)

A protocol contract with built-in circuit breaker:

- `updatePrice(uint256)` — simulates price updates, emits `PriceUpdated` events
- `paused()` — whether the protocol is currently paused
- `lastPrice()`, `lastPriceTimestamp()` — current price state
- `priceDeviationThresholdBps()` — deviation threshold in basis points (1000 = 10%)
- `tripCount()` — how many times the breaker has been tripped
- `_processReport(bytes)` — decodes action (PAUSE/UNPAUSE) and reason, updates state

## Getting Started

A demo `ProtocolWithBreaker` contract is pre-deployed on Sepolia with an anomalous price event already emitted — this template works out of the box.

### Prerequisites

- [Bun](https://bun.sh/) runtime installed
- [CRE CLI](https://docs.chain.link/cre) installed

### 1. Install Dependencies

```bash
cd my-workflow && bun install && cd ..
cd contracts && bun install && cd ..
```

### 2. Run Tests

```bash
cd my-workflow && bun test
```

### 3. Simulate

**Cron health check (trigger index 1):**
```bash
cre workflow simulate my-workflow --target staging-settings
```

**LogTrigger with pre-emitted anomalous price event (trigger index 0):**
```bash
cre workflow simulate my-workflow --target staging-settings --non-interactive \
--trigger-index 0 \
--evm-tx-hash 0x98dfb56db57cb0689e5e4092949d41431a283f52128066bc941b0c219d3f6203 \
--evm-event-index 0
```

### 4. Emit Your Own Price Events

Emit a normal price update (within 10% threshold):
```bash
cast send 0xaCb13C9940cB61367b45eEd504E410D4B4d7A6e4 \
"updatePrice(uint256)" 950000000000000000 \
--private-key $CRE_ETH_PRIVATE_KEY \
--rpc-url https://ethereum-sepolia-rpc.publicnode.com
```

Emit an anomalous price update (>10% deviation):
```bash
cast send 0xaCb13C9940cB61367b45eEd504E410D4B4d7A6e4 \
"updatePrice(uint256)" 100000000000000000 \
--private-key $CRE_ETH_PRIVATE_KEY \
--rpc-url https://ethereum-sepolia-rpc.publicnode.com
```

Then simulate with the resulting tx hash:
```bash
cre workflow simulate my-workflow --target staging-settings --non-interactive \
--trigger-index 0 \
--evm-tx-hash <YOUR_TX_HASH> \
--evm-event-index 0
```

### 5. Deploy Your Own Contract (Optional)

Deploy `contracts/evm/src/ProtocolWithBreaker.sol` using Foundry:

```bash
forge create src/ProtocolWithBreaker.sol:ProtocolWithBreaker --broadcast \
--private-key <KEY> --rpc-url <RPC> \
--constructor-args 0x15fc6ae953e024d975e77382eeec56a9101f9f88 1000000000000000000 1000
```

Constructor arguments:
- `forwarder`: CRE KeystoneForwarder on Sepolia (`0x15fc6ae953e024d975e77382eeec56a9101f9f88`)
- `_initialPrice`: Initial price in wei (e.g., `1000000000000000000` = 1e18)
- `_deviationThresholdBps`: Deviation threshold in basis points (`1000` = 10%)

## Customization

- **Change the threshold**: Deploy with a different `_deviationThresholdBps` value
- **Change the schedule**: Edit `schedule` in `config.staging.json`
- **Add webhook alerts**: Extend `tripCircuitBreaker()` with `cre.capabilities.HTTPClient` POST to Slack/PagerDuty
- **Monitor different events**: Change `logTriggerPriceUpdated()` to monitor other contract events
- **Add unpause logic**: Create a separate workflow or cron handler that calls ACTION_UNPAUSE when conditions normalize

## Security

- The contract is a **demo** — audit and customize before production use
- `ReceiverTemplate` validates that only CRE Forwarder can call `onReport()`
- The circuit breaker is **one-way by default** — only CRE can unpause via ACTION_UNPAUSE report
- Never commit `.env` files or secrets

## License

MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { parseAbi } from "viem"

export const ProtocolWithBreakerAbi = parseAbi([
"event PriceUpdated(uint256 indexed newPrice, uint256 indexed oldPrice, uint256 timestamp)",
"function paused() view returns (bool)",
"function lastPrice() view returns (uint256)",
"function lastPriceTimestamp() view returns (uint256)",
"function priceDeviationThresholdBps() view returns (uint256)",
"function tripCount() view returns (uint256)",
])
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ProtocolWithBreaker'
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/IERC165.sol)

pragma solidity >=0.4.16;

/**
* @dev Interface of the ERC-165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[ERC].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(
bytes4 interfaceId
) external view returns (bool);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC165} from "./IERC165.sol";

/// @title IReceiver - receives keystone reports
/// @notice Implementations must support the IReceiver interface through ERC165.
interface IReceiver is IERC165 {
/// @notice Handles incoming keystone reports.
/// @dev If this function call reverts, it can be retried with a higher gas
/// limit. The receiver is responsible for discarding stale reports.
/// @param metadata Report's metadata.
/// @param report Workflow report.
function onReport(
bytes calldata metadata,
bytes calldata report
) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {ReceiverTemplate} from "./ReceiverTemplate.sol";

/**
* @title ProtocolWithBreaker
* @notice A protocol contract with circuit breaker functionality.
* CRE monitors PriceUpdated events for anomalies and can pause the protocol
* via signed reports when deviation thresholds are breached.
*
* In production, replace updatePrice() with your actual oracle/price feed,
* and add protocol-specific pause logic in _processReport().
*/
contract ProtocolWithBreaker is ReceiverTemplate {
bool public paused;
uint256 public lastPrice;
uint256 public lastPriceTimestamp;
uint256 public priceDeviationThresholdBps; // basis points (e.g., 1000 = 10%)
uint256 public tripCount;

event PriceUpdated(
uint256 indexed newPrice,
uint256 indexed oldPrice,
uint256 timestamp
);
event CircuitBreakerTripped(string reason, uint256 indexed tripCount, uint256 timestamp);
event CircuitBreakerReset(uint256 timestamp);

uint8 constant ACTION_PAUSE = 1;
uint8 constant ACTION_UNPAUSE = 2;

constructor(
address forwarder,
uint256 _initialPrice,
uint256 _deviationThresholdBps
) ReceiverTemplate(forwarder) {
lastPrice = _initialPrice;
lastPriceTimestamp = block.timestamp;
priceDeviationThresholdBps = _deviationThresholdBps;
}

/// @notice Called by CRE Forwarder via ReceiverTemplate after metadata validation
/// @param report ABI-encoded (uint8 action, string reason)
function _processReport(bytes calldata report) internal override {
(uint8 action, string memory reason) = abi.decode(report, (uint8, string));

if (action == ACTION_PAUSE) {
paused = true;
tripCount++;
emit CircuitBreakerTripped(reason, tripCount, block.timestamp);
} else if (action == ACTION_UNPAUSE) {
paused = false;
emit CircuitBreakerReset(block.timestamp);
}
}

/// @notice Simulates a price update (in production, called by oracle or price feed)
function updatePrice(uint256 newPrice) external {
uint256 oldPrice = lastPrice;
lastPrice = newPrice;
lastPriceTimestamp = block.timestamp;
emit PriceUpdated(newPrice, oldPrice, block.timestamp);
}
}
Loading
Loading