Defi Security Considerations
Security guidance specific to decentralized finance protocols. DeFi protocols face unique security challenges due to their financial nature, composability, and...
DeFi Security Considerations
Security guidance specific to decentralized finance protocols.
Overview
DeFi protocols face unique security challenges due to their financial nature, composability, and adversarial environment. This guide covers DeFi-specific security considerations.
Oracle Security
Price Oracle Risks
| Risk | Description |
|---|---|
| Manipulation | Attacker manipulates price feed |
| Stale data | Oracle returns outdated prices |
| Flash loan attacks | Price manipulation via flash loans |
| Single point of failure | Reliance on one oracle |
Secure Oracle Pattern
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract SecurePriceFeed {
AggregatorV3Interface internal priceFeed;
uint256 public constant STALE_THRESHOLD = 1 hours;
function getPrice() public view returns (uint256) {
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// Check for stale data
require(
updatedAt >= block.timestamp - STALE_THRESHOLD,
"Stale price data"
);
// Check round completeness
require(answeredInRound >= roundId, "Incomplete round");
// Check for valid price
require(price > 0, "Invalid price");
return uint256(price);
}
}
TWAP for Manipulation Resistance
// Use time-weighted average price
function getTWAP(address pool, uint32 period) public view returns (uint256) {
uint32[] memory secondsAgos = new uint32;
secondsAgos[0] = period;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 tick = int24(tickCumulativesDelta / int56(uint56(period)));
return OracleLibrary.getQuoteAtTick(tick, amount, tokenIn, tokenOut);
}
Flash Loan Protection
Understanding Flash Loans
Flash loans allow borrowing without collateral if repaid in same transaction:
- Can manipulate prices
- Can drain liquidity pools
- Can exploit reentrancy
- Can bypass governance
Protection Patterns
contract FlashLoanSafe {
mapping(address => uint256) private _lastBlock;
// Prevent same-block operations
modifier noFlashLoan() {
require(
_lastBlock[msg.sender] != block.number,
"Same block not allowed"
);
_lastBlock[msg.sender] = block.number;
_;
}
function sensitiveOperation() external noFlashLoan {
// Protected from flash loan manipulation
}
}
Delay-Based Protection
contract DelayedWithdraw {
struct Withdrawal {
uint256 amount;
uint256 unlockTime;
}
mapping(address => Withdrawal) public withdrawals;
uint256 public constant DELAY = 1 days;
function requestWithdraw(uint256 amount) external {
withdrawals[msg.sender] = Withdrawal({
amount: amount,
unlockTime: block.timestamp + DELAY
});
}
function executeWithdraw() external {
Withdrawal memory w = withdrawals[msg.sender];
require(block.timestamp >= w.unlockTime, "Too early");
delete withdrawals[msg.sender];
// ... transfer
}
}
Liquidity Pool Security
Slippage Protection
function swap(
uint256 amountIn,
uint256 minAmountOut, // Slippage protection
uint256 deadline // Deadline protection
) external {
require(block.timestamp <= deadline, "Transaction expired");
uint256 amountOut = calculateSwapOutput(amountIn);
require(amountOut >= minAmountOut, "Insufficient output");
// Execute swap
}
First Depositor Attack Protection
contract SafeVault {
uint256 public constant MINIMUM_LIQUIDITY = 1000;
function deposit(uint256 amount) external returns (uint256 shares) {
uint256 supply = totalSupply;
if (supply == 0) {
// First deposit: lock minimum liquidity
shares = amount - MINIMUM_LIQUIDITY;
_mint(address(0), MINIMUM_LIQUIDITY); // Burn forever
} else {
shares = amount * supply / totalAssets();
}
require(shares > 0, "Zero shares");
_mint(msg.sender, shares);
}
}
Governance Security
Timelock for Critical Changes
contract Timelock {
uint256 public constant DELAY = 2 days;
mapping(bytes32 => uint256) public queuedTransactions;
function queueTransaction(
address target,
bytes calldata data
) external onlyAdmin returns (bytes32) {
bytes32 txHash = keccak256(abi.encode(target, data, block.timestamp));
queuedTransactions[txHash] = block.timestamp + DELAY;
return txHash;
}
function executeTransaction(
address target,
bytes calldata data,
uint256 eta
) external onlyAdmin {
bytes32 txHash = keccak256(abi.encode(target, data, eta));
require(queuedTransactions[txHash] != 0, "Not queued");
require(block.timestamp >= queuedTransactions[txHash], "Not ready");
delete queuedTransactions[txHash];
(bool success, ) = target.call(data);
require(success);
}
}
Vote Escrow Pattern
// Prevent flash loan governance attacks
function vote(uint256 proposalId, bool support) external {
require(
balanceOf[msg.sender] > 0 &&
lastTransfer[msg.sender] < proposalSnapshot[proposalId],
"Must hold tokens before proposal"
);
// ... voting logic
}
Token Security
Fee-on-Transfer Token Handling
function depositWithFeeToken(IERC20 token, uint256 amount) external {
uint256 balanceBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - balanceBefore;
// Use 'received' not 'amount'
deposits[msg.sender] += received;
}
Rebasing Token Handling
contract RebaseAwareVault {
// Track shares, not amounts
mapping(address => uint256) public shares;
uint256 public totalShares;
function deposit(uint256 amount) external {
uint256 totalAssets = token.balanceOf(address(this));
uint256 newShares = totalShares == 0
? amount
: amount * totalShares / totalAssets;
shares[msg.sender] += newShares;
totalShares += newShares;
token.safeTransferFrom(msg.sender, address(this), amount);
}
function withdraw(uint256 shareAmount) external {
uint256 totalAssets = token.balanceOf(address(this));
uint256 amount = shareAmount * totalAssets / totalShares;
shares[msg.sender] -= shareAmount;
totalShares -= shareAmount;
token.safeTransfer(msg.sender, amount);
}
}
Composability Risks
Protocol Dependency Risks
| Risk | Mitigation |
|---|---|
| Protocol upgrade breaks integration | Pin to specific versions |
| Protocol gets exploited | Limit exposure, circuit breakers |
| Protocol changes fees | Account for variable fees |
| Protocol gets paused | Have fallback mechanisms |
Circuit Breakers
contract ProtectedVault {
uint256 public constant MAX_SINGLE_WITHDRAWAL = 1000 ether;
uint256 public constant DAILY_LIMIT = 10000 ether;
uint256 public dailyWithdrawn;
uint256 public lastWithdrawDay;
function withdraw(uint256 amount) external {
require(amount <= MAX_SINGLE_WITHDRAWAL, "Amount too large");
uint256 today = block.timestamp / 1 days;
if (today > lastWithdrawDay) {
dailyWithdrawn = 0;
lastWithdrawDay = today;
}
require(dailyWithdrawn + amount <= DAILY_LIMIT, "Daily limit exceeded");
dailyWithdrawn += amount;
// ... withdrawal logic
}
}
Common DeFi Attack Vectors
1. Price Manipulation
Attack: Manipulate oracle price to profit
Mitigation: TWAP oracles, multiple sources, bounds checks
2. Sandwich Attacks
Attack: Front-run and back-run user transactions
Mitigation: Private mempools, slippage protection, commit-reveal
3. Reentrancy in DeFi
Attack: Re-enter during callback to drain funds
Mitigation: CEI pattern, reentrancy guards, checks before external calls
4. Governance Attacks
Attack: Flash loan to pass malicious proposals
Mitigation: Time locks, snapshot voting, quorum requirements
5. Economic Exploits
Attack: Profit from protocol design flaws
Mitigation: Economic modeling, invariant testing, rate limiting
Testing DeFi Protocols
Invariant Testing
function invariant_totalAssetsMatchesDeposits() public {
assertGe(
vault.totalAssets(),
vault.totalDeposited(),
"Assets must match deposits"
);
}
function invariant_noFreeMoney() public {
uint256 before = vault.totalAssets();
// ... operations
uint256 after = vault.totalAssets();
assertLe(
after,
before + maxExpectedGain,
"Unexpected asset increase"
);
}
Economic Simulation
Test edge cases:
- Very large deposits/withdrawals
- Multiple users competing
- Extreme price movements
- Flash loan scenarios
- Partial liquidations
Audit Focus Areas
When reviewing DeFi code, prioritize:
- Oracle interactions - Stale data, manipulation
- External calls - Reentrancy, return values
- Token handling - Non-standard tokens
- Access control - Admin functions
- Economic logic - Rounding, precision
- Upgrade paths - Storage collisions
Next Steps
- Common Vulnerability Patterns - General vulnerabilities
- Pre-Audit Checklist - Audit preparation
- Continuous Security - Ongoing practices