DeFi AMM 安全性 是做什麼的?
針對 Solidity AMM 合約、LP 金庫及交換函數的關鍵漏洞模式與強化實作範例。
使用時機
- 撰寫或審計 Solidity AMM 或流動性池合約時
- 實作持有代幣餘額的 swap、deposit、withdraw、mint 或 burn 流程時
- 審查任何在份額或儲備數學中使用
token.balanceOf(address(this))的合約時 - 為 DeFi 協定新增費用設定器、暫停功能、預言機更新或其他管理員函數時
運作方式
將此技能作為檢查清單與模式庫使用。針對每個使用者入口點,根據以下類別進行審查,並優先使用強化範例而非自行實作的變體。
執行安全
本技能中的 shell 指令為本地審計範例。請僅在受信任的檢查環境或一次性沙盒中執行,切勿將未受信任的合約名稱、路徑、RPC URL、私鑰或使用者提供的旗標拼接至 shell 指令中。在安裝工具或執行可能消耗大量本地或付費資源的長時間模糊測試/靜態分析作業前,請先詢問。
切勿在指令範例、日誌或報告中包含機密資訊、私鑰、助記詞、API 權杖或主網簽署憑證。
範例
重入攻擊:強制執行 CEI 順序
危險版本:
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
token.transfer(msg.sender, amount);
balances[msg.sender] -= amount;
}
安全版本:
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
token.safeTransfer(msg.sender, amount);
}
當有已強化的函式庫可用時,請勿自行撰寫保護機制。
捐贈或通貨膨脹攻擊
直接使用 token.balanceOf(address(this)) 進行份額數學運算,會讓攻擊者能夠透過非預期路徑將代幣轉入合約來操控分母。
// 危險版本
function deposit(uint256 assets) external returns (uint256 shares) {
shares = (assets * totalShares) / token.balanceOf(address(this));
}
// 安全版本
uint256 private _totalAssets;
function deposit(uint256 assets) external nonReentrant returns (uint256 shares) {
uint256 balBefore = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), assets);
uint256 received = token.balanceOf(address(this)) - balBefore;
shares = totalShares == 0 ? received : (received * totalShares) / _totalAssets;
_totalAssets += received;
totalShares += shares;
}
追蹤內部記帳,並測量實際收到的代幣數量。
預言機操縱
即時價格可被閃電貸操縱。請優先使用 TWAP。
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 1800;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgos);
int24 twapTick = int24(
(tickCumulatives[1] - tickCumulatives[0]) / int56(uint56(30 minutes))
);
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(twapTick);
滑點保護
每個交換路徑都需要由呼叫者提供滑點參數與截止時間。
function swap(
uint256 amountIn,
uint256 amountOutMin,
uint256 deadline
) external returns (uint256 amountOut) {
require(block.timestamp <= deadline, "Expired");
amountOut = _calculateOut(amountIn);
require(amountOut >= amountOutMin, "Slippage exceeded");
_executeSwap(amountIn, amountOut);
}
安全儲備數學
import {FullMath} from "@uniswap/v3-core/contracts/libraries/FullMath.sol";
uint256 result = FullMath.mulDiv(a, b, c);
對於大型儲備數學運算,在存在溢位風險時,避免使用樸素的 a * b / c。
管理員控制
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
contract MyAMM is Ownable2Step {
function setFee(uint256 fee) external onlyOwner { ... }
function pause() external onlyOwner { ... }
}
對於所有權轉移,優先使用明確的接受機制,並為每個特權路徑設置閘門。
安全性檢查清單
- 暴露重入風險的入口點使用了
nonReentrant - 遵守 CEI 順序
- 份額數學不依賴原始的
balanceOf(address(this)) - ERC-20 轉帳使用
SafeERC20 - 存款時測量實際收到的代幣
- 預言機讀取使用 TWAP 或其他抗操縱來源
- 交換要求提供
amountOutMin和deadline - 對溢位敏感的儲備數學使用安全的基礎設施,例如
mulDiv - 管理員函數具備存取控制
- 存在緊急暫停功能並經過測試
- 在上線前執行靜態分析和模糊測試
審計工具
pip install slither-analyzer
slither . --exclude-dependencies
echidna-test . --contract YourAMM --config echidna.yaml
forge test --fuzz-runs 10000