Install in Claude Code
Copygit clone --depth 1 https://github.com/PurpleAILAB/Vigilo /tmp/lending && cp -r /tmp/lending/packages/opencode/skills/lending ~/.claude/skills/lendingThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# Lending Protocol Patterns
This skill provides comprehensive knowledge for auditing DeFi lending protocols.
## Core Lending Concepts
| Concept | Description |
|---------|-------------|
| Collateral Factor | Max borrow % against collateral |
| Liquidation Threshold | Health level triggering liquidation |
| Utilization Rate | Borrowed / Total Supplied |
| Interest Rate | Function of utilization |
| Health Factor | Collateral value / Debt value |
---
## Why Lending Protocol Attacks Happen (Root Causes)
### Root Cause 1: Health Factor Calculation Timing
The fundamental vulnerability: health factors computed from stale or cached data instead of real-time prices.
```solidity
// VULNERABLE: Cached health factor
mapping(address => uint256) public healthFactors;
function liquidate(address user) external {
// Uses stale cached value
require(healthFactors[user] < 1e18);
// Actual health factor may be different!
}
// SECURE: Compute in real-time
function liquidate(address user) external {
uint256 collateralValue = getCollateralValue(user); // Real-time oracle
uint256 debtValue = getDebtValue(user);
uint256 healthFactor = collateralValue * 1e18 / debtValue;
require(healthFactor < 1e18);
}
```
**Attacker's view**: "The protocol checks a stale health factor. I can manipulate prices between the check and liquidation, or liquidate users who are actually safe."
### Root Cause 2: Liquidation Incentive Misconfiguration
Liquidation bonus set higher than the safety margin, making self-liquidation profitable.
```solidity
// VULNERABLE: Bonus exceeds safety margin
uint256 constant MAX_LTV = 80e16; // 80%
uint256 constant LIQUIDATION_BONUS = 15e16; // 15%
// Attacker borrows at 80% LTV, self-liquidates with 15% bonus
// Net profit: 15% - (100% - 80%) = -5%... wait, this is a loss
// But if bonus > (100% - LTV), it's profitable!
uint256 constant LIQUIDATION_BONUS = 25e16; // 25% (DANGEROUS!)
// Profit: 25% - 20% = 5% free money
```
**Attacker's view**: "I borrow at max LTV, wait for any price movement, then self-liquidate and pocket the bonus."
### Root Cause 3: Interest Precision Loss
Small amounts round to zero during interest accrual, allowing dust attacks or precision exploits.
```solidity
// VULNERABLE: Interest rounds to 0
uint256 interest = principal * rate / SECONDS_PER_YEAR;
// If principal * rate < SECONDS_PER_YEAR, interest = 0
// Example: principal = 100 wei, rate = 1e16 (1%)
// interest = 100 * 1e16 / 31536000 = 0 (rounds down)
// SECURE: Use high-precision accumulator
uint256 interestAccumulator; // Ray (1e27) precision
uint256 newAccumulator = oldAccumulator + (rate * 1e27 / SECONDS_PER_YEAR);
```
**Attacker's view**: "I can deposit tiny amounts that accrue zero interest, or exploit rounding to avoid paying interest on small borrows."
### Root Cause 4: Collateral Valuation Trust
Collateral value depends entirely on oracle accuracy. Stale, manipulated, or incorrect oracles break the entire protocol.
```solidity
// VULNERABLE: Single oracle, no staleness check
function getCollateralValue(address user) public view returns (uint256) {
uint256 price = oracle.getPrice(collateralToken); // Could be stale!
return userBalance * price / 1e18;
}
// SECURE: Validate oracle freshness
function getCollateralValue(address user) public view returns (uint256) {
(uint256 price, uint256 timestamp) = oracle.getPriceWithTimestamp(collateralToken);
require(block.timestamp - timestamp <= MAX_STALENESS);
return userBalance * price / 1e18;
}
```
**Attacker's view**: "If I can manipulate the oracle or use a stale price, I can inflate collateral value and borrow more than safe."
### Root Cause 5: Bad Debt Socialization Gap
When liquidation fails to recover full debt, bad debt accumulates and is socialized across remaining suppliers, creating insolvency.
```solidity
// VULNERABLE: No bad debt handling
function liquidate(address user) external {
uint256 collateralValue = getCollateralValue(user);
uint256 debtValue = getDebtValue(user);
// If collateral < debt, bad debt remains!
// Suppliers lose money
}
// SECURE: Handle underwater positions
function liquidate(address user) external {
uint256 collateralValue = getCollateralValue(user);
uint256 debtValue = getDebtValue(user);
if (collateralValue < debtValue) {
uint256 badDebt = debtValue - collateralValue;
// Socialize or use insurance fund
insuranceFund -= min(badDebt, insuranceFund);
}
}
```
**Attacker's view**: "If I can create a position where collateral < debt, the protocol becomes insolvent and I profit from the socialized loss."
---
## Liquidation Vulnerabilities
### 1. Profitable Self-Liquidation
**Root Cause**: Root Cause 2 (Liquidation Incentive Misconfiguration)
**Vulnerable Pattern:**
```solidity
// DANGEROUS: Liquidator bonus too high
uint256 constant LIQUIDATION_BONUS = 15e16; // 15%
function liquidate(address user, uint256 repayAmount) external {
// Attacker can:
// 1. Borrow at 80% LTV
// 2. Wait for tiny price drop
// 3. Self-liquidate with 15% bonus
// = Free 15% - (100% - 80%) = Net profit!
}
```
**Check:** Liquidation bonus should be less than (100% - Max LTV).
### 2. Liquidation Cascade
```solidity
// When one liquidation triggers another
// Large position liquidated → price drops → more liquidations
// Mitigations:
// - Gradual liquidation (partial)
// - Circuit breakers
// - Liquidation delays
```
### 3. Bad Debt Accumulation
**Root Cause**: Root Cause 5 (Bad Debt Socialization Gap)
**Vulnerable Pattern:**
```solidity
// DANGEROUS: No bad debt handling
function liquidate(address user) external {
// If collateral < debt after liquidation
// Bad debt remains in protocol
// Eventually becomes insolvent
}
// SECURE: Handle underwater positions
function liquidate(address user) external {
uint256 collateralValue = getCollateralValue(user);
uint256 deb