Flash Loan Attack Vector Remediation
How to eliminate flash loan attack surfaces by replacing manipulable spot prices with time-weighted average prices and adding sanity bounds on price inputs.
Flash Loan Attack Vector Remediation
Overview
Flash loan vulnerabilities arise when a protocol relies on on-chain spot prices (DEX reserves, slot0 values) that an attacker can manipulate within a single transaction using a large borrowed balance. The remediation is to replace instantaneous price reads with time-weighted average prices (TWAPs) that cannot be manipulated within a single block, or to add sanity bounds that reject implausible price movements.
Related Detector: Flash Loan Attack Vector
Recommended Fix
Before (Vulnerable)
interface IUniswapV2Pair {
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}
contract VulnerableLending {
IUniswapV2Pair public pair;
function getTokenPrice() public view returns (uint256) {
(uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
// VULNERABLE: spot price can be manipulated in the same transaction via flash loan
return (uint256(reserve1) * 1e18) / uint256(reserve0);
}
function borrow(uint256 collateralAmount) external {
uint256 price = getTokenPrice(); // Attacker inflates this
uint256 loanAmount = collateralAmount * price / 1e18;
_issueLoan(msg.sender, loanAmount);
}
}
After (Fixed)
interface IUniswapV3Pool {
function observe(uint32[] calldata secondsAgos)
external view returns (int56[] memory tickCumulatives, uint160[] memory);
}
contract SafeLending {
IUniswapV3Pool public pool;
uint32 public constant TWAP_PERIOD = 1800; // 30-minute TWAP
function getTWAPPrice() public view returns (uint256) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = TWAP_PERIOD;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
int56 tickDelta = tickCumulatives[1] - tickCumulatives[0];
int24 avgTick = int24(tickDelta / int56(uint56(TWAP_PERIOD)));
// Convert tick to price — cannot be manipulated in a single block
return _tickToPrice(avgTick);
}
function borrow(uint256 collateralAmount) external {
uint256 price = getTWAPPrice(); // TWAP — flash loan resistant
uint256 loanAmount = collateralAmount * price / 1e18;
_issueLoan(msg.sender, loanAmount);
}
}
Alternative Mitigations
Chainlink price feeds — off-chain aggregated prices that are not manipulable within a single block:
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract ChainlinkPricing {
AggregatorV3Interface public priceFeed;
uint256 public constant MAX_STALENESS = 1 hours;
function getPrice() public view returns (uint256) {
(, int256 price, , uint256 updatedAt, ) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt <= MAX_STALENESS, "Stale price");
require(price > 0, "Invalid price");
return uint256(price);
}
}
Price deviation bounds — reject prices that deviate too far from a trusted reference:
uint256 constant MAX_PRICE_DEVIATION_BPS = 500; // 5%
function validatePrice(uint256 spotPrice, uint256 twapPrice) internal pure {
uint256 deviation = spotPrice > twapPrice
? (spotPrice - twapPrice) * 10000 / twapPrice
: (twapPrice - spotPrice) * 10000 / twapPrice;
require(deviation <= MAX_PRICE_DEVIATION_BPS, "Price manipulation detected");
}
Common Mistakes
Using Uniswap V2 getReserves() with no manipulation check — always manipulable in the same block.
TWAP period too short — a 1-block TWAP provides almost no protection. Use at least 15 minutes, preferably 30 minutes or more for high-value operations.
Mixing oracle sources — using a TWAP for one asset and a spot price for another in the same calculation reintroduces the vulnerability through the spot-priced asset.