An oracle manipulation attack makes a smart contract act on a wrong price, and the useful first question is how the wrong price got there. Chainlink’s Education Hub draws the line: market manipulation changes the price of an asset, while an oracle attack makes the oracle report incorrect or invalid data. The oracle is working in the first case and failing in the second, so the defenses differ.
This guide covers both. It walks through the attack patterns (flash loan spot prices, thin collateral, TWAP, stale data, misconfigured feeds and L2 sequencer downtime), ten real incidents with their sources, the defenses and a freshness check in code. For how a price feed updates, see how Chainlink price feeds work; this article does not repeat it.
Market manipulation vs oracle exploits
Chainlink defines market manipulation as the price of an asset being artificially moved through the alteration of natural supply and demand. It defines an oracle exploit as an oracle reporting inaccurate data about an event or state of the external world. Its worked example uses two cases:
- Market manipulation. An asset worth 100 is pushed to 200 on the market. The oracle reports 200.
- Oracle attack. The market price stays at 100. The oracle reports 70.
The first case surprises people who assume a decentralized oracle is a shield. Chainlink says plainly that it is not: “if the market-wide price of an asset has been manipulated, the oracle will still report that price”. What its feeds are designed to resist is manipulation of a small part of the market. Chainlink says volume-weighted average price methods and outlier detection play a key role there, and that if only a few low-liquidity markets are manipulated, the feeds are designed to still report the accurate market-wide price.
How cheap the first case is depends on liquidity. The greater an asset’s liquidity, the more capital it takes to move its price, and the lower the liquidity, the less expensive it can be.
The label “oracle manipulation” gets used for both. The CFTC used it for the Mango Markets case and Ackee used it for KiloEx. In Chainlink’s terms the oracle there reported a pumped market price faithfully. KiloEx, where the price was set directly through an access control flaw, matches the definition of an oracle exploit. Both are covered below. For what an oracle is in the first place, see what is a blockchain oracle.
The attack patterns
Spot price from one AMM pool, moved with a flash loan
A protocol that reads the current price of a single AMM pool lets anyone who can move that pool decide the price. A 2020 research paper on flash loan attacks puts it this way: “DEX prices, however, can be manipulated with flash loans.” Halborn, writing about Polter Finance, says reliance on spot prices for tokens is always dangerous for the same reason.
The pattern is short. The attacker borrows with a flash loan, shifts the pool’s balance, uses the distorted price on the victim protocol and finishes inside a single transaction. bZx and Polter below are both this.
Chainlink’s documentation names the risk for DEX volume directly: market integrity can be compromised by flash loan-funded attacks, volume shifts to different exchanges or a well-capitalized actor temporarily manipulating the price on one exchange.
Thin collateral pumped on the venues the oracle reads
If an asset trades thinly, an attacker can buy enough of it to lift its price on every venue an oracle reads, post it as collateral and borrow against the gain. Chainlink’s text on this is blunt: thinly traded assets are easier for malicious actors to manipulate, so detailed market risk analysis should set the loan-to-value ratio for them.
Coverage matters as much as depth. Chainlink gives the example of an asset traded across five exchanges with 85% of volume on two of them. An oracle that reads the other three has poor market coverage. Liquidity migrations are a related trap: when liquidity moves to a new pool, the old pool is left thin and becomes susceptible to manipulation. Inverse Finance below follows this pattern; Mango Markets is the related case of a token bought up on every venue the oracle read.
TWAP manipulation and what it costs
A time-weighted average price (TWAP) averages a pool’s price over a window, so a one-transaction flash loan no longer sets it. The attacker’s task becomes holding a distorted price across the observations in the window.
Uniswap’s October 2022 research on TWAP oracles under proof of stake describes the tradeoffs:
- Spot prices are easy and relatively cheap to manipulate, so most protocols use a rolling 30-minute window. A long window gives smooth prices that lag the market.
- On proof of stake, validators control transaction sequencing. A validator controlling two consecutive blocks can move a pool’s price a long way in the first block and move it back in the next without being arbitraged against. The oracle records a manipulated data point. Uniswap’s introduction puts the direct cost at about two times the pool fee, but its summary adds that this is still prohibitively expensive on top pairs, that manipulation of most Uniswap v3 TWAP oracles is not currently feasible and that a 30-block attack needs roughly 40% of validators. The risk is real for thin pools, not a default for every TWAP.
- The cost falls the more consecutive blocks can be controlled. Uniswap says that with enough market share, validators could execute twenty to thirty block manipulations.
- Wide-range liquidity is, in its words, currently the best possible mitigation. Neither full-range liquidity nor double-sided limit orders fix the problem if a validator has enough market share. Removing the look-ahead for proposers would eliminate the problem, but Uniswap calls that non-trivial from the Ethereum perspective.
Read this as a 2022 analysis of Uniswap v3 on Ethereum. We did not check whether later Ethereum changes have altered the proposer look-ahead, so treat the validator scenario as the paper’s, not as a current measurement. The practical point stands: a TWAP is a cost to the attacker, not a guarantee. Inverse Finance below was a TWAP attack in April 2022, before proof of stake, using ordinary trades held across blocks.
Stale or frozen data
An oracle that is correct but old is wrong for a protocol that assumes it is current. Chainlink’s documentation tells applications to track updatedAt and to pause or switch to another mode if the answer is not updated within the heartbeat or within limits the application accepts. It also notes that some heartbeats last several hours and that congestion can delay an update. The mechanics, and a contract that enforces them, are in how Chainlink price feeds work.
We did not find a stale-data incident with a primary-source post-mortem, so none is in the incident list below.
Misconfigured feeds
The oracle can be right and the plumbing around it wrong. The sourced cases are in the incident list: Moonwell used a raw exchange ratio as a dollar price, and an Aave cap on an exchange rate was set below the valid rate. Other forms of the same mistake are easy to picture:
- Wrong asset. The feed you wired in is not the asset you think it is. Read the feed’s
description()and check the address against Chainlink’s price feed addresses list. - Wrapped asset priced as its base. Chainlink’s documentation says a feed built for the underlying asset reports the market-wide price of the underlying, not the price of the wrapped or bridged token. Wrapped tokens are often less liquid, so that reduces manipulation risk, but in an extreme event such as a bridge hack the wrapped asset can fall away from the underlying. Evaluate the tradeoff between a feed for the wrapped asset and a feed for the underlying.
- Wrong decimals. Read
decimals()from the feed rather than assuming a scale. - Mis-set caps. A ceiling on an exchange rate protects against inflation until it is set wrong.
L2 sequencer downtime
When a rollup’s sequencer is unavailable, users lose access to the standard read and write APIs and no new batched blocks are produced. Chainlink’s documentation says this imbalance in access can disrupt applications such as liquidations or market operations that rely on timely transactions. The danger is a rush when the sequencer returns, which is why the documentation asks for a grace period “to prevent mass liquidations or unexpected disruptions”.
The remedy is the L2 Sequencer Uptime Feed plus a grace period after the sequencer comes back. Chainlink says it is no longer expanding these feeds to additional networks, and that existing feeds on the supported networks continue to operate. Check the L2 Sequencer Uptime Feeds page for your network before you design around one. We did not find a primary-source liquidation incident caused by sequencer downtime, so none is listed below.
Ten incidents
Dates and losses (or, where the source gives only that, the attacker’s profit) are the ones the cited source gives. Where sources disagree the paragraph says so. For each case, the paragraph also says whether a Chainlink feed was involved, as the source words it.
bZx, 18 February 2020
The research paper “Attacking the DeFi Ecosystem with Flash Loans for Fun and Profit” (Imperial College London) describes bZx’s second attack, on 18 February 2020, after a first one on 15 February. The attacker took a flash loan of 7,500 ETH on bZx and pushed the sUSD/ETH price on Uniswap to 106.05 and on the Kyber reserve to 108.44. Those two sources together were the lender’s price oracle. The paper says the price moved from 268.30 to 108.44 sUSD per ETH while other DeFi platforms stayed at 268.30. The attacker made a profit of 2,381.41 ETH in a single transaction, which the paper puts at about 634.9 thousand US dollars. Chainlink was not involved: the paper names Uniswap and Kyber as the oracle.
Harvest Finance, 26 October 2020
Harvest’s own incident page says attackers launched an economic attack on its fUSDC and fUSDT vaults at 02:53 UTC and drained a total of 24 million US dollars. The attacker exploited the effects of impermanent loss inside the Curve.fi Y pool, used the manipulated asset value to get vault shares at a beneficial price and later exited at a regular share price. The page does not mention an oracle network.
Cream Finance, 27 October 2021
Immunefi reports a loss of 130 million US dollars in available liquidity from Cream on Ethereum; Halborn says over 130 million. Immunefi names two root causes: an easily manipulatable hybrid oracle and uncapped supplying of a token. The oracle valued yUSDVault tokens as the total value of the vault divided by its total supply. Halborn explains that by cutting the supply to about 8 million dollars and depositing about 8 million dollars of yUSD into the vault, the attacker roughly doubled the perceived value of the shares. Immunefi describes the same step as raising the vault’s pricePerShare. Neither write-up says Chainlink was involved.
Inverse Finance, 2 April 2022
Inverse’s own governance page says an attacker took advantage of a vulnerable Keep3r TWAP oracle and exploited Anchor and Frontier, its now deprecated lending market. Rekt News puts the loss at about 15.6 million dollars; Inverse’s own post-mortem was not retrievable, so that figure is Rekt’s. By Rekt’s account the attacker swapped 500 ETH for about 1.7 thousand INV through the INV-WETH pair on SushiSwap, which moved the price about 50 times because the pool was thin. They deposited the INV as collateral and borrowed the 15.6 million. Bert Miller (@bertcmiller), quoted by Rekt, says the attacker held an oracle’s price at an insane level across multiple blocks and prevented arbitrage bots from bringing it back. No Chainlink feed was involved: Inverse calls it a Keep3r TWAP oracle.
Mango Markets, 11 October 2022
On the court’s account, the MNGO perpetuals oracle took the average spot price of MNGO from three exchanges: FTX, AscendEX and Serum. The oracle was run by Switchboard. The court describes the scheme as straightforward and undisputed on the motion before it. Avraham Eisenberg deposited about five million USDC into two wallets, bought MNGO on the three exchanges, borrowed against the inflated long position, then sold MNGO on the same exchanges and borrowed and withdrew over 100 million dollars in total. The CFTC says over 110 million. Chainlink was not involved.
On the legal outcome, a jury convicted Eisenberg of commodities fraud, commodities manipulation and wire fraud. On 23 May 2025 the judge vacated the two commodities convictions on venue grounds and entered a judgment of acquittal on wire fraud. On the wire fraud count the opinion says Eisenberg made no false representations, because the platform automatically measured the value of his collateral. The government appealed to the Second Circuit (No. 25-1782, filed 23 July 2025 per CourtListener). Briefing was complete by 28 August 2026, and no decision had been reported when we checked on 24 October 2026.
Polter Finance, 16 November 2024
Halborn says the Polter contracts used the spot price from the SpookySwap V2/V3 pool for the BOO token instead of a trusted source. The attacker took a flash loan, changed the token balance in the pool, deposited some BOO and borrowed heavily against the inflated price. Halborn and SolidityScan put the loss at about 8.7 million dollars; Halborn adds that the team’s police report claimed 12 million. Both write-ups say the price came from the pool’s spot reserves. SolidityScan names the adapter that read it ChainlinkUniV2Adapter, but neither write-up says a Chainlink feed supplied the price.
KiloEx, 14 April 2025
Halborn and Ackee put the loss at about 7.5 million dollars. Halborn’s root cause is an access control flaw: KiloEx’s own KiloPriceFeed contract has a setPrices function that could be reached through a public forwarding contract, where the caller defines the source address and a signature that passes validation. Ackee’s steps show the attacker setting the price down and then up. This was not market manipulation: the price was written directly. Halborn says the attacker exploited it on Base and BSC. Chainlink is not mentioned in either write-up.
Moonwell wrsETH, 4 November 2025
Moonwell’s forum post says that around 5:44 AM UTC on Base an oracle malfunction left wrsETH collateral drastically overvalued, so an attacker could borrow millions in assets against minimal collateral. The post says the protocol prices wrsETH with two Chainlink oracles, ETH/USD and wrsETH/ETH, multiplied together, and that the issue originated from the wrsETH/ETH oracle, which erroneously reported that one wrsETH equals 1,649,934.60732 ETH. The post gives no cause for the value, and we did not find one from Chainlink, so we do not offer one. The bad debt was 3.7 million dollars, and the post says the attack ran within 30 seconds of the malfunction. Moonwell’s emergency steps were caps: it zeroed supply and borrow caps for wrsETH and reduced borrow caps in all markets on Base and Optimism.
Moonwell cbETH, 15 February 2026
Moonwell’s incident summary says a governance proposal executed at 6:01 PM UTC enabled Chainlink OEV wrapper contracts across its core markets on Base and Optimism, and one oracle configuration contained a critical error. Instead of multiplying the cbETH/ETH feed by the ETH/USD price, the oracle used the raw cbETH/ETH exchange rate, about 1.12, as the dollar price. Moonwell’s monitoring detected it at 6:05 PM, but liquidations continued because a correction needed a 5-day governance vote and timelock that could not be bypassed. Liquidators seized 1096.317 cbETH and the protocol was left with 1.78 million dollars of bad debt. Moonwell attributes the fault to its own oracle configuration, not to the feed’s data. OEV, the subject of those wrappers, is about who captures liquidation value and is a separate topic from price manipulation; Chainlink SVR covers it.
Aave wstETH CAPO, 10 March 2026
CAPO is a defense. According to the post-mortem by Chaos Labs on Aave’s governance forum, it places a deterministic, time-weighted upper bound on the exchange rate between a yield-bearing asset such as wstETH and its base asset, to protect lending protocols from oracle-driven inflation attacks and donation-style exploits. On 10 March 2026 a technical incident made the cap on the wstETH/stETH rate fall below the valid market rate on Aave’s Ethereum Core and Prime instances. The effective rate dropped about 2.85%, which triggered roughly 10,938 wstETH of E-Mode liquidations, with no bad debt and about 26 million dollars in liquidation volume. The cause was a mismatch: the on-chain snapshot ratio can only rise by 3% every 3 days, while the timestamp moved 7 days, so the calculated maximum rate of about 1.1939 sat below the rate already in use. The post calls it a configuration incident, not a flaw in the CAPO design. It does not mention Chainlink.
What the ten cases show
In two of the ten, both Moonwell, the source says a Chainlink feed was in the price path: one wrong feed value with no published cause, and one wrong configuration around feeds that Moonwell does not fault. The post-mortems we read do not describe an attacker moving a Chainlink feed’s price. Most of the losses came from a price derived from a thin pool, venue or vault, or from an oracle setup that was wrong around the price. That is where the defenses below put their effort.
Defenses
Chainlink’s documentation puts a lot on the developer: data quality checks, circuit breakers and contingency logic are the integrating team’s responsibility. We run Chainlink nodes (running since 2020), and the boundary the documentation draws is the one to design around. The network aggregates and reports the market price. What your contract does with that price is yours.
Use a decentralized, aggregated feed and read its risk category
Do not read one pool’s spot price as a price (bZx, Polter). A market price feed has three layers of aggregation, at the data source, the node operator and the oracle network. For crypto price feeds the data source rules call for three or more aggregators or market data vendors, and each feed is updated by multiple independent operators who must respond in sufficient number before the answer changes. This is how a feed resists manipulation of one venue. It does not stop a market-wide move.
Check the feed’s category. Chainlink groups feeds by market pricing risk: Low, Medium, High and Very High, plus New Token, Custom, Deprecating and Unrated. A feed that shows no category has not been evaluated, and Chainlink says to treat it as unevaluated and use it at your own risk. On High, you acknowledge that you are solely responsible for monitoring and mitigating the risks. On Very High, which covers assets in the middle of a hack, a bridge failure or a major delisting, the guidance is to wind down reliance or apply strict capital and risk management. Single-source feeds need extra safeguards: value bounds, caps, circuit breakers, freshness checks, fallback behavior and monitoring against independent references.
Check freshness against the feed’s own heartbeat
Compare updatedAt with the chain’s clock and reject or pause beyond the heartbeat plus a margin. The limit is per feed, because heartbeats differ widely between feeds on the same chain: on Ethereum mainnet Chainlink lists heartbeats from one hour to several weeks. How Chainlink price feeds work has the reasoning, a tested contract and measured update gaps. The Node script below is a version with no packages.
If you need fresher data than a push feed can give, Chainlink Data Streams delivers low-latency data off-chain and verifies it on-chain when you need it.
On L2, use the sequencer uptime feed with a grace period
Read the sequencer feed, revert if it reports down and also revert until a grace period has passed since it came back. Chainlink’s example uses 3600 seconds and labels itself unfit for production. Aave’s production sentinel uses the same idea to gate borrowing and liquidation. Remember that Chainlink is no longer expanding these feeds to new networks, so on a network without one you need a different plan. The code section below has a version.
Cap exchange-rate oracles carefully
A cap on a yield-bearing asset’s exchange rate, such as Aave’s CAPO, stops donation-style inflation. It is also one more parameter that can be wrong. In the March 2026 case the ceiling fell under the valid rate and liquidated healthy positions. Test any cap against the current valid rate before it goes live, and check what happens when its inputs, such as time between updates, are larger than you assumed. For stablecoins, Chainlink recommends assessing upside risk and an explicit valuation ceiling for each.
Limit what thin collateral can do
Cream’s second root cause was uncapped supplying of a token. Supply caps and borrow caps limit the damage when a price is wrong. A risk methodology post on Aave’s governance forum (Chaos Labs, December 2022) says borrow caps can prevent traditional and flash borrowing of an asset that may experience a price exploit and lead to insolvency. Treat listing as a risk decision: Chainlink’s own advice is to set loan-to-value from a market risk analysis of the asset, and a thin asset may deserve a low limit or no listing. Caps also work as an emergency brake: Moonwell zeroed supply and borrow caps for wrsETH within the day.
Add circuit breakers and sanity bounds of your own
Do not rely on minAnswer and maxAnswer. Chainlink’s documentation says that on most Data Feeds these values are no longer used and do not stop your application from reading the latest answer, so you decide what limits are acceptable. Some feeds are deliberately bounded, and on a bounded feed a price outside the cap is not written, so latestRoundData() keeps returning the last in-range value. A freshness check then matters even more.
Avoid answeredInRound in new code. The API reference marks it deprecated, so older tutorials that compare answeredInRound with roundId are out of date.
Chainlink’s list of risk mitigations is a good template: circuit breakers that pause on an extreme price event, update delays until a recent fresh input arrives, a manual kill switch, monitoring and soak testing. The Moonwell cbETH case shows why a pause path has to be faster than governance: the fix needed a 5-day vote and liquidations ran meanwhile.
Compare against more than one source
For feeds that rely on a single source, Chainlink’s guidance is to compare the feed with independent references, enforce value bounds or caps and define fallback or pause behavior before securing value with it. We do not cover a specific two-oracle design here. A comparison that only logs a mismatch is monitoring. One that pauses borrowing is a circuit breaker. For perpetuals, where latency matters as much as manipulation, see oracle infrastructure for perpetuals DEXs. If your risk is whether a tokenized asset is backed rather than what it trades at, that is a different feed type: Chainlink Proof of Reserve.
A freshness check in code
The script below reads a Chainlink price feed with a plain eth_call, decodes the five return values and marks the answer stale if it is older than the heartbeat plus a buffer. It needs Node 22 or later (for built-in fetch, as the code comment says) and no packages. It compares updatedAt with the timestamp of the latest block, not the local machine’s clock. We ran it with Node 24.19.0 against Ethereum mainnet on 24 October 2026 using a public RPC endpoint, on the ETH/USD proxy, whose listed heartbeat is 3600 seconds.
// check-feed.mjs (Node 22+, built-in fetch only, no packages)
// Reads latestRoundData() from a Chainlink price feed proxy with a plain eth_call,
// decodes the five return values and flags the answer as stale if it is older than
// the feed's heartbeat plus a buffer.
//
// Usage: RPC_URL=https://ethereum-rpc.publicnode.com node check-feed.mjs
const RPC_URL = process.env.RPC_URL ?? "https://ethereum-rpc.publicnode.com";
// ETH/USD proxy on Ethereum mainnet, from docs.chain.link/data-feeds/price-feeds/addresses
const FEED = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419";
const HEARTBEAT = Number(process.env.HEARTBEAT ?? 3600); // seconds; 3600 is listed for this feed in the Chainlink docs
const BUFFER = Number(process.env.BUFFER ?? 300); // allowance for a late update (an assumption, tune it per feed)
// 4-byte function selectors (first 4 bytes of keccak256 of the signature)
const LATEST_ROUND_DATA = "0xfeaf968c"; // latestRoundData()
const DECIMALS = "0x313ce567"; // decimals()
async function rpc(method, params) {
const res = await fetch(RPC_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
});
const body = await res.json();
if (body.error) throw new Error(`${method}: ${body.error.message}`);
return body.result;
}
const call = (data) => rpc("eth_call", [{ to: FEED, data }, "latest"]);
// Each return value is one 32-byte word. int256 needs two's complement handling.
const word = (hex, i) => hex.slice(2 + i * 64, 2 + (i + 1) * 64);
const uint = (w) => BigInt("0x" + w);
const int = (w) => BigInt.asIntN(256, uint(w));
const [roundHex, decHex, block] = await Promise.all([
call(LATEST_ROUND_DATA),
call(DECIMALS),
rpc("eth_getBlockByNumber", ["latest", false]),
]);
const roundId = uint(word(roundHex, 0));
const answer = int(word(roundHex, 1));
const startedAt = uint(word(roundHex, 2));
const updatedAt = uint(word(roundHex, 3));
const answeredInRound = uint(word(roundHex, 4));
const decimals = Number(uint(word(decHex, 0)));
// Compare against the chain's own clock (latest block), not the local machine's clock.
const now = BigInt(block.timestamp);
const age = Number(now - updatedAt);
const limit = HEARTBEAT + BUFFER;
const whole = answer / 10n ** BigInt(decimals);
const frac = (answer % 10n ** BigInt(decimals)).toString().padStart(decimals, "0");
console.log(`feed ${FEED}`);
console.log(`roundId ${roundId}`);
console.log(`answer (raw) ${answer} (${decimals} decimals)`);
console.log(`answer ${whole}.${frac}`);
console.log(`startedAt ${startedAt} ${new Date(Number(startedAt) * 1000).toISOString()}`);
console.log(`updatedAt ${updatedAt} ${new Date(Number(updatedAt) * 1000).toISOString()}`);
console.log(`answeredInRound ${answeredInRound}`);
console.log(`block timestamp ${now} ${new Date(Number(now) * 1000).toISOString()}`);
console.log(`age ${age} s (limit ${limit} s = heartbeat ${HEARTBEAT} + buffer ${BUFFER})`);
const problems = [];
if (answer <= 0n) problems.push("answer is not positive");
if (updatedAt === 0n) problems.push("round not complete (updatedAt is 0)");
if (age > limit) problems.push(`STALE: ${age} s old, limit ${limit} s`);
if (problems.length) {
console.log("RESULT DO NOT USE: " + problems.join("; "));
process.exitCode = 1;
} else {
console.log("RESULT fresh: within heartbeat plus buffer");
}
The output of that run, with the answer replaced by a placeholder:
feed 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
roundId 129127208515966896273
answer (raw) <answer> (8 decimals)
answer <answer>
startedAt 1792852198 2026-10-24T14:29:58.000Z
updatedAt 1792852211 2026-10-24T14:30:11.000Z
answeredInRound 129127208515966896273
block timestamp 1792853819 2026-10-24T14:56:59.000Z
age 1608 s (limit 3900 s = heartbeat 3600 + buffer 300)
RESULT fresh: within heartbeat plus buffer
We checked it three ways. Running it with HEARTBEAT=300 BUFFER=0 on the same call printed DO NOT USE: STALE with an age of 1608 s against a limit of 300 s, and the script exited with code 1. A call with an invalid selector (0xdeadbeef) to the same address returned execution reverted, so the real call does reach the feed. Two other public endpoints (https://eth.drpc.org and https://ethereum.publicnode.com) returned the same round. We did not test the decoding of a negative int256 answer, so BigInt.asIntN is standard but unproven here.
Change HEARTBEAT per feed. The 300 second BUFFER is an assumption about late updates; in a sample of 41 recent rounds the longest gap was 3672 seconds, 72 seconds past the heartbeat, which is one sample and not a guarantee.
The same check on-chain, with the sequencer
The Solidity below is an excerpt of a 34-line contract (the pragma, import and constructor are left out). It checks the sequencer uptime feed and a grace period first, then the price feed’s answer and age. We compiled the full contract with solc 0.8.24 against AggregatorV3Interface from the contracts-v1.3.0 tag of Chainlink’s repository. It was not deployed and not run, so treat it as a starting point and test it on a fork before use.
contract SafeOracleRead {
AggregatorV3Interface public immutable priceFeed;
AggregatorV3Interface public immutable sequencerFeed; // address(0) on L1
uint256 public immutable maxAge; // heartbeat + buffer, in seconds
uint256 public constant GRACE_PERIOD = 3600;
error SequencerDown();
error GracePeriodNotOver();
error InvalidAnswer();
error StalePrice();
// constructor(feed, sequencer, maxAgeSeconds) omitted
function price() external view returns (uint256) {
if (address(sequencerFeed) != address(0)) {
(, int256 isDown, uint256 startedAt, , ) = sequencerFeed.latestRoundData();
if (isDown != 0) revert SequencerDown(); // 0 = up, 1 = down
if (block.timestamp - startedAt <= GRACE_PERIOD) revert GracePeriodNotOver();
}
(, int256 answer, , uint256 updatedAt, ) = priceFeed.latestRoundData();
if (answer <= 0) revert InvalidAnswer();
if (block.timestamp - updatedAt > maxAge) revert StalePrice();
return uint256(answer);
}
}
Three notes on it:
- For the uptime feed,
answer0 means the sequencer is up and 1 means down.startedAtis documented as the time the feed changed status. Aave’s production sentinel reads the fourth return value,updatedAt, for the same test, so check which field you copy. - On Arbitrum, Chainlink’s documentation says
startedAtreturns 0 when the Sequencer Uptime contract is not yet initialized. WithstartedAtat 0 the code above would see a very long time since the sequencer came up and let the call pass. An Arbitrum deployment should rejectstartedAt == 0. - It does not handle decimals or choose
maxAgefor you. Readdecimals()from the feed and setmaxAgeper feed.
A checklist for protocol teams
- Never price an asset from one pool’s spot price.
- Know which category Chainlink gives each feed you use, and treat an uncategorised feed as unevaluated.
- Set
maxAgeper feed from its heartbeat plus a margin, and revert or pause when it is exceeded. - Check the L2 Sequencer Uptime Feed page for your network. Where a feed exists, add a grace period. Where none exists, write down your fallback.
- Add your own price bounds. Do not depend on
minAnswerormaxAnswer. - Cap supply and borrow of thin or new collateral, and set loan-to-value from a market risk analysis.
- Test every exchange-rate cap against the current valid rate and against the longest gap between updates it has to handle.
- Keep a pause path that does not wait for a multi-day governance vote.
- Check feed configuration (asset, base or quote,
decimals()) in review and again after every oracle change. - Treat a TWAP as a cost to the attacker, not a safe default, and know the liquidity behind it.
- Write the incident procedure before you need it: who pauses, how and who tells users.
Sources & References
All pages were opened on 24 October 2026 unless noted. Court and docket data were read the same day.
- Chainlink Education Hub, Market Manipulation vs. Oracle Exploits (last updated 26 March 2024)
- Chainlink Documentation, Selecting Quality Data Feeds
- Chainlink Documentation, Data Sources
- Chainlink Documentation, Developer Responsibilities
- Chainlink Documentation, Data Feeds overview
- Chainlink Documentation, Data Feeds API Reference
- Chainlink Documentation, L2 Sequencer Uptime Feeds
- Chainlink Documentation, Decentralized Data Model
- Chainlink Documentation, Price Feed Addresses
- Chainlink, AggregatorV3Interface.sol, tag contracts-v1.3.0
- Qin, Zhou, Livshits and Gervais, Attacking the DeFi Ecosystem with Flash Loans for Fun and Profit (arXiv:2003.03810v4)
- Harvest Finance, fUSDC/fUSDT Economic Attack, Oct 26 2020
- Immunefi, Hack Analysis: Cream Finance, Oct 2021
- Halborn, Explained: The CREAM Finance Hack (October 2021)
- Inverse Finance governance, proposal mills/139
- Rekt News, Inverse Finance
- United States v. Eisenberg, No. 23-cr-10 (AS), SDNY, Opinion and Order, Doc. 220, 23 May 2025
- CFTC, Press Release 8647-23, 9 January 2023
- CourtListener, docket search for the Second Circuit appeal, United States v. Eisenberg, No. 25-1782 (API query run 24 October 2026)
- Halborn, Explained: The Polter Finance Hack (November 2024)
- SolidityScan, Polter Finance Hack Analysis
- Halborn, Explained: The KiloEx Hack (April 2025)
- Ackee Blockchain Security, Inside the $7.5M KiloEx Hack
- Moonwell Governance Forum, wrsETH Oracle Malfunction 11/4/25
- Moonwell Governance Forum, MIP-X43 cbETH Oracle Incident Summary
- Aave Governance, Post-Mortem: Exchange Rate Misallignment on wstETH Core and Prime Instances
- Aave Governance, AAVE V3 Borrow Caps Methodology
- Aave, PriceOracleSentinel.sol and Aave documentation, Oracles
- Uniswap Blog, Uniswap v3 TWAP Oracles in Proof of Stake (27 October 2022)
- Ethereum mainnet read, 24 October 2026: ETH/USD proxy
0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419, public RPChttps://ethereum-rpc.publicnode.com.
Work with Matrixed.Link
Matrixed.Link operates Chainlink oracle infrastructure, validator nodes and full-stack blockchain infrastructure for protocols and institutions that need institutional-grade reliability. Official Chainlink node operator running Data Feeds, CRE, SVR and Proof of Reserve. ISO/IEC 27001:2022 certified. AAA-rated provider on StakingRewards.
Contact Matrixed.Link to discuss Chainlink oracle infrastructure for your protocol.