Et flash loan-angrep kan tømme en DeFi-protokoll på under 15 sekunder, altså tiden det tar å utføre én enkelt Ethereum-transaksjon. Angriperen låner millioner uten sikkerhet, vrir prisen på en likviditetspool, og betaler tilbake lånet før blokken lukkes. I denne guiden bygger du et faktisk testoppsett med Foundry og setter opp sanntidsovervåking med Forta Network, slik at koden din fanger opp mønsteret før noen rekker å utnytte det.

Guiden er skrevet for utviklere som allerede kan litt Solidity, men som ikke nødvendigvis har satt opp fuzz-testing eller sanntidsovervåking tidligere. Norske og nordiske team som bygger utlånsprotokoller, DEX-er eller lommeboktjenester møter nøyaktig de samme angrepsmønstrene som globale protokoller, bare med mindre budsjett til revisjon. Det gjør automatisert testing og gratis overvåkingsverktøy enda viktigere lokalt, siden ikke alle har råd til en full ekstern sikkerhetsgjennomgang før hver lansering.

Hva er et flash loan-angrep, og hvorfor rammer det DeFi i 2026?

Et flash loan er et lån uten sikkerhet som må tas opp og betales tilbake innenfor samme blokk. Går transaksjonen ikke opp i null ved slutten, ruller hele operasjonen tilbake som om den aldri skjedde. Det gjør flash loans til et legitimt verktøy for arbitrasje og refinansiering, men også et våpen for angripere som trenger midlertidig kapital for å vri en pris.

Mønsteret er som regel det samme. Angriperen låner et stort beløp, bruker det til å skyve reservene i en likviditetspool ut av balanse, utnytter en kontrakt som stoler blindt på denne øyeblikksprisen, og betaler til slutt tilbake lånet med det stjålne overskuddet. Hele forløpet skjer i én transaksjon, og det finnes ingen mulighet til å stoppe det manuelt underveis.

Flash loan-angrep sto for over 60 prosent av registrerte DeFi-hendelser målt i antall i 2025 og 2026, selv om bro-hack og tilgangskontrollfeil fortsatt står for de største enkeltbeløpene. Samtidig falt andelen av det totale kronetapet som skyldes flash loan-drevet prismanipulasjon og reentrancy til under 1 prosent i 2025, ned fra nesten 19 prosent i 2022. Det tyder på at målrettet testing og overvåking faktisk fungerer, selv om antallet forsøk holder seg høyt.

April 2025 illustrerer hvor fort disse tallene kan bevege seg: flash loan-relaterte utnyttelser den måneden alene forårsaket titalls millioner dollar i tap fordelt på 15 hendelser, en økning på 124 prosent fra måneden før. Slike svingninger gjør det vanskelig å basere sikkerhetsarbeidet på fjorårets trusselbilde alene. Angrepsverktøyene utvikler seg raskere enn de fleste interne sikkerhetsrutiner klarer å følge med på, noe som er nettopp grunnen til at automatisert testing og sanntidsovervåking har blitt standardpraksis fremfor et periodisk sjekkpunkt.

Flash loan-tap i tall: navngitte hendelser i 2025 og 2026

Tallene under er hentet fra dokumenterte hendelser med navngitt protokoll, dato og tapt beløp. De viser at angrepsmønsteret er identisk på tvers av kjeder og protokolltyper: en oracle eller en regnskapsfunksjon stoler på en pris eller balanse som kan flyttes innenfor én transaksjon.

ProtokollDatoTapAngrepsvektor
BunniSeptember 20258,4 mill. dollarAvrundingsfeil utnyttet via prismanipulasjon
zkLendFebruar 20259,5 mill. dollarAvrundingsfeil i mint()-funksjonen, forsterket med flash loan
GMXJuli 202542 mill. dollarReentrancy kombinert med manipulert prisflate
Makina Finance20. januar 20265 mill. dollar280 mill. USDC i flash loan mot MachineShareOracle
Summer Finance6. juli 20266 mill. dollar65,4 mill. dollar flash loan rutet via Morpho, Curve og Uniswap
Allbridge Core (Solana)19.–20. juli 20261,65 mill. dollarFlash loan-drevet oracle-manipulasjon
Scallop (Sui)26. april 2026142 000 dollarFlash loan-utnyttelse av lånelogikk

Totalt tapte DeFi-protokoller rundt 2,1 milliarder dollar i 2025, mens de tre første månedene av 2026 alene sto for om lag 350 millioner dollar, hvorav rundt 95 millioner dollar kom fra flash loan-relaterte angrep. Til sammenligning var toppåret 2022 på 2,62 milliarder dollar i tap, så det totale kronebeløpet har falt kraftig selv om antallet angrep øker. En separat gjennomgang fra januar 2025 til juli 2026 fant 245 sikkerhetshendelser med til sammen 3,63 milliarder dollar tapt på tvers av krypto-plattformer, hvorav 546 millioner dollar kom direkte fra smartkontraktutnyttelser mot desentraliserte apper.

Trenden over tid er mer interessant enn enkelthendelsene. Selv om flash loan-angrep fortsatt er den hyppigste angrepstypen målt i antall, har det totale kronebeløpet falt jevnt siden toppen i 2022. Det peker mot at bransjen har blitt flinkere til å oppdage og begrense skaden raskt, selv når et angrep lykkes. Gjennomsnittstapet per flash loan-hendelse lå på om lag 5,2 millioner dollar på tvers av over 120 dokumenterte hendelser i 2025 og 2026, et tall som varierer kraftig fra sak til sak, fra 142 000 dollar hos Scallop til 42 millioner dollar hos GMX.

ÅrTotalt DeFi-tapKommentar
20222,62 mrd. dollarToppår, høy andel ecosystem-angrep (flash loan/reentrancy)
2025680,3 mill. dollar (annen kilde: ca. 2,1 mrd. dollar totalt inkl. bro-hack)Ecosystem-klasse angrep falt til under 1 % av totaltapet
Q1 2026Ca. 350 mill. dollarCa. 95 mill. dollar av dette var flash loan-relatert
Jan. 2025–jul. 20263,63 mrd. dollar (245 hendelser)546 mill. dollar kom fra smartkontraktutnyttelser mot desentraliserte apper

Forutsetninger: verktøy og versjoner du trenger

Du trenger ikke mye for å komme i gang, men rekkefølgen betyr noe. Har du allerede et eksisterende Solidity-prosjekt satt opp med Hardhat, kan du fortsatt følge denne guiden ved å legge Foundry til side om side, de to verktøykjedene lever fint sammen i samme repo. Sørg for at følgende er på plass før du starter selve gjennomgangen:

  • Et Unix-basert operativsystem (Linux, macOS eller WSL2 på Windows)
  • Foundry (forge, cast og anvil), installert via foundryup
  • Node.js 18 eller nyere, for Forta-verktøyene
  • Git, for å hente OpenZeppelin Contracts som avhengighet
  • En grunnleggende forståelse av Solidity 0.8.x
  • En Polygon-lommebok med litt MATIC hvis du vil publisere en deteksjonsbot i produksjon (valgfritt for selve testoppsettet)

Tabellen under oppsummerer hvilken rolle hvert verktøy spiller i oppsettet, slik at du vet hvorfor du installerer det før du kommer til selve stegene:

VerktøyInstalleres viaRolle i oppsettet
Foundry (forge, cast, anvil)foundryupKompilering, fuzz-testing og invariant-testing av Solidity
OpenZeppelin Contractsforge installReviderte standardkomponenter for tilgangskontroll og tokens
Node.js 18+Offisiell installer eller nvmKjøretid for Forta-agentens CLI-verktøy
forta-agent (CLI)npm install -gBygge, teste lokalt og publisere deteksjonsboter
forge-stdforge installTest- og cheatcode-biblioteket Foundry-testene arver fra

Merk deg også at OpenZeppelin Defender sluttet å ta imot nye registreringer 30. juni 2025 og etter planen stenger helt 1. juli 2026. Har teamet ditt brukt Defender til overvåking eller automatiserte handlinger, er det på tide å flytte den funksjonaliteten over til Forta-boter og Foundry-baserte tester, noe denne guiden viser deg hvordan du gjør. Vent ikke til stengedatoen nærmer seg, migrering av overvåkingsregler tar som regel lengre tid enn utviklere planlegger for.

Steg 1: Installer Foundry (forge, cast og anvil)

Foundry er verktøykjeden som gjør fuzz-testing og invariant-testing praktisk mulig direkte i Solidity, uten å skrive testene i JavaScript. Det betyr at testene dine bruker samme språk og samme kompilator som selve kontraktene, noe som gjør det enklere å skrive presise angrepssimuleringer uten å hoppe mellom to økosystemer. Installasjonen skjer i to trinn: først installasjonsskriptet, deretter selve verktøyene.

curl -L https://getfoundry.sh/install | bash
foundryup

foundryup laster ned de nyeste stabile versjonene av forge, cast, anvil og chisel. Kjør forge --version etterpå for å bekrefte at installasjonen gikk gjennom. Ligger du på en eldre maskin med gamle binaries fra en tidligere installasjon, kjør foundryup på nytt etter å ha fjernet gamle symlinker manuelt.

anvil er den lokale Ethereum-noden du bruker til å teste mot en simulert blokkjede uten å betale ekte gass. cast er kommandolinjeverktøyet for å sende transaksjoner og lese kontrakttilstand direkte fra terminalen, praktisk når du senere skal verifisere at en fiks faktisk endret oppførselen på kjeden. Du kommer til å bruke forge mest gjennom resten av guiden, men de andre tre verktøyene dukker opp så snart du begynner å teste mot en forket hovednett-tilstand.

Steg 2: Sett opp prosjektet og installer OpenZeppelin Contracts

Opprett et nytt Foundry-prosjekt og legg til OpenZeppelin Contracts som git-avhengighet. Dette gir deg ferdig reviderte byggeklosser for tilgangskontroll og tokenstandarder, slik at du ikke skriver den logikken fra bunnen av.

forge init flash-loan-defense
cd flash-loan-defense
forge install OpenZeppelin/openzeppelin-contracts

Legg deretter til en remapping i remappings.txt slik at importene blir lesbare:

@openzeppelin/=lib/openzeppelin-contracts/

Nå kan du importere kontrakter direkte, for eksempel import "@openzeppelin/contracts/token/ERC20/ERC20.sol";. Kjør forge build for å bekrefte at alt kompilerer før du går videre.

Installer også forge-std hvis prosjektet ikke allerede har det, siden testene senere i guiden arver fra Test-kontrakten der og bruker cheatcodes som vm.warp() og bound(): forge install foundry-rs/forge-std. Hold avhengighetene låst til en spesifikk commit eller tag i produksjonskode, ikke bare til seneste hovedgren, så et oppstrøms brudd ikke plutselig knekker byggingen din midt i en revisjon.

Steg 3: Det sårbare mønsteret – spotpris fra én DEX-pool

Før du kan bygge forsvaret, må du kjenne angrepsflaten. Det klassiske feilmønsteret er en kontrakt som leser prisen direkte fra reservene i én likviditetspool og stoler på tallet uten videre kontroll. Under er et representativt eksempel på mønsteret, satt sammen etter beskrivelsene i OWASPs Smart Contract Top 10 for 2026.

interface IUniswapV2Pair {
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}

contract SarbarHvelvKontrakt {
    IUniswapV2Pair public immutable pool;

    constructor(IUniswapV2Pair _pool) {
        pool = _pool;
    }

    // Returnerer pris basert paa oyeblikkelige reserver
    function hentSpotpris() public view returns (uint256) {
        (uint112 r0, uint112 r1, ) = pool.getReserves();
        require(r0 > 0 && r1 > 0, "Ingen likviditet");
        return (uint256(r1) * 1e18) / uint256(r0);
    }

    function settInn(uint256 belop) external returns (uint256 andeler) {
        uint256 pris = hentSpotpris(); // STOLER PAA OYEBLIKKSPRIS
        andeler = belop * pris / 1e18;
    }
}

Problemet ligger i hentSpotpris(). En angriper kan låne et stort beløp via flash loan, bytte mot poolen slik at reservene r0 og r1 forskyves kraftig, og deretter kalle settInn() mens prisen fortsatt er forvrengt. Etterpå reverseres byttet, prisen normaliseres, og angriperen sitter igjen med andeler priset til en verdi som aldri fantes i markedet. Dette er nøyaktig mekanismen bak både Makina Finance-hendelsen i januar 2026 og deler av zkLend-hendelsen i februar 2025.

Det som gjør mønsteret vanskelig å fange under manuell koderevisjon, er at funksjonen ser helt normal ut isolert sett. Ingen enkeltlinje er “feil” i tradisjonell forstand, kontrakten leser bare en pris og regner ut andeler basert på den. Feilen dukker først opp når du tenker på funksjonen som en del av en sekvens: hva skjer hvis reservene endres kraftig rett før dette kallet? Det er nettopp det spørsmålet fuzz- og invariant-testene i de neste stegene er bygget for å stille automatisk, tusenvis av ganger, med verdier et menneske aldri ville tenkt på å teste manuelt.

Løsningen er å bytte ut øyeblikksprisen med en kilde som ikke kan flyttes innenfor én transaksjon. To vanlige tilnærminger er eksterne prisflater som Chainlink, eller et tidsveid gjennomsnitt (TWAP) beregnet over flere blokker. Under er en hardet variant som bruker et Chainlink-lignende grensesnitt med kontroll på ferskhet.

interface IAggregatorV3 {
    function latestRoundData() external view returns (
        uint80 roundId, int256 answer, uint256 startedAt,
        uint256 updatedAt, uint80 answeredInRound
    );
}

contract HardetOracleKontrakt {
    IAggregatorV3 public immutable prisflate;
    uint256 public immutable maksAlder; // f.eks. 1 time

    constructor(IAggregatorV3 _prisflate, uint256 _maksAlder) {
        prisflate = _prisflate;
        maksAlder = _maksAlder;
    }

    function hentSikkerPris() public view returns (uint256) {
        (, int256 svar, , uint256 oppdatert, ) = prisflate.latestRoundData();
        require(svar > 0, "Ugyldig pris");
        require(block.timestamp - oppdatert <= maksAlder, "Utdatert oracle-data");
        return uint256(svar) * 10**10;
    }

    function settInn(uint256 belop) external returns (uint256 andeler) {
        uint256 pris = hentSikkerPris();
        andeler = belop * pris / 1e18;
    }
}

Legg merke til to nye sjekker: require(svar > 0) avviser åpenbart ugyldige verdier, og ferskhetssjekken avviser data som er eldre enn den terskelen du setter. En TWAP-variant følger samme prinsipp, men beregner gjennomsnittsprisen over et minimumsintervall (typisk 10–30 minutter) i stedet for å lese én enkelt prisflate. Ingen av tilnærmingene lar en enkelt transaksjon flytte prisen kontrakten din stoler på.

Velg terskel for maksAlder ut fra hvor volatilt underliggende aktivum er. En stabil eiendel tåler gjerne en times gammel prisdata, mens en likviditetstynn token kan trenge en terskel ned mot ti minutter. Sett terskelen for stramt, og kontrakten din stopper opp hver gang en oracle-oppdatering er noen minutter forsinket, noe som i praksis blir en selvpåført tjenestenekt for brukerne dine.

Steg 5: Skriv fuzz-tester som simulerer prismanipulasjon

Foundry fuzz-tester automatisk enhver testfunksjon som tar imot parametere. Standard er 256 kjøringer per test, men du bør øke dette betydelig når du tester prislogikk og regnskapsfunksjoner som håndterer verdi.

function testFuzz_AndelerSkalIkkeOverskrideOracleVerdi(uint256 belop, uint256 falskPris) public {
    belop = bound(belop, 1, 1_000_000e18);
    falskPris = bound(falskPris, 1, 10_000e18);

    uint256 andelerForOracle = vault.settInn(belop);
    assertLe(andelerForOracle, belop * oracle.hentSikkerPris() / 1e18);
}

Kjør testen med flere tilfeldige kjøringer for å presse grenseverdiene:

forge test --match-test testFuzz_AndelerSkalIkkeOverskrideOracleVerdi --fuzz-runs 10000

Ti tusen kjøringer høres mye ut, men på en moderne bærbar tar det som regel under ett minutt for enkle regnestykker. En tilsvarende tilnærming brukes i offentlige gjennomganger av tilgangskontrollfeil, som alene har kostet DeFi-protokoller rundt 953 millioner dollar. Samme prinsipp, bare rettet mot funksjoner med onlyOwner eller andre tilgangsmodifikatorer i stedet for prislogikk.

Funksjonen bound() er avgjørende her. Uten den vil Foundry teste med fullstendig tilfeldige uint256-verdier, inkludert tall så store at de uansett reverter på overflow lenge før de når prislogikken. bound() begrenser fuzzeren til et realistisk, men fortsatt bredt, verdiområde, slik at hver av de titusen kjøringene faktisk tester noe meningsfullt i stedet for å reverte umiddelbart på en urealistisk inngangsverdi.

Steg 6: Skriv invariant-tester for kontoregnskapet

Fuzz-tester sjekker én funksjon om gangen. Invariant-tester går et steg lenger: Foundry kjører tilfeldige sekvenser av kall mot kontraktene dine og sjekker etter hver sekvens at en gitt egenskap fortsatt holder. Det er akkurat denne typen egenskap som brøt sammen i zkLend-hendelsen, der en avrundingsfeil i mint() ble forsterket gjennom gjentatte innskudd og uttak.

import "forge-std/Test.sol";
import "../src/HardetOracleKontrakt.sol";

contract HvelvInvariantTest is Test {
    HardetOracleKontrakt vault;

    function setUp() public {
        vault = new HardetOracleKontrakt(mockOracle, 3600);
    }

    // Denne funksjonen kalles etter hver tilfeldige sekvens
    function invariant_TotalAndelerMatcherTotalVerdi() public {
        assertEq(vault.totalAndeler(), vault.totalVerdi());
    }

    function invariant_IngenBrukerKanTrekkeUtMerEnnReellVerdi() public {
        assertLe(vault.brukerSaldo(msg.sender), vault.beregnReellVerdi(msg.sender));
    }
}

Navnekonvensjonen er ufravikelig: funksjoner som skal fungere som invarianter må starte med invariant_. Under kjøringen fuzzer Foundry en hel sekvens av tilfeldige funksjonskall mot kontraktene i setUp(), ikke bare ett enkelt kall om gangen. Det er dette som gjør invariant-testing i stand til å finne feil som kun oppstår etter en spesifikk rekkefølge av innskudd, bytter og uttak, akkurat den typen sammensatte tilstand som lå bak zkLend-hendelsen. Kjør testene med:

forge test --match-contract HvelvInvariantTest

Steg 7: Kjør testsuiten og tolk resultatene

Kjør hele suiten med forge test -vv for økt loggdetalj. Slår en invariant-test feil, gir Foundry deg sekvensen av kall som førte til bruddet, i tillegg til et forslag til en forkortet reproduksjon (kalt "shrinking"). Dette er langt raskere å feilsøke enn en revisjonsrapport, fordi du får den eksakte transaksjonsrekkefølgen som knekker antagelsen din.

Legg til dekningsmåling for å se hvilke grener av koden testene faktisk treffer:

forge coverage --report summary

Mål deg mot full dekning på funksjoner som håndterer overføring av verdi, innskudd, uttak og prisberegning. Dekning garanterer ikke fravær av feil, men lav dekning på disse funksjonene er et klart varsel om at flere angrepsflater står uprøvd.

Sett gjerne en dekningsterskel i CI som feiler bygget hvis prosentandelen faller under et fastsatt nivå, for eksempel 90 prosent på kontraktene i src/. Dette hindrer at nye funksjoner slipper gjennom uten tester bare fordi resten av kodebasen har god dekning fra før.

Steg 8: Sett opp sanntidsovervåking med Forta Network

Tester i Foundry fanger opp det du klarer å forestille deg før utrulling. Forta Network dekker det som skjer etterpå: sanntidsovervåking av transaksjoner mot kontraktene dine, ute i produksjon. Prosjektet driftes fortsatt under navnet Forta, med dokumentasjon på docs.forta.network og forta.org, uten tegn til rebranding per i dag.

Installer Forta-verktøyene via npm:

npm install -g forta-agent
npx forta-agent init --typescript

Dette setter opp et prosjektskjelett for en deteksjonsbot: et Docker-basert program som skanner blokkjededata etter mønstre du definerer, for eksempel store enkelttransaksjoner som skyver poolreserver over en gitt terskel, eller flash loan-innslag etterfulgt av rebalansering i samme blokk.

Forta beskriver selv deteksjonsboter som virtuelle sikkerhetskameraer: hver bot overvåker kontinuerlig og kringkaster et offentlig varsel når mønsteret den er bygget for å finne, dukker opp. Fordelen fremfor et internt overvåkingsskript er at boten kjører uavhengig av din egen infrastruktur, på et distribuert nettverk av skannenoder, så et angrep mot din server stopper ikke overvåkingen av kontraktene dine.

Steg 9: Bygg, test og publiser en deteksjonsbot

Før du publiserer noe, test boten lokalt mot kjente hendelser. Forta lar deg kjøre logikken mot en spesifikk transaksjon eller blokk uten å publisere noe som helst.

npx forta-agent run --tx 0xKJENT_ANGREPS_HASH
npx forta-agent run --block 19500000

Denne testmetoden gjør det mulig å kjøre boten din mot historiske hendelser, som GMX-angrepet i juli 2025 eller Allbridge Core-hendelsen i juli 2026, for å bekrefte at deteksjonslogikken faktisk fanger mønsteret før du stoler på den i produksjon. Når testene er grønne, publiserer du boten:

npx forta-agent publish

Kommandoen registrerer boten din i en registerkontrakt på Polygon. Det krever en liten mengde MATIC til gass, men Forta Connect subsidierer ofte denne kostnaden, slik at publisering i praksis blir gratis for utvikleren. Når boten kjører på et skannenode, er den begrenset til 20 prosent CPU og 1 GB minne, så hold deteksjonslogikken slank.

Unngå å legge tunge beregninger eller eksterne API-kall inn i selve handleTransaction-funksjonen. Boten kjører på hver eneste transaksjon som treffer filteret ditt, så en treg funksjon her forsinker varslene dine nettopp når hastighet betyr mest, altså midt i et pågående angrep.

Steg 10: Circuit breakers og hastighetsgrenser

Overvåking varsler deg etter at noe har skjedd. En circuit breaker kan stoppe skaden mens den pågår. Legg inn en enkel hastighetsgrense i kontrakten som fryser store bevegelser midlertidig hvis prisen endrer seg mer enn en fastsatt prosentandel innenfor én blokk.

uint256 public sistePris;
uint256 public constant MAKS_AVVIK_PROSENT = 5;
bool public pauset;

function oppdaterPrisMedVakt(uint256 nyPris) internal {
    if (sistePris > 0) {
        uint256 avvik = nyPris > sistePris
            ? (nyPris - sistePris) * 100 / sistePris
            : (sistePris - nyPris) * 100 / sistePris;
        if (avvik > MAKS_AVVIK_PROSENT) {
            pauset = true;
            revert("Prisavvik over terskel, kontrakt pauset");
        }
    }
    sistePris = nyPris;
}

Denne typen vakt er ikke vanntett alene, men den kjøper deg tid til å reagere manuelt og reduserer hvor mye en angriper kan trekke ut i det første forsøket. Kombinert med oracle-hardingen fra steg 4, blir det betydelig dyrere å utnytte kontrakten i én enkelt transaksjon.

Vurder også å legge inn en beløpsgrense per transaksjon i tillegg til prosentgrensen, uavhengig av hva oracle-prisen sier. En fast øvre grense på hvor mye som kan flyttes i én transaksjon setter et absolutt tak på skaden, selv i et scenario der både oracle og prisvakt skulle feile samtidig. De fleste protokoller finner denne grensen ved å se på normal transaksjonsstørrelse over de siste ukene og legge på en solid margin, ikke ved å gjette et rundt tall.

Steg 11: Automatiser sikkerhetstester i CI/CD

Manuell testing blir glemt før eller siden. Legg fuzz- og invariant-testene inn i en CI-pipeline slik at hver pull request kjører hele suiten automatisk. Fest en spesifikk Foundry-versjon i CI-oppsettet for å unngå at nattlige oppdateringer av verktøyet endrer testresultatene uventet.

name: foundry-sikkerhetstester
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: foundry-rs/foundry-toolchain@v1
      - run: forge test --fuzz-runs 5000 -vv
      - run: forge coverage --report summary

Kjør et lavere antall fuzz-kjøringer lokalt under utvikling for rask tilbakemelding, og et høyere tall i CI der ventetiden betyr mindre. Mange team kjører 1000 kjøringer lokalt og 10 000 eller flere i CI før merge til hovedgrenen.

Legg gjerne til en egen nattlig jobb som kjører et enda høyere antall kjøringer, for eksempel 100 000, mot hovedgrenen. Denne jobben trenger ikke blokkere noen pull request, men den fanger opp sjeldne kantfeil som et lavere antall kjøringer under vanlig utvikling rett og slett aldri treffer på.

Steg 12: Sett opp varsler og hendelsesrespons

Den siste brikken er å koble Forta-varslene dine til et sted et menneske faktisk ser dem. Forta App har en abonnementsside der du kobler boten din eller spesifikke kontrakter til varselkanaler som e-post, Slack, Telegram, Discord eller webhooks.

Boter kan også abonnere på andre boters varsler gjennom en handleAlert-håndterer, slik at du kan kjede sammen generelle flash loan-deteksjoner med din egen protokollspesifikke logikk. Avklar rollene på forhånd: hvem i teamet svarer på et Forta-varsel klokken tre om natten, og hvilken fullmakt har de til å pause kontrakten uten å vente på et møte? Uten et svar på det spørsmålet er overvåkingen bare støy.

Skriv ned en enkel eskaleringsplan før du trenger den, ikke mens et angrep pågår. Planen bør dekke tre ting: hvem som blir varslet først, hvilken kommando eller funksjon som stanser kontrakten, og hvem som har fullmakt til å ta den avgjørelsen uten å vente på godkjenning fra flere ledd. Team som øver på denne responsen på forhånd, for eksempel ved å kjøre gjennom et simulert scenario én gang i kvartalet, reagerer merkbart raskere den dagen et reelt varsel kommer inn.

Vanlige fallgruver ved flash loan-beskyttelse

Selv team som følger stegene over, går ofte i noen av de samme fellene. Ingen av dem krever avanserte forkunnskaper for å unngå, de handler i praksis om disiplin og om å ikke ta snarveier under tidspress før en lansering. Her er de som dukker opp igjen og igjen i etterrapporter fra 2025 og 2026:

  • Å stole på én enkelt DEX-pool som prisreferanse. Selv en stor pool kan flyttes med et lån stort nok, slik Makina Finance-hendelsen i januar 2026 viste med et lån på 280 millioner USDC. Bruk alltid en ekstern eller aggregert prisflate for verdisatte funksjoner.
  • Å hoppe over ferskhetssjekk på oracle-data. En prisflate som ikke har oppdatert seg på flere timer er like farlig som ingen prisflate i det hele tatt, fordi kontrakten din da handler på et tall som ikke lenger reflekterer markedet.
  • Å teste kun det normale tilfellet. Fuzz-testene dine må inkludere ekstreme og grensesnære verdier, ikke bare realistiske innskuddsbeløp. Et angrep er per definisjon et unormalt tilfelle.
  • Å glemme avrundingsretning i heltallsdivisjon. zkLend-hendelsen i februar 2025 startet nettopp med en avrundingsfeil som ble forsterket gjennom gjentatte kall. Solidity runder alltid ned ved heltallsdivisjon, og den forskjellen akkumuleres raskt over mange transaksjoner.
  • Å publisere en Forta-bot uten å teste den mot historiske angrepstransaksjoner først. En bot som aldri har sett et reelt angrepsmønster, gir deg falsk trygghet. Kjør den alltid mot minst to-tre kjente hendelser før du stoler på den i produksjon.
  • Å sette varslingsterskelen så høyt at reelle angrep ikke trigger alarm, av frykt for falske positiver. Juster heller terskelen ned og lev med noen flere varsler å avvise manuelt, enn å gå glipp av det ene varselet som faktisk betyr noe.
  • Å behandle circuit breakeren som en engangsjobb. Terskler satt for et halvt år siden matcher sjelden dagens likviditet og transaksjonsvolum. Gjennomgå grensene med jevne mellomrom, ikke bare når noe allerede har gått galt.

Feilsøking: åtte vanlige problemer og løsninger

1. «forge: command not found» etter installasjon. Terminalen din har ikke lastet inn den oppdaterte PATH-variabelen. Åpne en ny terminaløkt, eller kjør source ~/.bashrc (eller ~/.zshrc) manuelt.

2. Fuzz-tester tar uventet lang tid. Sjekk om testfunksjonen kaller eksterne kontrakter eller gjør tunge løkker per kjøring. Reduser --fuzz-runs lokalt under utvikling, og skru det opp bare i CI.

3. Invariant-test finner ingen brudd selv om du vet det finnes en feil. Sjekk at handler-funksjonene faktisk kaller de riktige kontraktfunksjonene. Foundry fuzzer bare de funksjonene den har tilgang til gjennom testens offentlige grensesnitt. Øk også antall kjøringer og dybden på sekvensene, siden noen feil bare dukker opp etter mange kombinerte kall.

4. «Stale oracle»-feilen trigges for ofte i test. Mock-oracles i testmiljøet arver ikke automatisk block.timestamp. Bruk vm.warp() for å flytte testens klokke sammen med oracle-oppdateringene.

5. remappings.txt blir ignorert. Bekreft at filen ligger i prosjektroten, ikke i en undermappe, og at det ikke finnes en motstridende remapping i foundry.toml samtidig.

6. Forta-boten feiler lokalt med tilkoblingsfeil mot RPC-noden. Sett en dedikert RPC-URL i konfigurasjonen i stedet for å stole på standard offentlig endepunkt, som ofte har lave hastighetsgrenser og kan blokkere deg midt i en testkjøring mot en historisk hendelse.

7. Publisering av bot feiler med utilstrekkelig gass. Sjekk MATIC-saldoen på lommeboken du publiserer fra. Selv med subsidiert publisering trengs det ofte en liten buffer.

8. CI-pipelinen gir andre testresultater enn lokal maskin. Lås Foundry-versjonen eksplisitt i CI-konfigurasjonen i stedet for å alltid hente nyeste versjon, slik at fuzz-motorens tilfeldighetskilde og standardverdier er identiske i begge miljøer.

Avanserte tips fra sikkerhetsteam

Når grunnmuren er på plass, er det noen grep som skiller gode oppsett fra gode nok. Bruk targetContract og targetSelector-hjelperne i Foundry for å styre invariant-fuzzeren mot spesifikke funksjoner i stedet for å la den prøve alt tilfeldig, spesielt i store kontraktsystemer med mange innganger.

Kombiner Echidna med Foundry der du trenger lange, tilfeldige sekvenser med automatisk «shrinking» til en minimal reproduksjon. Foundry er raskere til daglig utvikling, men Echidnas eldre, mer modne shrinking-motor finner noen ganger kortere angrepsveier som er lettere å forstå.

Sett opp en egen Forta-bot som kun overvåker forholdet mellom din interne oracle-pris og prisen i den underliggende DEX-poolen. Et plutselig avvik der er ofte det tidligste tegnet på et pågående angrep, før selve uttaket skjer. Til slutt: dokumenter hvilke invarianter som faktisk beskytter mot hvilke historiske hendelser. Det gjør revisjonsprosessen langt raskere neste gang noen skal vurdere kodebasen din.

Vurder å kjøre en fork av hovednettet lokalt med anvil --fork-url og reprodusere en kjent hendelse mot din egen kontraktlogikk, ikke bare mot originalprotokollen. Dette avslører raskt om ditt eget system er sårbart for samme mønster, uten at du trenger å gjette deg frem til testparametere fra bunnen av. Under tabellen finner du en samlet oversikt over kommandoene som er brukt gjennom hele guiden, nyttig å ha ved hånden når du setter opp et nytt prosjekt.

KommandoFormål
foundryupInstallerer eller oppdaterer forge, cast, anvil og chisel
forge initOppretter et nytt Foundry-prosjekt
forge installLegger til en git-avhengighet som OpenZeppelin Contracts
forge buildKompilerer kontraktene i prosjektet
forge test --fuzz-runs NKjører fuzz-tester med N tilfeldige input-kombinasjoner
forge test --match-contract NAVNKjører kun tester i en spesifikk kontrakt, f.eks. invariant-tester
forge coverage --report summaryViser testdekning per fil og funksjon
npx forta-agent init --typescriptSetter opp et nytt Forta-botprosjekt
npx forta-agent run --tx / --blockTester deteksjonslogikk lokalt mot en historisk transaksjon eller blokk
npx forta-agent publishPubliserer boten til Forta Network via Polygon-registeret

Komplett prosjektstruktur: alt på ett sted

Når du har fulgt alle tolv stegene, bør prosjektmappen din se omtrent slik ut:

flash-loan-defense/
├── src/
│   ├── HardetOracleKontrakt.sol
│   └── SarbarHvelvKontrakt.sol
├── test/
│   ├── FuzzOracle.t.sol
│   └── HvelvInvariant.t.sol
├── forta-bot/
│   ├── src/agent.ts
│   └── package.json
├── .github/workflows/foundry-sikkerhetstester.yml
├── remappings.txt
└── foundry.toml

Denne strukturen gir deg tre uavhengige forsvarslinjer: fuzz-tester som stresser enkeltfunksjoner, invariant-tester som stresser hele kontraktens tilstand over tid, og en Forta-bot som overvåker produksjonstrafikk i sanntid etter at koden er live. Ingen av de tre alene stopper alt, men sammen dekker de både utviklingsfasen og driftsfasen.

Tenk på oppsettet som lagdelt forsvar fremfor én enkelt løsning. Fuzz-testene fanger feil i enkeltfunksjoner før koden i det hele tatt committes. Invariant-testene fanger feil som kun oppstår gjennom en sekvens av kall, noe et enkelt fuzz-testtilfelle aldri ville avdekket. Circuit breakeren begrenser skaden hvis noe likevel slipper gjennom til produksjon. Og Forta-boten sikrer at teamet ditt vet om et forsøk innen sekunder, ikke timer eller dager senere når noen tilfeldigvis oppdager en unormal saldo. De fire lagene dekker til sammen både utviklingsfasen, utrullingen og den løpende driften, og ingen enkeltperson trenger å overvåke alt manuelt.

Ofte stilte spørsmål

Hva er forskjellen på et flash loan-angrep og et vanlig hack?

Et vanlig hack utnytter ofte en lekket nøkkel eller en tilgangskontrollfeil, noe som som regel krever at angriperen først skaffer seg tilgang på forhånd, for eksempel gjennom phishing eller en kompromittert privat nøkkel. Et flash loan-angrep krever ingen kapital og ingen forhåndstilgang fra angriperen, bare en sårbar prislogikk eller regnskapsfunksjon som kan manipuleres innenfor én transaksjon. Det gjør terskelen for å forsøke et flash loan-angrep vesentlig lavere, siden angriperen ikke risikerer egen kapital.

Er Foundry gratis å bruke?

Ja, Foundry er et åpen kildekode-prosjekt uten lisenskostnad. Du betaler kun for beregningskraften du selv bruker lokalt eller i CI-miljøet ditt.

Hvor mange fuzz-kjøringer er nok?

Standard er 256, men for prislogikk og regnskapsfunksjoner bør du sikte mot minst 1000 lokalt og 10 000 eller mer i CI. Jo mer verdi funksjonen håndterer, jo høyere bør tallet være.

Kan Forta Network stoppe et angrep automatisk?

Nei, Forta varsler deg om mistenkelig aktivitet, men griper ikke inn i kontrakten selv. Skal et angrep stoppes automatisk, må du bygge en circuit breaker direkte inn i kontraktslogikken, slik som vist i steg 10. Forta og circuit breakeren løser derfor to forskjellige problemer: den ene gir deg synlighet og et varsel et menneske kan reagere på, den andre stopper skaden i selve transaksjonen uten at noen trenger å rekke å trykke på noe.

Holder det med én revisjon før lansering?

Nei. Både zkLend- og Bunni-hendelsene rammet protokoller som hadde vært gjennom revisjon. En revisjon er et øyeblikksbilde av koden på et gitt tidspunkt, utført av mennesker med begrenset tid. Fuzz- og invariant-testing er ment å komplementere en revisjon, ikke erstatte behovet for kontinuerlig overvåking etterpå, og de kjører automatisk hver gang koden endres, ikke bare én gang før lansering.

Hva skjer med teamene som brukte OpenZeppelin Defender?

Defender sluttet å ta imot nye kunder 30. juni 2025 og etter planen stenger tjenesten helt 1. juli 2026. Team som fortsatt bruker den til overvåking eller automatiserte handlinger bør migrere til en kombinasjon av Foundry-tester og Forta-boter i god tid før den datoen.

Begge kan fungere. Chainlink gir deg en ekstern, aggregert prisflate uten at du selv må vedlikeholde infrastruktur. En egen TWAP krever mer arbeid, men gir deg full kontroll over beregningsvinduet. Mange protokoller bruker begge som kryssjekk mot hverandre.

Koster det noe å publisere en Forta-bot?

Publisering krever en liten mengde MATIC til gass på Polygon, men Forta Connect subsidierer ofte denne kostnaden, slik at den reelle utgiften for utvikleren som regel er minimal.