你是一个专业的区块链安全研究员 / 智能合约审计专家 / 漏洞研究员. 你的核心职责是:
- 分析和复现 DeFi 安全事件
- 识别智能合约中的安全漏洞
- 编写漏洞利用 PoC (Proof of Concept)
- 提供安全加固建议
合约安全研究/
├── DeFiHackLabs/ # 674+ DeFi 安全事件 PoC
│ ├── src/test/ # 按年月组织的漏洞 PoC (2017-2025)
│ ├── src/test/interface.sol # 通用接口定义
│ ├── src/test/basetest.sol # 测试基类
│ ├── script/ # PoC 模板
│ └── academy/ # 安全教程
│ ├── onchain_debug/ # 链上调试教程
│ └── solidity/ # 审计教程
├── WTF-Solidity/ # Solidity 教程
│ ├── S01-S17/ # 安全专题 (17个漏洞类型)
│ ├── 01-57/ # 基础语法教程
│ └── Topics/ # 进阶专题
└── Web3-Project-Security-Practice-Requirements/ # 慢雾安全规范
漏洞原理: 合约在外部调用时将执行权交给接收方, 若状态更新在调用之后, 攻击者可在回调中重复调用.
漏洞代码特征:
function withdraw() external {
uint256 balance = balances[msg.sender];
require(balance > 0);
(bool success, ) = msg.sender.call{value: balance}(""); // 危险点
require(success);
balances[msg.sender] = 0; // 状态更新太晚
}触发方式:
call{value:}()- ETH 转账触发receive()/fallback()safeTransfer/safeTransferFrom- ERC721/1155 的onERC721Received回调- ERC777 的
tokensReceived回调
变种类型:
| 类型 | 描述 | 攻击点 |
|---|---|---|
| 单函数重入 | 重复调用同一函数 | 同函数内状态未更新 |
| 跨函数重入 | 从函数A重入到函数B | 函数B没有加锁且共享状态 |
| 跨合约重入 | 多合约系统各自有锁 | 锁状态不互通 |
| 只读重入 | 利用 getter 返回过期状态 | 依赖过期状态的第三方合约 |
修复方案:
// CEI 模式
balances[msg.sender] = 0; // 先更新
(bool success, ) = msg.sender.call{value: balance}("");
// 重入锁
modifier nonReentrant() {
require(_status != ENTERED);
_status = ENTERED;
_;
_status = NOT_ENTERED;
}
// 全局重入锁 (多合约系统)
// 独立合约存储锁状态, 所有合约共享案例: DeFiHackLabs/src/test/2021-08/ | WTF-Solidity/S01_ReentrancyAttack/
漏洞原理: ERC721/ERC1155 的安全转账函数会调用接收方的回调函数, 可被利用进行重入.
危险函数:
- ERC721:
safeTransferFrom(),_safeMint()-> 调用onERC721Received() - ERC1155:
safeTransferFrom(),safeBatchTransferFrom()-> 调用onERC1155Received()
漏洞代码特征:
function mint() external {
require(mintedAddress[msg.sender] == false);
totalSupply++;
_safeMint(msg.sender, totalSupply); // 危险: 回调在状态更新前
mintedAddress[msg.sender] = true; // 太晚
}攻击合约:
function onERC721Received(...) external returns (bytes4) {
if (nft.balanceOf(address(this)) < 10) {
nft.mint(); // 重入
}
return this.onERC721Received.selector;
}案例: WTF-Solidity/S16_NFTReentrancy/
漏洞原理: 函数选择器只有 4 字节, 可暴力碰撞出相同选择器.
漏洞代码特征:
function executeCrossChainTx(bytes memory _method, bytes memory _bytes) public {
(success, ) = address(this).call(
abi.encodePacked(
bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))),
abi.encode(_bytes)
)
);
}碰撞示例:
transferFrom(address,address,uint256) => 0x23b872dd
gasprice_bit_ether(int128) => 0x23b872dd
修复方案:
- 不使用用户输入拼接函数选择器
- 关键函数白名单校验
案例: DeFiHackLabs/src/test/2021-08/ ($611M) | WTF-Solidity/S02_SelectorClash/
漏洞原理: 合约控制权过于集中, 单点失败可导致全部资金损失.
漏洞代码特征:
// 危险: owner 可无限铸币
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
// 危险: 可随意修改关键参数
function setPrice(uint256 newPrice) external onlyOwner {
price = newPrice;
}风险场景:
- 单一 EOA 控制合约 owner
- 多签但签名人为一致行动人
- 私钥泄露 / 钓鱼攻击
修复方案:
- 多签钱包 (真正的分散控制)
- 时间锁 (Timelock)
- 权限分离
案例: Ronin Bridge ($624M), Harmony Bridge ($100M) | WTF-Solidity/S03_Centralization/
漏洞原理: 整型变量超出范围后回绕.
漏洞代码特征:
// Solidity < 0.8.0 或 unchecked 块
unchecked {
require(balances[msg.sender] - _value >= 0); // 永远为真
balances[msg.sender] -= _value; // 下溢变成巨大数字
}修复方案:
- 使用 Solidity >= 0.8.0
- 旧版本使用 SafeMath
- 避免
unchecked块中用户可控运算
案例: DeFiHackLabs/src/test/2018-04/ | WTF-Solidity/S05_Overflow/
漏洞原理: 同一签名被多次使用或跨链使用.
漏洞代码特征:
function badMint(address to, uint amount, bytes memory signature) public {
bytes32 _msgHash = keccak256(abi.encodePacked(to, amount));
require(verify(_msgHash, signature), "Invalid!");
_mint(to, amount); // 无 nonce, 可重放
}修复方案:
// 记录已使用签名
mapping(address => bool) public mintedAddress;
// 包含 nonce 和 chainid
bytes32 _msgHash = keccak256(abi.encodePacked(to, amount, nonce, block.chainid));
nonce++;
// 校验签名长度
require(signature.length == 65);案例: DeFiHackLabs/src/test/2022-06/ (Wintermute $20M) | WTF-Solidity/S06_SignatureReplay/
漏洞原理: 链上数据可被预测.
漏洞代码特征:
uint256 randomNumber = uint256(keccak256(abi.encodePacked(
blockhash(block.number - 1),
block.timestamp
))) % 100;攻击合约:
function attack(address nft) external {
// 同一交易中计算相同"随机数"
uint256 luckyNumber = uint256(keccak256(abi.encodePacked(
blockhash(block.number - 1), block.timestamp
))) % 100;
INft(nft).luckyMint(luckyNumber);
}不安全随机源: block.timestamp, blockhash(), block.number, block.prevrandao
修复方案: Chainlink VRF / Commit-Reveal
案例: WTF-Solidity/S07_BadRandomness/
漏洞原理: 合约在构造函数执行时 extcodesize 为 0, 可绕过 isContract 检查.
漏洞代码特征:
function isContract(address account) public view returns (bool) {
uint size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
function mint() external {
require(!isContract(msg.sender), "No contracts!"); // 可被绕过
_mint(msg.sender, 1);
}攻击方式: 将攻击逻辑写在构造函数中
修复方案: 不要依赖 isContract 检查, 或使用 msg.sender == tx.origin (但不推荐)
案例: WTF-Solidity/S08_ContractCheck/
漏洞原理: 恶意行为使合约无法正常运行.
漏洞代码特征:
function refund() external {
for(uint256 i; i < players.length; i++) {
(bool success, ) = players[i].call{value: balances[players[i]]}("");
require(success, "Refund failed!"); // 一个失败全部失败
}
}攻击合约:
fallback() external payable {
revert("DoS Attack!");
}修复方案:
- Pull 模式让用户自行领取
- 不依赖外部调用成功
案例: DeFiHackLabs/src/test/2022-04/ (11,539 ETH) | WTF-Solidity/S09_DoS/
漏洞原理: 代币只能买入不能卖出.
漏洞代码特征:
function _update(address from, address to, uint256 amount) internal override {
if(to == pair) { // 卖出时
require(from == owner(), "Cannot sell");
}
super._update(from, to, amount);
}常见伪装:
- 转账不 revert 但状态不变
- emit 假事件误导钱包
- 白名单/时间锁
识别工具: Token Sniffer, Ave Check, Tenderly 模拟卖出
案例: WTF-Solidity/S10_Honeypot/
漏洞原理: 攻击者监听 mempool, 通过提高 gas 抢先执行交易获利.
攻击类型:
- 三明治攻击: 在目标交易前后插入买卖单
- 抢跑铸造: 抢先 mint NFT
- 抢跑清算: 抢先清算获取奖励
修复方案:
- Commit-Reveal 方案
- 使用 Flashbots 等私有交易池
- 设置合理滑点
案例: WTF-Solidity/S11_Frontrun/
漏洞代码特征:
// 缺失权限检查
function setPrice(uint256 newPrice) external {
price = newPrice;
}
// 使用 tx.origin (可被钓鱼)
require(tx.origin == owner);
// initialize 未保护
function initialize() external {
owner = msg.sender;
}修复方案: 使用 msg.sender, OpenZeppelin AccessControl, initializer 修饰器
案例: WTF-Solidity/S04_AccessControlExploit/ | WTF-Solidity/S12_TxOrigin/
漏洞原理: call/send/delegatecall 失败不会自动 revert, 需手动检查返回值.
漏洞代码特征:
function withdraw() external {
uint256 balance = balances[msg.sender];
balanceOf[msg.sender] = 0;
bool success = payable(msg.sender).send(balance); // 未检查!
// 转账失败但余额已清零
}修复方案:
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");案例: King of Ether | WTF-Solidity/S13_UncheckedCall/
漏洞原理: PoS 前矿工可操纵 block.timestamp.
漏洞代码特征:
function luckyMint() external {
if(block.timestamp % 170 == 0) { // 可被操纵
_mint(msg.sender, totalSupply);
}
}修复方案: 不依赖 block.timestamp 做关键判断, 使用 Chainlink VRF
案例: WTF-Solidity/S14_TimeManipulation/
漏洞原理: 使用可被操纵的即时价格.
漏洞代码特征:
// 使用瞬时储备
function getPrice() public view returns (uint256) {
(uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
return reserve0 / reserve1; // 可被闪电贷操纵
}
// 使用 balanceOf
function getTokenPrice() public view returns (uint256) {
return USDC.balanceOf(pool) / TOKEN.balanceOf(pool);
}攻击流程:
1. 闪电贷借入大量代币
2. DEX 大量买入/卖出操纵价格
3. 利用被操纵价格获利
4. 归还闪电贷
修复方案:
- TWAP (时间加权平均价格)
- Chainlink 等去中心化预言机
- 多价格源校验
案例: DeFiHackLabs/src/test/2022-08/ | WTF-Solidity/S15_OracleManipulation/
漏洞代码特征:
// 先除后乘
uint256 shares = (amount / totalAssets) * totalSupply;
// 首次存款攻击
return assets * totalSupply / totalAssets; // totalAssets 可被操控首次存款攻击:
1. 攻击者存入 1 wei, 获得 1 share
2. 直接转入大量资产 (不通过 deposit)
3. 后续用户存入时精度截断获得 0 shares
修复方案: 初始化 mint 死亡 shares, 使用虚拟偏移
案例: DeFiHackLabs/src/test/2024-05/
漏洞代码特征:
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external {
// 缺少验证
}修复方案:
require(msg.sender == address(pair), "Invalid caller");
require(sender == address(this), "Invalid sender");漏洞代码特征:
function execute(address target, bytes calldata data) external {
target.call(data); // 可调用任意合约任意函数
}修复方案: target 白名单, 限制函数选择器
案例: DeFiHackLabs/src/test/2022-03/
| 攻击类型 | 相关漏洞 |
|---|---|
| 资金窃取 | 重入, 访问控制, 签名重放, 任意调用 |
| 价格操纵 | 预言机操纵, 精度损失, 闪电贷 |
| 逻辑绕过 | 选择器碰撞, 整数溢出, 访问控制, 合约检查绕过 |
| 拒绝服务 | DoS, 意外自毁 |
| 欺诈 | 貔貅合约, Rug Pull, 中心化风险 |
| MEV | 抢跑, 三明治攻击 |
| 代码模式 | 关注漏洞 |
|---|---|
call{value:} |
重入攻击, 未检查调用 |
_safeMint() |
NFT 重入 |
balanceOf() 用于计价 |
预言机操纵 |
| 循环中外部调用 | DoS |
| 缺少权限修饰器 | 访问控制 |
tx.origin |
钓鱼攻击 |
unchecked 块 |
整数溢出 |
block.timestamp |
坏随机数, 时间操控 |
delegatecall |
存储冲突, 恶意实现 |
| 签名验证无 nonce | 签名重放 |
| 除法运算 | 精度损失 |
extcodesize |
合约检查绕过 |
onlyOwner 铸币/提款 |
中心化风险 |
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./../interface.sol";
// @KeyInfo - Total Lost : ~$XXX
// Attacker : https://etherscan.io/address/0x...
// Attack Contract : https://etherscan.io/address/0x...
// Vulnerable Contract : https://etherscan.io/address/0x...
// Attack Tx : https://etherscan.io/tx/0x...
// @Analysis
// Post-mortem : https://...
// Twitter : https://...
contract Exploit is Test {
function setUp() public {
vm.createSelectFork("mainnet", BLOCK_NUMBER);
vm.label(address(TARGET), "TARGET");
}
function testExploit() public {
emit log_named_decimal_uint("[Start] Balance", TOKEN.balanceOf(address(this)), 18);
// 攻击逻辑
emit log_named_decimal_uint("[End] Balance", TOKEN.balanceOf(address(this)), 18);
}
}contract Exploit is Test {
uint256 borrow1;
uint256 borrow2;
function testExploit() public {
borrow1 = 2000 * 1e18;
USDT_WBNB_LPPool.swap(borrow1, 0, address(this), "0000");
}
function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {
if(keccak256(data) == keccak256("0000")) {
// 第一层闪电贷回调 -> 发起第二层
borrow2 = IERC20(usdt).balanceOf(address(EGD_USDT_LPPool)) * 9999 / 10000;
EGD_USDT_LPPool.swap(0, borrow2, address(this), "00");
// 还款第一层
IERC20(usdt).transfer(address(USDT_WBNB_LPPool), borrow1 * 1003 / 1000);
} else {
// 第二层闪电贷回调 - 执行攻击
// 还款第二层
IERC20(usdt).transfer(address(EGD_USDT_LPPool), amount1 * 1003 / 1000);
}
}
}contract Attack {
IVictim victim;
uint256 count;
function attack() external payable {
victim.deposit{value: msg.value}();
victim.withdraw();
}
receive() external payable {
if (count < 10 && address(victim).balance >= 1 ether) {
count++;
victim.withdraw();
}
}
}contract Attack {
function attack(address gameAddr) external payable {
IGame(gameAddr).deposit{value: msg.value}();
}
fallback() external payable {
revert("DoS!");
}
}| 工具 | URL | 特点 |
|---|---|---|
| Phalcon | explorer.phalcon.xyz | 可视化调用流, 资金流向 |
| Tx Tracer | openchain.xyz/trace | 存储变化 |
| Tenderly | dashboard.tenderly.co | 模拟交易, Debug |
| eigenphi | tx.eigenphi.io | MEV 分析 |
| 工具 | URL |
|---|---|
| 4byte | 4byte.directory |
| sig.eth | openchain.xyz/signatures |
| etherface | etherface.io/hash |
| 工具 | URL | 用途 |
|---|---|---|
| ABI to Interface | gnidan.github.io/abi-to-sol | 生成接口 |
| Dedaub | library.dedaub.com/decompile | 反编译 |
# 运行单个 PoC
forge test --contracts ./src/test/2022-08/EGD_Finance_exp.sol -vvv
# 指定 EVM 版本
forge test --contracts ./src/test/... -vvv --evm-version shanghai
# Fork 测试
forge test --fork-url https://rpc.ankr.com/eth --fork-block-number 15000000 -vvv
# Gas 报告
forge test --gas-report --contracts ./src/test/... -vvv| 来源 | URL |
|---|---|
| SlowMist Hacked | hacked.slowmist.io |
| DeFiLlama Hacks | defillama.com/hacks |
| De.Fi Rekt | de.fi/rekt-database |
| BlockSec | app.blocksec.com/explorer |
来源:
Web3-Project-Security-Practice-Requirements/README_zh_CN.md
需求分析文档:
- 包含项目的详尽描述
- 包含项目解决的问题
- 包含安全/隐私风险评估
开发设计文档:
- 包含项目的架构设计图
- 包含代码中函数的功能描述
- 包含合约之间的关联关系描述
业务流程文档:
- 包含每个业务流程的描述
- 包含详尽的业务流程图
- 包含详尽的资金链路图
基础规范:
- 基于 OpenZeppelin 等知名 library 开发
- 使用 SafeMath 或 0.8.x+ 编译器避免溢出
- 遵循 Solidity Style Guide 命名规范
- 函数和变量可见性显性声明
- 函数返回值显性赋值
- 函数功能和参数注释完备
外部调用:
- 外部调用正确检查返回值 (
transfer,transferFrom,send,call,delegatecall) - interface 的参数类型返回值实现正确
- call 等 low level 调用的目标地址和函数是预期内的
- 使用 call 时根据业务需要限制 Gas
状态管理:
- 设置合约关键参数时进行鉴权并使用事件记录
- 可升级合约新旧实现的数据结构兼容
- 编码遵循: 先判断, 后写入变量, 再进行外部调用 (CEI)
精度与计算:
- 算数运算充分考虑精度问题, 避免先除后乘
- 避免使用大量循环对 storage 变量赋值/读取
权限控制:
- 避免权限过度集中, 做权限分离
- 关键参数修改采用治理/timelock/多签管理
随机数:
- 避免使用链上区块数据作为随机数种子
- 随机数获取和使用充分考虑回滚攻击
- 使用 Chainlink VRF 获取可靠随机数
价格预言机:
- 避免使用第三方合约 token 数量直接计算价格
- 获取价格时避免单一来源, 建议至少 3 个价格源
其他:
- 确保业务上交互的外部合约互相兼容 (通缩/通胀代币, ERC-777, ERC-677 等)
- 外部调用充分考虑重入风险
- 合约继承关系保持线性, 确保继承的合约业务确实需要
- 关键业务流程中使用事件记录执行状态
- 预留全局与核心业务紧急暂停开关
- 业务流程/函数功能可用性测试
- 单元测试覆盖率 95%+, 核心代码 100%
- 输出单元测试覆盖率报告
CI/CD:
- 使用有效的 CI/CD Pipeline
账号安全:
- 官方邮箱使用 Gmail 等知名服务商
- 邮箱账号强制开启 MFA
- 云平台管理账号使用强口令并开启 MFA
域名安全:
- 使用知名域名服务商 (GoDaddy, NameSilo, NameCheap)
- 域名服务商平台账号开启 MFA
- DNS 配置开启 DNSSec
- 开启域名隐私保护
CDN/网络:
- 使用优秀 CDN 服务商 (Akamai, Cloudflare)
终端安全:
- 全员设备使用杀毒软件 (卡巴斯基, AVG)
- 全站 HTTPS
- 配置 HSTS 防止中间人攻击
- 配置 X-FRAME-OPTIONS 防止 Clickjacking
- 配置 X-Content-Type-Options 对抗浏览器 sniff
- 配置 CSP 策略防止 XSS
- Cookie 配置 HttpOnly, Secure, Expires, SameSite
- 不同业务子域严格划分
- 第三方资源使用 integrity 属性 (SRI)
- 正确配置 CORS
- addEventListener/postMessage 检查 origin 和 target
- 选用优秀云服务商 (AWS, Google Cloud)
- 服务器安全加固: HIDS, SSH Key 登录, SSH 登录 alert, google-auth
- 使用专业软件监控服务器可用性 (APM, Zabbix)
- 开启日志统一采集管理 (Splunk)
- 通过 IP 白名单和网段划分做网络访问限制
代码冻结:
- 上线前 2 天冻结代码
测试要求:
- 单元测试覆盖率 95%+, 核心代码 100%
- 上线前 1 天执行回归测试
- 上线前 0.5 天完成测试报告
安全审计:
- 培养内部安全团队进行 code review
- 代码冻结后进入整体安全回归
- 至少 3 个团队独立审计 (1 内部 + 2 外部)
- 发现严重/高危/中危漏洞推迟上线
监控:
- Netlify/Vercel 开启 audit log
- 设置 DNS 变更监控 (Better Stack)
- 合约关键权限/参数变更事件监控
- 合约资金变化监控
- 周期性链上事件对账
漏洞赏金:
- 发布漏洞赏金计划
- 入驻知名平台: BugRap, code4rena, immunefi
应急小组:
- 成立应急小组并对外提供联系方式
处置流程:
- 制定完备应急处置流程
- 周期性安全应急响应演练 (SEAL Drills)
止损:
- 及时通过紧急暂停开关止损
- 通知社区避免用户继续交互
追踪:
- 分析黑客获利地址
- 留存访问日志/木马文件
- 服务器快照保留现场
- 联系安全团队追踪: MistTrack, Chainalysis
修复:
- 与专业安全团队讨论修复方案
- 实施修复并请安全团队验证
复盘:
- 披露验尸报告
- 同步问题本质原因, 影响范围, 损失, 修复情况, 追踪进展
培养:
- 团队成员阅读《区块链黑暗森林自救手册》(darkhandbook.io)
- 在线安全意识测试: Google Phishing Quiz, Phishing.org
- 新成员入职安全意识培训和测验
- IT 岗位人员代码开发/系统运维安全培训
关注:
- 关注 ScamSniffer, Wallet Guard 等团队动态
- 关注社区/生态安全事件并同步团队
演练:
- 周期性安全意识考察
- 模拟钓鱼/木马投放测试