常见漏洞 #
一、重入攻击 #
1.1 漏洞描述 #
重入攻击是最著名的智能合约漏洞之一,攻击者利用合约在更新状态前进行外部调用的漏洞。
1.2 漏洞示例 #
solidity
// 有漏洞的合约
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
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; // 状态更新太晚
}
}
1.3 攻击合约 #
solidity
contract Attack {
VulnerableBank public bank;
constructor(address _bank) {
bank = VulnerableBank(_bank);
}
receive() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw(); // 重入调用
}
}
function attack() public payable {
bank.deposit{value: 1 ether}();
bank.withdraw();
}
}
1.4 防御方案 #
solidity
contract SafeBank {
mapping(address => uint256) public balances;
// 方案1:检查-生效-交互模式
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// 1. 检查(已在上面的require)
// 2. 生效:先更新状态
balances[msg.sender] = 0;
// 3. 交互:后转账
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
contract SafeBankWithReentrancyGuard {
mapping(address => uint256) public balances;
bool private _locked;
// 方案2:重入锁
modifier nonReentrant() {
require(!_locked, "Reentrant call");
_locked = true;
_;
_locked = false;
}
function withdraw() public nonReentrant {
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");
}
}
二、整数溢出 #
2.1 漏洞描述 #
整数溢出发生在算术运算结果超出类型范围时。
2.2 Solidity 0.8.x 的改进 #
solidity
contract OverflowProtection {
// Solidity 0.8.x 内置溢出检查
function safeAdd(uint8 a, uint8 b) public pure returns (uint8) {
return a + b; // 如果溢出会自动revert
}
}
// 0.7.x及以下版本需要SafeMath
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
}
2.3 unchecked块 #
solidity
contract UncheckedExample {
function unsafeAdd(uint256 a, uint256 b) public pure returns (uint256) {
unchecked {
return a + b; // 绕过溢出检查,可能回绕
}
}
}
三、权限漏洞 #
3.1 tx.origin钓鱼攻击 #
solidity
// 有漏洞的合约
contract VulnerableWallet {
address public owner;
constructor() {
owner = msg.sender;
}
// 危险:使用tx.origin检查权限
function transfer(address to, uint256 amount) public {
require(tx.origin == owner, "Not authorized");
payable(to).transfer(amount);
}
}
// 攻击合约
contract PhishingAttack {
VulnerableWallet public wallet;
constructor(address _wallet) {
wallet = VulnerableWallet(_wallet);
}
receive() external payable {
// 如果owner调用了这个合约,会绕过检查
wallet.transfer(msg.sender, address(wallet).balance);
}
}
// 安全的做法
contract SafeWallet {
address public owner;
constructor() {
owner = msg.sender;
}
function transfer(address to, uint256 amount) public {
require(msg.sender == owner, "Not authorized"); // 安全
payable(to).transfer(amount);
}
}
3.2 未初始化的存储指针 #
solidity
// 有漏洞的合约
contract VulnerableStorage {
struct User {
string name;
uint256 balance;
}
mapping(address => User) public users;
// 危险:未初始化的存储指针
function updateUser(string memory _name) public {
User storage user; // 未初始化,指向slot 0
user.name = _name;
}
}
// 安全的做法
contract SafeStorage {
struct User {
string name;
uint256 balance;
}
mapping(address => User) public users;
function updateUser(string memory _name) public {
User storage user = users[msg.sender]; // 正确初始化
user.name = _name;
}
}
四、可见性问题 #
4.1 意外的public函数 #
solidity
// 有漏洞的合约
contract VulnerableVisibility {
address public owner;
uint256 private secret;
// 忘记指定可见性,默认为public
function initOwner() {
owner = msg.sender;
}
// 或者错误地设置为external
function setSecret(uint256 _secret) external {
secret = _secret;
}
}
// 安全的做法
contract SafeVisibility {
address public owner;
uint256 private secret;
function initOwner() internal { // 明确指定internal
owner = msg.sender;
}
function setSecret(uint256 _secret) internal {
secret = _secret;
}
}
五、随机数问题 #
5.1 可预测的随机数 #
solidity
// 有漏洞的合约
contract VulnerableRandom {
// 危险:使用区块变量作为随机数源
function random() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty)));
}
function play() public view returns (bool) {
return random() % 2 == 0;
}
}
// 矿工可以操纵block.timestamp和block.difficulty
// 更好的方案:使用Chainlink VRF
interface VRFCoordinator {
function requestRandomWords() external returns (uint256);
}
六、拒绝服务攻击 #
6.1 循环DoS #
solidity
// 有漏洞的合约
contract VulnerableRefund {
address[] public investors;
function refundAll() public {
for (uint256 i = 0; i < investors.length; i++) {
payable(investors[i]).transfer(1 ether); // 如果一个失败,全部失败
}
}
}
// 安全的做法
contract SafeRefund {
address[] public investors;
mapping(address => bool) public refunded;
function claimRefund() public {
require(!refunded[msg.sender], "Already refunded");
refunded[msg.sender] = true;
payable(msg.sender).transfer(1 ether);
}
}
七、最佳实践 #
7.1 安全检查清单 #
| 检查项 | 说明 |
|---|---|
| 重入保护 | 使用nonReentrant修饰器 |
| 整数溢出 | 使用Solidity 0.8.x或SafeMath |
| 权限检查 | 使用msg.sender而非tx.origin |
| 可见性 | 明确指定所有函数可见性 |
| 随机数 | 使用Chainlink VRF等安全方案 |
| 外部调用 | 检查返回值,处理失败情况 |
7.2 使用OpenZeppelin #
solidity
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
contract SecureContract is ReentrancyGuard, Ownable, Pausable {
mapping(address => uint256) public balances;
function withdraw() public nonReentrant whenNotPaused {
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");
}
}
八、总结 #
常见漏洞要点:
| 漏洞 | 防御方案 |
|---|---|
| 重入攻击 | 检查-生效-交互模式、重入锁 |
| 整数溢出 | 使用Solidity 0.8.x |
| 权限漏洞 | 使用msg.sender |
| 可见性问题 | 明确指定可见性 |
| 随机数 | 使用Chainlink VRF |
| DoS攻击 | 避免批量操作依赖 |
接下来,让我们学习安全模式!
最后更新:2026-03-27