DeFi lending protocols — Aave, Compound, and their forks — have collectively lost over $1 billion to exploits since 2020. The vulnerabilities rarely lie in the ERC-20 transfer logic; they live in the intersection of oracle pricing, collateral accounting, and the protocol's assumptions about market behavior.
This guide covers the attack vectors unique to lending systems, real exploit post-mortems, and the defensive patterns that auditors look for in every lending protocol review.
Oracle Manipulation: The Root of Most Exploits
Spot Price vs. TWAP
The most fundamental mistake in lending protocol design is using a spot price from a single DEX as a collateral value:
// VULNERABLE: spot price from a low-liquidity pool
function getCollateralValue(address token, uint256 amount) public view returns (uint256) {
(uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(pair).getReserves();
uint256 spotPrice = (reserve1 * 1e18) / reserve0;
return (amount * spotPrice) / 1e18;
}
An attacker with a large flash loan can:
1. Borrow the flash loan
2. Dump tokens into the pair, moving the spot price dramatically
3. Use the manipulated price to borrow far more than their collateral is worth
4. Repay the flash loan, keep the excess borrow
This was the exact mechanism behind the Mango Markets exploit ($117M, October 2022) and many smaller attacks.
Chainlink With Circuit Breakers
The standard defense uses Chainlink aggregators with staleness and sanity checks:
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract SecureOracle {
AggregatorV3Interface public immutable priceFeed;
uint256 public constant STALENESS_THRESHOLD = 3600; // 1 hour
uint256 public constant MAX_PRICE_DEVIATION = 1000; // 10% in bps
constructor(address _feed) {
priceFeed = AggregatorV3Interface(_feed);
}
function getPrice() external view returns (uint256) {
(
uint80 roundId,
int256 answer,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
require(answer > 0, "invalid price");
require(updatedAt >= block.timestamp - STALENESS_THRESHOLD, "stale price");
require(answeredInRound >= roundId, "incomplete round");
return uint256(answer);
}
}
Additional defensive layers for illiquid assets:
- TWAP from Uniswap v3 as secondary confirmation
- Circuit breaker that reverts if current price deviates more than X% from 24h TWAP
- Manual override role for extreme market events (with timelock)
Collateral Factor Misconfiguration
Every lending protocol assigns collateral factors — the percentage of collateral value that can be borrowed against. Set them too high, and a sharp price drop causes systemic bad debt that cannot be liquidated profitably.
struct Market {
bool isListed;
uint256 collateralFactorMantissa; // e.g., 0.75e18 = 75%
uint256 liquidationIncentiveMantissa; // e.g., 1.08e18 = 8% bonus
}
The Bad Debt Scenario
For liquidation to be profitable:
collateral_value_at_liquidation > loan_value + liquidation_cost
If collateralFactor = 0.90 and price drops 15% before any liquidation fires:
Position before: 100 ETH collateral → 90 ETH borrow (90% LTV)
Price drops 15%: 85 ETH collateral value → 90 ETH debt (underwater)
Liquidation bonus: 8%
Liquidator profit: (85 * 1.08) - 85 = 6.8 ETH... but debt is 90 ETH
At 90% collateral factor, a 10%+ price drop with 8% liquidation bonus means liquidators break even or lose money — creating bad debt that the protocol must absorb.
Auditors check: collateral factor + liquidation bonus + maximum realistic price volatility must leave a positive liquidation profit margin for all listed assets. Illiquid tokens should carry collateral factors no higher than 40–60%.
Liquidation Failures
Partial vs. Full Liquidation
Compound v2's partial liquidation model allows liquidators to repay up to closeFactor (typically 50%) of a borrower's debt in one transaction. This creates a problem with small underwater positions:
Debt: $10 (too small for gas to be profitable)
Gas cost: $8
Liquidator profit: $0.80 (8% bonus on $10)
Net: -$7.20 loss for liquidator
Small dust positions accumulate as bad debt. The fix is either a minimum borrow size enforced at the protocol level, or a "bad debt socialization" mechanism that writes off tiny positions immediately.
Liquidation in Single Block
// VULNERABLE: no protection against flash loan liquidation attack
function liquidate(
address borrower,
address collateralAsset,
uint256 repayAmount
) external {
// Attacker can: flash loan → liquidate (taking discount) → repay flash loan
// This is actually intended behavior for lending protocols
// BUT ensure the oracle cannot be manipulated in the same block
uint256 collateralValue = oracle.getPrice(collateralAsset);
// ...
}
Flash loan liquidations are expected in lending protocols — they improve liquidation speed. The danger is when an attacker manipulates the oracle in the same block to create an artificially underwater position and then liquidates it.
Interest Rate Model Attacks
Compound v2's interest rate model is based on utilization:
utilization = borrows / (cash + borrows - reserves)
borrowRate = baseRate + utilization * multiplier
At 100% utilization:
- cash = 0
- New borrows are impossible (nothing to lend)
- Existing borrowers pay maximum rates
The Utilization Manipulation Attack
An attacker with a large flash loan can:
1. Borrow all available liquidity → utilization → 100%
2. Any outstanding variable-rate borrows now accrue at the maximum rate
3. Repay flash loan
Against protocols where attackers also have short positions or competitive advantages from high rates, this spike has value. The mitigation is interest rate model caps and flash loan fees that make this unprofitable.
// Defensive: cap the effective borrow rate
function getBorrowRate(uint256 cash, uint256 borrows, uint256 reserves)
external view returns (uint256)
{
uint256 utilization = getUtilizationRate(cash, borrows, reserves);
uint256 rate = baseRatePerBlock + (utilization * multiplierPerBlock / 1e18);
// Hard cap prevents extreme rate spikes
return rate > maxBorrowRatePerBlock ? maxBorrowRatePerBlock : rate;
}
Reentrancy in Lending Protocols
The standard reentrancy pattern is well-understood, but lending protocols have unique variants:
Read-Only Reentrancy (Curve/Balancer Pattern)
// Attacker's scenario:
// 1. Call lendingProtocol.borrow() which calls back into attacker contract
// 2. During callback, attacker reads Curve pool price (which hasn't settled)
// 3. Uses stale/incorrect price from a third protocol that depends on Curve LP price
// 4. Opens under-collateralized position in lending protocol B
This cross-protocol reentrancy doesn't modify the victim contract's state — it reads stale state from an external contract during a locked intermediate state. Standard ReentrancyGuard doesn't protect against this because the attacker never re-enters the guarded function.
Mitigations:
- Use nonReentrant on all state-reading functions that external protocols might query
- Add explicit checks in oracle adapters: require(!isEntered, "reentrant oracle read")
- For Balancer/Curve LP price oracles, always verify the pool is not in a locked state
ERC-4626 Vault Integration Bugs
Many Aave v3 and Compound v3 forks accept ERC-4626 vault tokens as collateral. The share-inflation attack applies here:
// Attacker steps:
// 1. Deploy fresh ERC-4626 vault
// 2. Deposit 1 wei → receive 1 share
// 3. Directly donate 1000 ETH to vault (not through deposit())
// 4. totalAssets() = 1000 ETH + 1 wei
// 5. pricePerShare() = 1000 ETH (massively inflated)
// 6. Use 1 share as collateral → borrow 1000 ETH worth
// 7. Vault is drained
function convertToAssets(uint256 shares) public view returns (uint256) {
uint256 supply = totalSupply();
// VULNERABLE: no minimum deposit, no virtual shares offset
return supply == 0 ? shares : shares * totalAssets() / supply;
}
Fix: Use OpenZeppelin's ERC-4626 with virtual shares (introduced in v4.9):
function _decimalsOffset() internal view virtual returns (uint8) {
return 3; // adds 1000:1 virtual shares, making inflation attack 1000x more expensive
}
Always add a minimum initial deposit requirement and verify pricePerShare() reasonability before accepting a vault token as collateral.
Governance Attack on Lending Protocol Parameters
Compound's governance model allows COMP holders to vote on:
- Collateral factors for new assets
- Interest rate model changes
- New market listings
The Compound governance attack surface includes:
1. List a malicious token with a high collateral factor
2. Artificially inflate its price via a manipulated oracle or illiquid market
3. Borrow against inflated collateral → drain the protocol
Defense: Timelocks on all governance parameter changes (Compound uses 48 hours), and collateral factor caps enforced at the contract level regardless of governance vote.
Security Checklist for Lending Protocol Audits
Oracle Security
- [ ] No spot price from a single DEX used for collateral valuation
- [ ] Chainlink feeds checked for staleness and negative answers
- [ ] Price deviation circuit breaker against TWAP
- [ ] Oracle manipulation resistant during flash loan block
Collateral & Liquidation
- [ ] Collateral factors account for worst-case price drop + liquidation bonus
- [ ] Minimum borrow amount enforced to prevent dust accumulation
- [ ] Liquidation profitability positive across all market conditions
- [ ] Bad debt socialization mechanism exists
Reentrancy
- [ ] All external calls follow CEI (Checks-Effects-Interactions)
- [ ] nonReentrant on deposit, withdraw, borrow, repay, liquidate
- [ ] Cross-contract reentrancy via callbacks analyzed
Interest Rate Model
- [ ] Maximum borrow rate capped
- [ ] Flash loan fee makes utilization manipulation unprofitable
Governance
- [ ] 48+ hour timelock on collateral factor changes
- [ ] Hard caps on collateral factors in contract (not just governance)
- [ ] Multi-sig required for emergency parameter changes
Automated Scanning Coverage
Static analysis catches a subset of lending protocol vulnerabilities — the deterministic ones: missing reentrancy guards, unchecked oracle return values, and unsafe arithmetic in interest rate calculations. Economic attack vectors (oracle manipulation, collateral factor misconfiguration) require manual review and economic modeling.
A full lending protocol audit combines static scanning, manual review, economic analysis, and often formal verification for the interest rate math.
ContractScan's AI engine detects reentrancy paths, unchecked oracle calls, and unsafe token transfer patterns common in lending protocol forks. Run a free scan →
Important Notes
This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.