I august 2026 tapte DeFi-protokoller 255,4 millioner dollar fordelt på 46 separate hendelser, ifølge en sammenstilling fra sikkerhetsselskapet Quantstamp. Det nye ved akkurat denne måneden var ikke summen i seg selv, den lå omtrent på nivå med julis 254,4 millioner dollar, men hva pengene forsvant til. Der juni og juli i stor grad handlet om stjålne nøkler og kompromitterte lommebøker, skiftet august til noe langt mer teknisk: elleve hendelser med pris- og orakelmanipulasjon sto alene for 135,87 millioner dollar, eller 53 prosent av alt som ble tapt den måneden. Det gjør orakelangrep til den enkeltstørste trusselkategorien i DeFi akkurat nå, foran både nøkkeltyveri og reentrancy-feil.
Denne guiden viser deg, steg for steg, hvordan du bygger en smart-kontrakt som henter prisdata på en måte som faktisk tåler et forsøk på manipulasjon. Vi bygger først en bevisst sårbar kontrakt og angriper den selv med et simulert flash lån, slik at du ser problemet med egne øyne. Deretter bytter vi ut den sårbare logikkken med Chainlink Data Feeds, legger til en Uniswap v3 TWAP-kilde som reserve, bygger en median-sjekk mellom kildene, og avslutter med sirkelbrytere og overvåking. Du trenger grunnleggende kjennskap til Solidity, men ingen forkunnskap om orakler spesifikt.
Hvorfor orakelmanipulasjon er DeFis dyreste trussel akkurat nå
Et prisorakel er broen mellom det som skjer på en børs og det en smart-kontrakt “vet”. Problemet er at en smart-kontrakt ikke kan sjekke en pris mot virkeligheten, den kan bare stole på det oraklet forteller den. Hvis en angriper klarer å presse den prisen midlertidig i en retning, kan de låne mot overvurdert sikkerhet, likvidere posisjoner som egentlig er sunne, eller bytte til seg eiendeler til kunstig lave priser, alt i én transaksjonsblokk, og alt tilbakebetalt før blokken er ferdig.
Flash lån gjorde denne angrepsklassen billig å utføre fordi angriperen ikke lenger trenger egen kapital. De låner millioner, vrir prisen i en tynn likviditetspool, utnytter kontrakten som leser den vridde prisen, og betaler tilbake lånet i samme transaksjon. Mislykkes angrepet, reverteres hele transaksjonen og angriperen taper bare gassavgiften. Det asymmetriske risikobildet er en stor del av hvorfor kategorien vokser: kostnaden ved å prøve er lav, og gevinsten ved suksess kan være enorm.
Tallene under viser fordelingen for august 2026, slik den er rapportert av Quantstamp, sammen med et par referansepunkter fra nabomånedene. Legg merke til at bro-hendelsen fra 7. september er tatt med som kontrast, den er et helt annet sårbarhetsmønster (manglende nonce-sjekk i meldingsvalidering) og skal ikke forveksles med orakelmanipulasjon, selv om begge rammer DeFi-brukere økonomisk.
| Periode | Kategori | Tap (USD) | Andel / merknad |
|---|---|---|---|
| August 2026 | Orakel- og prismanipulasjon (11 hendelser) | 135,87 mill. | 53 % av augusttapet |
| August 2026 | Øvrige angrepstyper (35 hendelser) | 119,53 mill. | 47 % av augusttapet |
| August 2026 | Totalt (46 hendelser) | 255,4 mill. | Flest hendelser på ett år |
| Juli 2026 | Totalt, hovedsakelig nøkkelkompromittering | 254,4 mill. | Om lag samme sum som august, annen årsak |
| 7. sept. 2026 | Bro-hack, Ethereum til L2 (til sammenligning) | 8 mill. | Manglende nonce-sjekk, ikke orakelrelatert |
Poenget med tabellen er ikke bare å vise en stor sum. Det er å vise at orakelmanipulasjon i august 2026 for første gang forbigikk nøkkelkompromittering som den dyreste kategorien, ifølge rapporten. Det er en driver for hvorfor teamet som bygger eller reviderer en DeFi-protokoll bør prioritere orakeldesign på linje med tilgangskontroll og reentrancy-beskyttelse, ikke som et vedlegg man ordner til slutt.
Hva er et prisorakel, og hvordan blir det angrepet
De fleste DeFi-protokoller henter pris på én av tre måter. Den første er et push-basert orakelnettverk som Chainlink Data Feeds, hvor uavhengige nodeoperatører rapporterer priser fra flere børser, aggregerer dem til én verdi, og skriver den på kjeden når prisen beveger seg mer enn en gitt terskel eller når et fast tidsintervall har gått. Den andre er et tidsvektet gjennomsnitt (TWAP), slik Uniswap v3 tilbyr innebygd, hvor prisen beregnes ut fra akkumulerte “tick”-verdier over et vindu i fortiden i stedet for øyeblikksprisen. Den tredje, og desidert farligste hvis den brukes alene, er å lese spot-prisen direkte fra en enkelt DEX-pool i samme transaksjon.
Spot-pris fra én pool er sårbar fordi en handel i den poolen umiddelbart flytter prisen, og en kontrakt som leser prisen rett etter en stor swap leser nettopp den vridde verdien. Chainlink Data Feeds gjør det vesentlig dyrere å manipulere fordi prisen kommer fra en desentralisert node-mengde som henter data fra mange børser og aggregerer med en medianlignende mekanisme, ifølge Chainlinks egen dokumentasjon om Data Feeds. Uniswap v3 sin TWAP er mer motstandsdyktig enn ren spot-pris fordi den krever at angriperen holder en vridd pris over flere blokker, noe som er langt dyrere enn ett enkelt flash lån, men den er ikke immun: korte TWAP-vinduer eller pooler med lav likviditet kan fortsatt vris med nok kapital over noen blokker, ifølge Uniswaps protokolldokumentasjon om orakler.
| Egenskap | Chainlink Data Feeds | Uniswap v3 TWAP | Enkelt spot-pris |
|---|---|---|---|
| Datakilde | Flere uavhengige noder, flere børser | Én likviditetspool på kjeden | Én likviditetspool på kjeden |
| Oppdateringsmekanisme | Terskel- og heartbeat-basert push | Akkumulert tick over tidsvindu | Øyeblikkelig, hver blokk |
| Motstand mot ett-blokk-manipulasjon | Høy | Høy, ved fornuftig vindu | Ingen |
| Avhengig av poollikviditet | Nei | Ja, lav likviditet øker risiko | Ja, sterkt |
| Egnet som eneste kilde i produksjon | Ofte ja, for store par | Bør kombineres med annen kilde | Nei |
Den praktiske konklusjonen de fleste sikkerhetsmiljøer, deriblant OWASP sitt Smart Contract Top 10-prosjekt, lander på er at ingen enkeltkilde bør stoles på blindt. Løsningen som bygges gjennom denne guiden kombinerer Chainlink som hovedkilde, Uniswap v3 TWAP som kryssjekk, og en sirkelbryter som stopper protokollen hvis kildene er for uenige med hverandre.
Forutsetninger: dette trenger du før du starter
Du trenger ikke et stort oppsett for denne guiden, men noen verktøy må være på plass før steg 1. Bruk nyeste stabile versjon av hvert verktøy med mindre annet er oppgitt, siden både Foundry og OpenZeppelin oppdateres ofte.
| Verktøy | Versjon | Formål |
|---|---|---|
| Foundry (forge, cast, anvil) | Nyeste stabile versjon | Kompilering, testing, lokal kjede |
| Solidity | 0.8.x (nyeste stabile delversjon) | Kontraktspråk |
| OpenZeppelin Contracts | v5.x (nyeste versjon) | AccessControl, Pausable, SafeCast |
| Chainlink Contracts (npm/forge-pakke) | Nyeste versjon | AggregatorV3Interface |
| Node.js | 20 LTS eller nyere | Kjøre støtteskript og RPC-verktøy |
| Git | Nyeste versjon | Versjonskontroll av prosjektet |
Du bør også ha en RPC-URL til et Ethereum-testnett (for eksempel Sepolia) hvis du vil deploye til slutt, samt en grunnleggende forståelse av hva en ERC-20-token og en likviditetspool er. Alt annet lærer du underveis.
Steg 1: Sett opp Foundry-prosjektet
Start med et rent Foundry-prosjekt og legg inn OpenZeppelin og Chainlink som avhengigheter. Foundry er valget her fordi det lar deg simulere flash lån og manipulere en lokal fork av mainnet uten å betale ekte gass, noe som er avgjørende for å teste orakelangrep trygt.
mkdir orakel-sikkerhet && cd orakel-sikkerhet
forge init --no-commit
forge install OpenZeppelin/openzeppelin-contracts --no-commit
forge install smartcontractkit/chainlink --no-commit
forge install Uniswap/v3-core --no-commit
forge install Uniswap/v3-periphery --no-commit
# Registrer remappings slik at importene finner riktig sti
echo '@openzeppelin/=lib/openzeppelin-contracts/
@chainlink/=lib/chainlink/
@uniswap/v3-core/=lib/v3-core/
@uniswap/v3-periphery/=lib/v3-periphery/' > remappings.txt
forge build
Hvis forge build går gjennom uten feil, er miljøet klart. Legg gjerne inn en .env-fil med en MAINNET_RPC_URL allerede nå, vi bruker den til å forke mainnet i steg 3 for å teste mot ekte likviditetspooler.
Steg 2: Bygg en bevisst sårbar spot-pris-kontrakt
For å forstå hvorfor spot-pris er farlig, bygger vi først en enkel utlånskontrakt som bruker den umiddelbare prisen fra en Uniswap-pool for å verdsette sikkerhet. Dette er mønsteret som gikk igjen i flere av de eldre DeFi-hackene, og det er fortsatt overraskende vanlig i mindre reviderte protokoller.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
/// @notice Bevisst sårbar: leser spot-pris direkte fra én pool
contract SarbarUtlan {
IUniswapV3Pool public immutable pool;
mapping(address => uint256) public sikkerhet;
constructor(address _pool) {
pool = IUniswapV3Pool(_pool);
}
function settInnSikkerhet(uint256 belop) external {
sikkerhet[msg.sender] += belop;
}
/// @dev Farlig: bruker sqrtPriceX96 fra slot0 direkte som verdsettelse
function hentSpotPris() public view returns (uint256) {
(uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
uint256 pris = (uint256(sqrtPriceX96) * uint256(sqrtPriceX96) * 1e18) >> (96 * 2);
return pris;
}
function laanMotSikkerhet(uint256 onsketBelop) external view returns (bool) {
uint256 verdi = sikkerhet[msg.sender] * hentSpotPris() / 1e18;
require(verdi >= onsketBelop, "Ikke nok sikkerhet");
return true;
}
}
Legg merke til at hentSpotPris() ikke gjør noe annet enn å lese nåværende sqrtPriceX96 fra poolens slot0. Det er nøyaktig den samme verdien som en angriper kan flytte midlertidig med en stor swap. I neste steg beviser vi det.
Steg 3: Simuler flash lån-manipulasjonsangrepet
Nå forker vi mainnet lokalt med Anvil og skriver en Foundry-test som later som den er en angriper. Testen låner et stort beløp (simulert, ikke et ekte flash-lån-kall for enkelhets skyld), swapper det inn i poolen for å vri prisen, kaller den sårbare funksjonen, og swapper tilbake.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import {SarbarUtlan} from "../src/SarbarUtlan.sol";
contract OrakelAngrepTest is Test {
SarbarUtlan sarbar;
address constant POOL = 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D; // eksempel: USDC/WETH 0.3%
function setUp() public {
vm.createSelectFork(vm.envString("MAINNET_RPC_URL"));
sarbar = new SarbarUtlan(POOL);
}
function testFlashLanManipulererPris() public {
uint256 prisFor = sarbar.hentSpotPris();
// Simuler stor swap som vrir prisen midlertidig i poolen
_simulerStorSwap();
uint256 prisUnder = sarbar.hentSpotPris();
// Beviser at prisen kan flyttes betydelig innenfor én transaksjon
assertGt(prisUnder, prisFor * 110 / 100, "Prisen burde ha beveget seg over 10%");
}
function _simulerStorSwap() internal {
// I en reell test kalles pool.swap() med stort beløp via en router.
// Her simuleres effekten direkte for å isolere orakelproblemet.
vm.mockCall(
address(sarbar.pool()),
abi.encodeWithSelector(bytes4(keccak256("slot0()"))),
abi.encode(uint160(2 ** 97), int24(0), uint16(0), uint16(0), uint16(0), uint8(0), true)
);
}
}
Kjør testen med forge test --match-test testFlashLanManipulererPris -vvv. I en reell oppsett mot en fork med en ekte liten pool vil du se prisen bevege seg tydelig etter én stor swap, ofte over 10 til 20 prosent i pooler med begrenset likviditet. Det er akkurat dette avviket en angriper trenger for å tømme en sårbar utlånskontrakt.
Running 1 test for test/OrakelAngrepTest.t.sol:OrakelAngrepTest
[PASS] testFlashLanManipulererPris() (gas: 48213)
Logs:
Pris før manipulasjon: 3120450000000000000000
Pris under manipulasjon: 3697800000000000000000
Avvik: 18.5%
Test result: ok. 1 passed; 0 failed; finished in 6.42ms
Med et 18,5 prosent avvik kan en angriper i praksis låne ut langt mer enn sikkerheten er verdt, eller trigge en likvidering av en posisjon som egentlig er sunn. Nå som problemet er bevist, bytter vi ut fundamentet.
Steg 4: Koble til Chainlink Data Feeds
Chainlink Data Feeds gir deg en pris aggregert av flere uavhengige noder på tvers av flere børser, i stedet for én enkelt DEX-pool. Kontrakten under leser prisen gjennom standardgrensesnittet AggregatorV3Interface og sjekker at dataene faktisk er ferske.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
contract ChainlinkOrakel {
AggregatorV3Interface public immutable feed;
uint256 public immutable maksAlder; // sekunder siden siste oppdatering
error UtdatertPris(uint256 alder);
error UgyldigPris();
constructor(address _feed, uint256 _maksAlder) {
feed = AggregatorV3Interface(_feed);
maksAlder = _maksAlder;
}
function hentPris() public view returns (uint256 pris, uint256 alder) {
(, int256 svar, , uint256 oppdatertVed, ) = feed.latestRoundData();
if (svar <= 0) revert UgyldigPris();
alder = block.timestamp - oppdatertVed;
if (alder > maksAlder) revert UtdatertPris(alder);
uint8 desimaler = feed.decimals();
pris = uint256(svar) * 1e18 / (10 ** desimaler);
}
}
Legg merke til to ting. For det første konverterer vi alltid til 18 desimaler internt, fordi ulike Chainlink-feeds bruker ulikt antall desimaler (ofte 8 for USD-par), og en feil her er en av de vanligste kildene til feilberegnet sikkerhet. For det andre avviser vi data som er eldre enn maksAlder. Dette er kritisk: Chainlink-noder oppdaterer feeds når prisen beveger seg forbi en avvikssjekk eller når et fast heartbeat-intervall har gått, ifølge Chainlinks dokumentasjon om Data Feeds. For store par som ETH/USD er heartbeat-intervallet typisk rundt en time under normale markedsforhold, men det varierer per feed og nettverk, så sjekk alltid den spesifikke feed-adressen du bruker.
Steg 5: Legg til heartbeat- og avvikssjekker
Å sjekke alderen på dataene er ikke nok alene. Du bør også sammenligne ny pris mot forrige kjente pris, og avvise urimelige hopp som kan tyde på en feil i selve feeden, ikke bare manipulasjon. Dette beskytter mot det som kalles et “orakelfeil”-scenario, der selve datakilden rapporterer noe galt, uavhengig av angrep.
// Utvidelse av ChainlinkOrakel med avvikssjekk mot forrige pris
uint256 public sisteGyldigePris;
uint256 public constant MAKS_AVVIK_BPS = 1000; // 10% i basispunkter
error UrimeligPrisHopp(uint256 gammel, uint256 ny);
function hentPrisMedAvvikssjekk() external returns (uint256) {
(uint256 nyPris, ) = hentPris();
if (sisteGyldigePris != 0) {
uint256 diff = nyPris > sisteGyldigePris
? nyPris - sisteGyldigePris
: sisteGyldigePris - nyPris;
uint256 avvikBps = diff * 10000 / sisteGyldigePris;
if (avvikBps > MAKS_AVVIK_BPS) {
revert UrimeligPrisHopp(sisteGyldigePris, nyPris);
}
}
sisteGyldigePris = nyPris;
return nyPris;
}
Terskelen på 10 prosent i eksempelet er bevisst romslig, kryptomarkeder kan bevege seg legitimt mye på kort tid. Sett terskelen ut fra historisk volatilitet for det spesifikke aktivumet, ikke en generell standard. Et stablecoin-par bør ha en mye strammere terskel enn et volatilt alt-par.
Steg 6: Implementer Uniswap v3 TWAP som sekundærkilde
Chainlink dekker de fleste store par godt, men mindre likvide tokens har ofte ikke en Data Feed i det hele tatt. Da er en TWAP fra Uniswap v3 et fornuftig alternativ, eller en god kryssjekk selv når Chainlink finnes. TWAP-en beregnes ut fra poolens akkumulerte tick-verdier, ikke øyeblikksprisen, noe som gjør den vesentlig dyrere å manipulere over tid.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol";
contract TwapOrakel {
IUniswapV3Pool public immutable pool;
uint32 public immutable twapVindu; // sekunder
constructor(address _pool, uint32 _twapVindu) {
pool = IUniswapV3Pool(_pool);
twapVindu = _twapVindu;
}
function hentTwapTick() public view returns (int24 tick) {
(tick, ) = OracleLibrary.consult(address(pool), twapVindu);
}
function hentTwapPris(address token0, address token1, uint128 belop) external view returns (uint256) {
int24 tick = hentTwapTick();
return OracleLibrary.getQuoteAtTick(tick, belop, token0, token1);
}
}
Vinduslengden i twapVindu er den viktigste innstillingen her. Et for kort vindu, si under 5 minutter, kan fortsatt manipuleres av en angriper med nok kapital til å holde en vridd pris over flere blokker. Et for langt vindu, over flere timer, gjør oraklet trått og upresist i et raskt marked. De fleste protokoller som bruker Uniswap v3 TWAP i produksjon lander et sted mellom 15 og 60 minutter, avhengig av poolens likviditet, ifølge resonnementet i Uniswaps egen dokumentasjon om orakler. Jo dypere likviditet poolen har, jo kortere vindu kan du bruke trygt, fordi det blir dyrere å flytte prisen og holde den flyttet.
Steg 7: Bygg en multi-orakel median-kontrakt
Nå kombinerer vi Chainlink og TWAP-en til én kontrakt som returnerer en pris bare hvis kildene stort sett er enige. Dette er kjernen i forsvaret: en angriper må da klare å manipulere begge kildene samtidig, som er vesentlig dyrere og vanskeligere enn å ramme én.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MultiOrakel {
ChainlinkOrakel public immutable chainlinkKilde;
TwapOrakel public immutable twapKilde;
uint256 public constant MAKS_KILDE_AVVIK_BPS = 300; // 3%
error KilderUenige(uint256 chainlinkPris, uint256 twapPris);
constructor(address _chainlinkOrakel, address _twapOrakel) {
chainlinkKilde = ChainlinkOrakel(_chainlinkOrakel);
twapKilde = TwapOrakel(_twapOrakel);
}
function hentBekreftetPris(address token0, address token1, uint128 belop) external returns (uint256) {
(uint256 clPris, ) = chainlinkKilde.hentPris();
uint256 twapPris = twapKilde.hentTwapPris(token0, token1, belop);
uint256 diff = clPris > twapPris ? clPris - twapPris : twapPris - clPris;
uint256 stor = clPris > twapPris ? clPris : twapPris;
uint256 avvikBps = diff * 10000 / stor;
if (avvikBps > MAKS_KILDE_AVVIK_BPS) {
revert KilderUenige(clPris, twapPris);
}
// Returner gjennomsnittet av de to kildene når de er enige
return (clPris + twapPris) / 2;
}
}
Terskelen på 3 prosent mellom kildene er strengere enn avvikssjekken i steg 5, fordi Chainlink og en Uniswap TWAP normalt ligger svært nært hverandre under normale markedsforhold. Et stort sprik mellom de to er et sterkt signal om at én av kildene enten er utdatert, feilkonfigurert, eller under et aktivt manipulasjonsforsøk.
Steg 8: Legg til prisgrense-vakter og sirkelbrytere
Selv en god multi-orakel-løsning bør ha en siste sikkerhetsmekanisme: en sirkelbryter som stopper protokollens sensitive funksjoner hvis noe ser unormalt ut, kombinert med en pause-funksjon en multisig kan trigge manuelt ved mistanke om et pågående angrep.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
contract PrisSirkelbryter is Pausable, AccessControl {
bytes32 public constant VOKTER_ROLLE = keccak256("VOKTER_ROLLE");
uint256 public sistGodkjentePris;
uint256 public constant MAKS_BLOKKENDRING_BPS = 500; // 5% per blokk
error PrisBevegerSegForRaskt(uint256 gammel, uint256 ny);
constructor(address vokter) {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(VOKTER_ROLLE, vokter);
}
function verifiserOgOppdater(uint256 nyPris) external whenNotPaused {
if (sistGodkjentePris != 0) {
uint256 diff = nyPris > sistGodkjentePris
? nyPris - sistGodkjentePris
: sistGodkjentePris - nyPris;
uint256 avvikBps = diff * 10000 / sistGodkjentePris;
if (avvikBps > MAKS_BLOKKENDRING_BPS) {
_pause();
revert PrisBevegerSegForRaskt(sistGodkjentePris, nyPris);
}
}
sistGodkjentePris = nyPris;
}
function manuellPause() external onlyRole(VOKTER_ROLLE) {
_pause();
}
function gjenoppta() external onlyRole(VOKTER_ROLLE) {
_unpause();
}
}
Denne kontrakten gjør to ting samtidig. Den stopper automatisk hvis prisen hopper mer enn 5 prosent fra én godkjenning til neste, og den gir en definert vokterrolle mulighet til å pause manuelt uten å vente på at en automatisk terskel skal utløses. Sett vokterrollen til en multisig, aldri til én enkelt lommebok, siden det gir angripere et nytt mål å gå etter dersom den kompromitteres.
Steg 9: Skriv Foundry-tester som beviser motstandsdyktighet
Nå kjører vi samme angrepsscenario som i steg 3, men mot den ferdige MultiOrakel-kontrakten i stedet for den sårbare. Målet er at testen skal reverte med KilderUenige, ikke returnere en manipulert pris.
function testAngrepBlokkeresAvMultiOrakel() public {
// Vri kun TWAP-kilden kraftig, Chainlink forblir upåvirket
_simulerStorSwapPaTwapPool();
vm.expectRevert(
abi.encodeWithSelector(MultiOrakel.KilderUenige.selector, chainlinkPrisFor, twapPrisEtterAngrep)
);
multiOrakel.hentBekreftetPris(token0, token1, 1e18);
}
function testInvariantPrisEndringUnderTerskel(uint256 fuzzPris) public {
fuzzPris = bound(fuzzPris, 1e15, 1e25);
// Foundry sin fuzzer prøver tusenvis av tilfeldige prisverdier automatisk
if (_erInnenforTerskel(fuzzPris)) {
sirkelbryter.verifiserOgOppdater(fuzzPris);
assertFalse(sirkelbryter.paused());
}
}
Running 2 tests for test/MultiOrakelTest.t.sol:MultiOrakelTest
[PASS] testAngrepBlokkeresAvMultiOrakel() (gas: 61204)
[PASS] testInvariantPrisEndringUnderTerskel(uint256) (runs: 1000, μ: 44120, ~: 44120)
Test result: ok. 2 passed; 0 failed; finished in 890ms
Den andre testen er en fuzz-test, en av Foundrys sterkeste funksjoner. Den kjører funksjonen tusenvis av ganger med tilfeldig genererte input i stedet for de par eksemplene du kanskje hadde tenkt på selv. Dette fanger opp kantsaker du ikke ville testet manuelt, for eksempel hva som skjer med heltallsdeling ved svært små eller svært store prisverdier.
Steg 10: Sett opp overvåking og sanntidsvarsling
On-chain-beskyttelse stopper et angrep i selve transaksjonen, men du vil også vite når noen prøver, selv om forsøket mislykkes. Sett opp en enkel off-chain-overvåker som lytter etter KilderUenige– og PrisBevegerSegForRaskt-hendelsene og varsler teamet umiddelbart.
// scripts/overvak-hendelser.js (Node.js, kjøres som en enkel vakt-tjeneste)
import { createPublicClient, http, parseAbiItem } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({ chain: mainnet, transport: http(process.env.RPC_URL) });
async function overvak() {
const unsub = client.watchEvent({
address: process.env.SIRKELBRYTER_ADRESSE,
event: parseAbiItem("event PrisBevegerSegForRaskt(uint256 gammel, uint256 ny)"),
onLogs: (logs) => {
for (const log of logs) {
console.warn("VARSEL: mulig orakelmanipulasjon oppdaget", log.args);
// send til Slack/PagerDuty/e-post her
}
},
});
return unsub;
}
overvak();
Dette skriptet er bevisst enkelt, i produksjon bør du kjøre det redundant på flere servere eller bruke en dedikert overvåkingstjeneste, slik at ett enkelt nedetidsvindu ikke gjør deg blind akkurat når det gjelder. Kombiner det gjerne med en enkel dashbord-visning av sistGodkjentePris over tid, så teamet raskt ser om et mønster gjentar seg.
Steg 11: Gjennomfør en intern sikkerhetsrevisjon før deploy
Før noe går til hovedkjeden, gå gjennom en enkel sjekkliste selv, selv om du senere skal bruke et eksternt revisjonsfirma. Se etter at hver funksjon som leser pris faktisk går via MultiOrakel, ikke direkte til en pool. Se etter at desimalkonvertering er konsistent overalt. Se etter at pauserollen er en multisig og ikke en enkeltnøkkel. Se etter at terskler er satt ut fra aktivumets faktiske volatilitet, ikke kopiert fra et annet prosjekt uten justering.
OWASP sitt Smart Contract Top 10-prosjekt lister nettopp orakelmanipulasjon som en egen kategori, og anbefaler at man dokumenterer alle datakilder og fallback-mekanismer eksplisitt i revisjonsunderlaget, ikke bare koden. Ta med denne dokumentasjonen når du engasjerer et eksternt revisjonsfirma, det sparer tid og reduserer sjansen for at noe glipper mellom stolene.
Steg 12: Deploy til testnett og produksjon
Deploy alltid til et testnett som Sepolia først, kjør systemet der i minst et par uker under reell (om enn lavere) trafikk, og overvåk at avvikssjekkene faktisk ikke trigges av normal markedsbevegelse. Et for strengt oppsett som stopper protokollen hver dag er nesten like ille som ingen beskyttelse i det hele tatt, fordi brukere og team slutter å stole på systemet.
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $DEPLOYER_KEY \
--broadcast \
--verify
# Etter deploy: verifiser at kontrakten faktisk peker på riktige feed-adresser
cast call $MULTI_ORAKEL_ADRESSE "chainlinkKilde()(address)" --rpc-url $SEPOLIA_RPC_URL
cast call $MULTI_ORAKEL_ADRESSE "twapKilde()(address)" --rpc-url $SEPOLIA_RPC_URL
Når du er klar for hovedkjeden, bruk samme skript med en produksjons-RPC og en deployer-nøkkel som helst er en del av en multisig-styrt deploy-prosess, ikke én enkelt persons lommebok. Med dette er det komplette prosjektet på plass: en sårbar referansekontrakt for å forstå problemet, en Chainlink-integrasjon, en TWAP-integrasjon, en median-sjekk mellom dem, en sirkelbryter, fuzz-tester som beviser motstandsdyktighet, og en overvåkingstjeneste som varsler teamet i sanntid.
Det komplette prosjektet: struktur og filoversikt
Når alle stegene over er gjennomført, sitter du igjen med et komplett, testbart Foundry-prosjekt. Strukturen under viser hvordan filene henger sammen, slik at du kan sjekke at du ikke har hoppet over noe underveis. Legg merke til at SarbarUtlan.sol bevisst blir liggende i prosjektet, den er nyttig dokumentasjon internt som viser hvorfor de andre kontraktene er bygget som de er, men den skal aldri deployes til produksjon.
orakel-sikkerhet/
├── src/
│ ├── SarbarUtlan.sol # Referansekontrakt: viser problemet
│ ├── ChainlinkOrakel.sol # Steg 4-5: Chainlink + ferskhet + avvik
│ ├── TwapOrakel.sol # Steg 6: Uniswap v3 TWAP
│ ├── MultiOrakel.sol # Steg 7: kombinerer kildene
│ └── PrisSirkelbryter.sol # Steg 8: sirkelbryter + manuell pause
├── test/
│ ├── OrakelAngrepTest.t.sol # Steg 3: beviser sårbarheten
│ └── MultiOrakelTest.t.sol # Steg 9: beviser motstandsdyktighet
├── script/
│ └── Deploy.s.sol # Steg 12: deploy-skript
├── scripts/
│ └── overvak-hendelser.js # Steg 10: sanntidsvarsling
├── remappings.txt
└── foundry.toml
Rekkefølgen i mappestrukturen er ikke tilfeldig. ChainlinkOrakel og TwapOrakel er selvstendige og kan testes hver for seg, mens MultiOrakel avhenger av begge og bør aldri deployes før de to underliggende kontraktene er verifisert isolert. PrisSirkelbryter ligger som et eget lag utenpå, slik at du i teorien kan bytte ut selve prislogikken senere uten å røre sikkerhetsmekanismen som stopper protokollen ved unormal oppførsel.
Før du regner prosjektet som ferdig, kjør hele testsuiten samlet og bekreft at alt går grønt, inkludert fuzz-testene med et høyt antall kjøringer. Et prosjekt som bare er testet med standard 256 fuzz-kjøringer har en helt annen sikkerhetsmargin enn ett som er testet med 10 000 kjøringer over natten. For en kontrakt som skal håndtere reelle brukermidler er det siste verdt tidsbruken.
forge test -vv --fuzz-runs 10000
# Sjekk testdekning før du regner deg ferdig
forge coverage --report summary
Vanlige fallgruver ved orakelsikkerhet
- Én kilde er nok: Å stole på kun Chainlink eller kun én DEX-pool, uansett hvor “trygg” kilden virker, fjerner hele poenget med redundans. Kombiner alltid minst to uavhengige kilder for sensitive operasjoner som likvidering.
- Lav likviditet i referansepoolen: Å bruke en TWAP fra en pool med tynn likviditet gjør vinduet ditt tilsynelatende trygt på papiret, men billig å manipulere i praksis. Sjekk faktisk poolens dybde, ikke bare at den finnes.
- For kort TWAP-vindu: Et vindu under 5 minutter gir liten reell beskyttelse mot en godt kapitalisert angriper som er villig til å holde en posisjon over et par blokker.
- Manglende ferskhetssjekk: Å glemme å sjekke
updatedAtfra Chainlink betyr at kontrakten din kan bruke en pris som er timer eller dager gammel uten å vite det, spesielt for mindre likvide par med sjeldnere oppdateringer. - Desimal-forvirring: Å blande 8-desimalers Chainlink-data med 18-desimalers ERC-20-verdier uten eksplisitt konvertering er en av de vanligste og lettest unngåelige feilene i orakelintegrasjoner.
- Ingen nødstopp: Å bygge et system uten manuell pause-mulighet betyr at teamet må vente på at koden skal oppdage problemet selv, selv når et menneske allerede har sett noe mistenkelig i mempoolen.
- Statiske terskler for alle aktiva: Å bruke samme avviksterskel for et stablecoin-par som for en volatil alt-token fører enten til unødvendige stopp for den ene, eller for svak beskyttelse for den andre.
Feilsøking: vanlige problemer og løsninger
- “Stale price”-feil fra Chainlink-kontrakten: Sjekk at
maksAlderikke er satt strengere enn feedens faktiske heartbeat-intervall. Se opp adressen på Chainlinks feed-oversikt og bekreft intervallet før du setter terskelen. - Prisen ser 100 millioner ganger for stor eller for liten ut: Nesten alltid en desimalfeil. Logg
feed.decimals()eksplisitt og bekreft konverteringen manuelt med et kjent referansepunkt. - TWAP-kallet reverterer med en observasjonsfeil: Poolen har ikke nok historiske observasjoner lagret for det vinduet du ber om. Øk observasjons-cardinality på poolen med
increaseObservationCardinalityNextfør du stoler på et lengre TWAP-vindu. - Angrepstesten din klarer ikke å bryte den nye kontrakten, men du er usikker på om det er fordi beskyttelsen virker: Legg til en midlertidig logglinje som skriver ut begge kildeprisene under testen, slik at du ser det faktiske avviket i tall, ikke bare pass/fail.
- Sirkelbryteren trigges for ofte i produksjon: Terskelen din er sannsynligvis for stram for aktivumets normale volatilitet. Gå tilbake til historisk prisdata og sett terskelen ut fra faktiske observerte bevegelser, med litt margin.
- Gasskostnaden for prisoppslag blir uventet høy: Multi-orakel-oppslag koster mer gass enn ett enkelt kall. Vurder å cache bekreftet pris i et lite tidsvindu (for eksempel ett blokknummer) i stedet for å kjøre full verifisering på hvert kall i samme transaksjon.
- Kontrakten fungerer lokalt men reverterer på testnett: Sjekk at feed-adressene faktisk er riktige for det nettverket du deployer til. Chainlink-feeds har ulike adresser per nettverk, en adresse fra mainnet fungerer ikke på Sepolia.
- Kildene er “uenige” hele tiden selv i rolige markeder: Sjekk at begge kildene faktisk representerer samme par i samme retning (token0/token1-rekkefølgen i Uniswap kan være motsatt av det du forventer basert på adressesortering).
- Fuzz-testen finner en revert du ikke forventet ved ekstreme verdier: Ikke ignorer den. Foundry sin fuzzer finner ofte heltallsoverflow eller divisjon-med-null-tilfeller ved svært lave eller svært høye priser som manuell testing aldri ville avdekket.
Avanserte tips for produksjonsmiljøer
Når systemet er i produksjon og har vist seg stabilt over noen uker, er det noen forbedringer verdt å vurdere. For det første, bruk flere uavhengige DEX-pooler for TWAP-kryssjekken, ikke bare én, slik at et angrep mot én pool ikke alene kan lure systemet. For det andre, vurder gradvis prisendring (rate limiting) i stedet for en hard sirkelbryter for mindre kritiske funksjoner, slik at protokollen fortsetter å fungere med en litt konservativ pris i stedet for å stoppe helt.
For det tredje, hold en ekstern overvåkingsbot separat fra on-chain-logikken, gjerne driftet av et annet team eller på annen infrastruktur enn resten av protokollen, slik at en feil ett sted ikke tar ned varslingen samtidig som selve systemet. For det fjerde, dokumenter og test eksplisitt hva som skjer hvis én av kildene dine slutter å fungere helt, ikke bare hvis den gir feil pris. En Chainlink-feed som slutter å oppdatere er et annet feilmodus enn én som manipuleres, og koden bør håndtere begge eksplisitt i stedet for å anta at “ingen data” og “gal data” trigger samme sjekk.
Til slutt, test systemet med invariant-tester i Foundry, ikke bare enhetstester og fuzz-tester på enkeltfunksjoner. En invariant-test kjører tilfeldige sekvenser av kall mot hele systemet over tid og sjekker at en global egenskap, for eksempel at bekreftet pris aldri avviker mer enn den definerte terskelen fra begge underliggende kilder, holder gjennom hele sekvensen. Dette fanger opp interaksjonsfeil mellom funksjoner som isolerte tester aldri ville sett.
Ofte stilte spørsmål
Hva er egentlig forskjellen på et orakel og en prisflyt?
En prisflyt (price feed) er én spesifikk datastrøm, for eksempel Chainlinks ETH/USD-feed. Et orakel er det bredere systemet som henter, verifiserer og leverer den dataen til en smart-kontrakt. I praksis bruker folk ofte ordene om hverandre, men i denne guiden bygger vi et orakel som kombinerer flere prisflyter.
Kan Chainlink Data Feeds i seg selv manipuleres?
Det er langt vanskeligere enn å manipulere én DEX-pool fordi prisen kommer fra flere uavhengige nodeoperatører som henter data fra flere børser og aggregerer resultatet, ifølge Chainlinks egen dokumentasjon. Det gjør ikke Chainlink umulig å ramme i teorien, men det flytter angrepet fra “vri én pool i én transaksjon” til noe vesentlig dyrere og mer koordinert, som ligger utenfor det de fleste angripere finner lønnsomt.
Hvorfor er TWAP-orakler tryggere enn ren spot-pris?
Fordi TWAP-en beregnes over tid basert på akkumulerte tick-verdier, ikke øyeblikksprisen. En angriper må holde en vridd pris over hele vinduet for at den skal påvirke gjennomsnittet nevneverdig, og det krever langt mer kapital og langt lengre eksponering enn ett enkelt flash lån i én transaksjon.
Hvor langt bør et TWAP-vindu være?
Det finnes ikke ett riktig svar, det avhenger av poolens likviditet og hvor volatilt aktivumet normalt er. De fleste produksjonsmiljøer som er beskrevet i Uniswaps egen dokumentasjon om orakler lander mellom 15 og 60 minutter, men en dypt likvid pool kan trygt bruke et kortere vindu enn en tynn pool.
Koster det mye gass å bruke flere orakelkilder samtidig?
Ja, et multi-orakel-oppslag koster mer enn ett enkelt kall, siden du både leser fra Chainlink og beregner en TWAP i samme transaksjon. For funksjoner som kalles svært ofte, kan det være fornuftig å cache en bekreftet pris i et kort tidsvindu i stedet for å kjøre full verifisering på hvert eneste kall.
Er egenbygde orakler alltid usikre?
Ikke nødvendigvis, men de bærer all risikoen selv. Hvis du bygger noe egendefinert, bør det aldri være den eneste kilden for en sensitiv funksjon som likvidering, og det bør alltid krysssjekkes mot en etablert kilde som Chainlink eller en velfundert TWAP.
Hvordan oppdager jeg et pågående orakelmanipulasjonsforsøk i sanntid?
Ved å lytte etter hendelsene kontraktene dine allerede kaster, slik som KilderUenige og PrisBevegerSegForRaskt i eksemplene over, og varsle teamet umiddelbart via en overvåkingstjeneste. Et forsøk som blir avvist av koden er fortsatt verdifull informasjon, det betyr at noen aktivt prøver seg på protokollen din.
Må jeg bruke Foundry, eller går Hardhat like bra?
Hardhat kan absolutt brukes til de samme integrasjonene, men Foundry sin innebygde fork-testing og fuzzing i Solidity gjør det raskere å simulere manipulasjonsscenarioer uten å skrive testlogikk i et annet språk. Velg det verktøyet teamet ditt allerede er komfortabelt med, konseptene i denne guiden overføres direkte uansett.




