DeFi AMM 보안
Solidity AMM 컨트랙트, LP 볼트, 스왑 함수를 위한 주요 취약점 패턴 및 강화된 구현입니다.
사용 시점
- Solidity AMM 또는 유동성 풀 컨트랙트를 작성하거나 감사할 때
- 토큰 잔액을 보유하는 스왑, 입금, 출금, 발행, 소각 흐름을 구현할 때
token.balanceOf(address(this))를 공유 또는 준비금 계산에 사용하는 컨트랙트를 검토할 때- DeFi 프로토콜에 수수료 설정자, 일시 중지 함수, 오라클 업데이트 또는 기타 관리자 함수를 추가할 때
작동 방식
이 스킬을 체크리스트 겸 패턴 라이브러리로 사용하세요. 아래 카테고리를 기준으로 모든 사용자 진입 지점을 검토하고, 직접 만든 변형보다 강화된 예제를 선호하세요.
실행 안전성
이 스킬의 셸 명령어는 로컬 감사 예제입니다. 신뢰할 수 있는 체크아웃 또는 일회용 샌드박스에서만 실행하고, 신뢰할 수 없는 컨트랙트 이름, 경로, RPC URL, 개인 키 또는 사용자 제공 플래그를 셸 명령어에 포함하지 마십시오. 도구를 설치하거나 로컬 또는 유료 리소스를 많이 소비할 수 있는 장기 퍼징/정적 분석 작업을 실행하기 전에 문의하세요.
명령어 예제, 로그 또는 보고서에 비밀, 개인 키, 시드 문구, 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