前言
2026年,DeFi协议总锁仓量已突破千亿美元,但智能合约安全事件造成的损失同样触目惊心。据统计,仅今年第一季度,合约漏洞导致的资产损失就超过了3亿美元。这些损失的背后,往往不是复杂的密码学攻击,而是一些看似简单的编程错误——缺失的校验、松弛的权限控制、未处理边界情况的数值计算。
安全不是事后补丁,而是开发流程的一部分。本文将用大量真实案例和防御代码,带你系统性地认识Solidity合约中的常见漏洞类型。这些漏洞每一条都曾在生产环境中造成严重损失,理解它们是写出安全合约的第一步。

重入攻击:跨函数调用埋下的定时炸弹
漏洞原理
重入攻击是智能合约安全中最经典、也是最危险的漏洞类型。它的本质是:当合约调用外部地址时,外部代码有机会在你的合约状态更新前再次执行。如果你的合约逻辑没有考虑到这种”回滚”的可能性,攻击者就能通过递归调用耗尽合约资金。
让我们看一个典型的存在漏洞的提款合约:
solidity
// 存在重入漏洞的合约
contract VulnerableBank {
mapping(address => uint256) public balances;
// 存在漏洞的提款函数
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
// 危险:先转账后清零
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0; // 问题:状态更新在外部调用之后
}
receive() external payable {
balances[msg.sender] += msg.value;
}
}
问题出在哪里?当合约通过call转账ETH时,如果接收方是一个恶意合约,它可以在receive函数中再次调用withdraw。由于balances[msg.sender]还没被清零,第二次调用仍然能通过余额检查,合约就这样被反复掏空。
2016年的The DAO事件就是典型案例,攻击者通过这种手法盗走了价值6000万美元的ETH。
防御策略一:检查-生效-交互模式
最基础的防御是确保状态更新在外部调用之前完成:
solidity
// 安全的提款实现
contract SecureBankV1 {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
// 第一步:检查(验证前置条件)
require(balances[msg.sender] >= amount, "Insufficient balance");
// 第二步:生效(更新状态)
balances[msg.sender] -= amount;
// 第三步:交互(与外部地址通信)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
现在即使攻击合约递归调用,balances已经被减掉,第二次调用会直接触发require失败。
防御策略二:互斥锁
对于更复杂的业务逻辑,可以使用互斥锁防止函数在执行期间被重入:
solidity
// 使用互斥锁防止重入
contract SecureBankV2 {
mapping(address => uint256) public balances;
mapping(address => bool) private isExecuting;
modifier noReentrancy() {
require(!isExecuting[msg.sender], "Reentrancy detected");
isExecuting[msg.sender] = true;
_;
isExecuting[msg.sender] = false;
}
function withdraw(uint256 amount) external noReentrancy {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
防御策略三:使用Transfer Helper
OpenZeppelin提供了成熟的安全转账工具:
solidity
// 使用SafeTransferHelper
contract SecureBankV3 {
using Address for address payable;
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
// OpenZeppelin的安全转账会自动检查返回值
payable(msg.sender).sendValue(amount);
}
}
跨合约重入的隐蔽陷阱
有时候重入不一定是跨函数调用,而是跨合约调用:
solidity
// 看似安全的NFT转账,实际上存在跨合约重入
contract VulnerableNFTMarket {
mapping(uint256 => uint256) public prices;
function buyItem(address nftContract, uint256 tokenId) external {
uint256 price = prices[tokenId];
require(msg.value >= price, "Insufficient payment");
// 购买时先转账NFT
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
// 再支付给卖家
payable(ownerOf(tokenId)).transfer(price); // 这里可能触发重入
}
}
防御方法是确保所有状态更新完成后,再进行外部调用:
solidity
// 安全的NFT市场实现
contract SecureNFTMarket {
mapping(uint256 => address) public owners;
mapping(uint256 => uint256) public prices;
function buyItem(address nftContract, uint256 tokenId) external payable {
uint256 price = prices[tokenId];
address seller = owners[tokenId];
require(msg.value >= price, "Insufficient payment");
// 预先计算:先记录所有需要的状态变更
uint256 excess = msg.value - price;
// 清空状态(防止重入)
prices[tokenId] = 0;
owners[tokenId] = address(0);
// 执行NFT转账
IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
// 最后转账资金
payable(seller).transfer(price);
payable(msg.sender).transfer(excess);
}
}
整数溢出与下溢:数字运算的暗礁
Solidity 0.8+的原生保护
在Solidity 0.8之前,整数溢出是一个严重问题:
solidity
// Solidity 0.7.x - uint256最大值为2^256-1
uint256 public counter = 0;
counter++; // 如果counter已是最大值,会"绕回"到0
从Solidity 0.8开始,算术运算会自动检查溢出,如果发生溢出会回滚交易:
solidity
// Solidity 0.8+ 不需要SafeMath了
uint256 public counter = type(uint256).max;
counter++; // 自动回滚,不再是安全漏洞
但这不意味着你可以忽视数值安全,以下场景仍需特别注意:
精度损失问题
DeFi合约中最常见的问题之一是精度损失导致资金漏洞:
solidity
// 有精度风险的借贷合约
contract VulnerableLoan {
function calculateInterest(
uint256 principal,
uint256 rate, // 百分比,如50代表5%
uint256 duration // 秒数
) public pure returns (uint256) {
// 错误的计算方式:乘法优先级问题
return principal * rate * duration / 1000 / 365 days;
}
}
正确的做法是先放大精度再计算:
solidity
// 安全的高精度利率计算
contract SecureLoan {
uint256 public constant PRECISION = 1e18;
function calculateInterest(
uint256 principal,
uint256 rate, // 使用basis points,如500表示5%
uint256 duration // 秒数
) public pure returns (uint256) {
// 将利率转为高精度数
uint256 ratePerSecond = (rate * PRECISION) / 10000 / 365 days;
// 确保不会溢出:使用SafeMath(如果不用0.8+)
return (principal * ratePerSecond * duration) / PRECISION;
}
}
除法向下取整的陷阱
solidity
// 不安全的分配函数
contract VulnerableDistributor {
address[] public recipients;
uint256 public totalAmount;
function distribute() external {
uint256 count = recipients.length;
uint256 amountPerPerson = totalAmount / count; // 可能丢失余数
for (uint256 i = 0; i < count; i++) {
payable(recipients[i]).transfer(amountPerPerson);
}
// 剩余的 wei 被永久锁定在合约中
}
}
solidity
// 安全的分配实现
contract SecureDistributor {
address[] public recipients;
mapping(address => uint256) public pendingWithdrawals;
uint256 public totalAmount;
uint256 public distributed;
function distribute() external {
uint256 count = recipients.length;
require(count > 0, "No recipients");
uint256 amountPerPerson = totalAmount / count;
uint256 remainder = totalAmount % count; // 明确处理余数
for (uint256 i = 0; i < count; i++) {
pendingWithdrawals[recipients[i]] += amountPerPerson;
}
// 将余数存入合约,可由管理员提取
distributed = totalAmount - remainder;
}
function withdraw() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "Nothing to withdraw");
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
权限控制缺陷:谁有权做什么?
缺失的权限校验
最常见的权限漏洞是应该检查调用者身份的地方被遗漏:
solidity
// 存在权限漏洞的合约
contract VulnerableVault {
address public owner;
mapping(address => uint256) public deposits;
constructor() {
owner = msg.sender;
}
// 缺失onlyOwner修饰符 - 任何人都能调用
function emergencyWithdraw() external {
payable(owner).transfer(address(this).balance);
}
// 正确写法应该是:
// modifier onlyOwner() {
// require(msg.sender == owner, "Not owner");
// _;
// }
}
使用OpenZeppelin的AccessControl
OpenZeppelin提供了成熟的权限管理系统:
solidity
// 使用AccessControl的安全合约
contract SecureVault is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant WITHDRAWER_ROLE = keccak256("WITHDRAWER_ROLE");
mapping(address => uint256) public deposits;
constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
}
// 只有管理员才能存款
function deposit(address user) external payable onlyRole(ADMIN_ROLE) {
deposits[user] += msg.value;
}
// 只有提现权限的角色才能提取
function withdraw(uint256 amount) external onlyRole(WITHDRAWER_ROLE) {
require(deposits[msg.sender] >= amount, "Insufficient balance");
deposits[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
// 转移管理员权限
function transferAdmin(address newAdmin) external onlyRole(DEFAULT_ADMIN_ROLE) {
_grantRole(DEFAULT_ADMIN_ROLE, newAdmin);
_revokeRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
}
多签钱包的权限验证
solidity
// 简化的多签合约
contract SecureMultiSig {
struct Transaction {
address to;
uint256 value;
bytes data;
uint256 confirmations;
bool executed;
}
uint256 public requiredConfirmations;
Transaction[] public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
mapping(address => bool) public isOwner;
event ExecuteTransaction(uint256 indexed txId, address indexed to, uint256 value);
constructor(address[] memory owners, uint256 _required) {
require(owners.length > 0 && _required > 0 && _required <= owners.length);
for (uint256 i = 0; i < owners.length; i++) {
require(!isOwner[owners[i]], "Duplicate owner");
isOwner[owners[i]] = true;
}
requiredConfirmations = _required;
}
modifier onlyOwner() {
require(isOwner[msg.sender], "Not owner");
_;
}
function submitTransaction(address to, uint256 value, bytes memory data)
external onlyOwner returns (uint256)
{
transactions.push(Transaction({
to: to,
value: value,
data: data,
confirmations: 0,
executed: false
}));
return transactions.length - 1;
}
function confirmTransaction(uint256 txId) external onlyOwner {
require(!confirmations[txId][msg.sender], "Already confirmed");
require(!transactions[txId].executed, "Already executed");
confirmations[txId][msg.sender] = true;
transactions[txId].confirmations++;
}
function executeTransaction(uint256 txId) external onlyOwner {
Transaction storage tx = transactions[txId];
require(tx.confirmations >= requiredConfirmations, "Not enough confirmations");
require(!tx.executed, "Already executed");
tx.executed = true;
(bool success, ) = tx.to.call{value: tx.value}(tx.data);
require(success, "Transaction failed");
emit ExecuteTransaction(txId, tx.to, tx.value);
}
}
前端合约调用风险
交易排序依赖
矿工或验证者可以影响交易的执行顺序,这可能导致套利攻击:
solidity
// 存在交易排序依赖漏洞的合约
contract VulnerableOracle {
uint256 public price;
uint256 public lastUpdate;
function updatePrice(uint256 newPrice) external {
// 价格更新时间戳可被预测
price = newPrice;
lastUpdate = block.timestamp;
}
function getPrice() external view returns (uint256) {
return price;
}
}
solidity
// 安全的Oracle实现
contract SecureOracle {
uint256 public price;
uint256 public lastUpdate;
uint256 public priceUpdateDelay = 1 hours;
mapping(bytes32 => bool) public reportedPrices; // 防重放
event PriceReported(bytes32 indexed key, uint256 price);
function updatePrice(uint256 newPrice, bytes32 key, uint256 deadline)
external
{
// 验证签名和过期时间
require(block.timestamp < deadline, "Price report expired");
require(!reportedPrices[key], "Key already used");
// 验证报告者权限(简化版,实际应使用VRF或预言机网络)
require(msg.sender == authorizedReporter, "Not authorized");
reportedPrices[key] = true;
price = newPrice;
lastUpdate = block.timestamp;
emit PriceReported(key, newPrice);
}
}
闪电贷攻击防护
闪电贷本身不是漏洞,但配合其他漏洞会造成巨大损失:
solidity
// 易受闪电贷攻击的价格操控
contract VulnerableAMM {
function getPrice(address token) public view returns (uint256) {
return IERC20(token).balanceOf(address(this));
}
function swap(address token, uint256 amount) external {
uint256 price = getPrice(token);
// 简单地用余额作为价格,可被操控
uint256 amountOut = amount * price / 1e18;
IERC20(token).transfer(msg.sender, amountOut);
}
}
防御方法:使用时间加权平均价格(TWAP)而不是即时价格:
solidity
// 安全的TWAP价格源
contract SecurePriceOracle {
uint256 public price;
uint256 public lastUpdate;
uint256 public constant TWAP_PERIOD = 30 minutes;
uint256 public cumulativePrice;
uint256 public lastCumulativeUpdate;
function updatePrice(uint256 newPrice) external {
uint256 timeElapsed = block.timestamp - lastUpdate;
if (timeElapsed > 0) {
cumulativePrice += price * timeElapsed;
lastCumulativeUpdate = block.timestamp;
}
price = newPrice;
lastUpdate = block.timestamp;
}
function getTWAP() public view returns (uint256) {
uint256 timeElapsed = block.timestamp - lastCumulativeUpdate;
if (timeElapsed > TWAP_PERIOD) {
return cumulativePrice / TWAP_PERIOD;
}
// 如果时间太短,返回即时价格作为后备
return price;
}
}
实用安全检查清单
在部署合约前,使用这个清单逐一核对:
重入防护
- 所有状态更新都在外部调用之前完成
- 使用了互斥锁或Checks-Effects-Interactions模式
- 调用外部合约后,状态已被正确更新
整数安全
- 不存在精度损失导致的资金丢失风险
- 除法运算的余数被正确处理
- 汇率计算使用高精度数
权限控制
- 所有关键函数都有权限修饰符
- 使用成熟的AccessControl而非简单的owner检查
- 管理员操作有多签保护
输入验证
- 所有外部输入都被验证
- 不存在数组越界风险
- 边界条件被正确处理
事件记录
- 所有状态变更都触发事件
- 事件包含足够用于调试的信息
- 关键操作有多方见证
总结
智能合约安全没有银弹,但通过理解常见漏洞模式和使用成熟的防护策略,我们可以将风险降到最低。本文的要点总结:
防御纵深原则:单一防御手段可能失效,应该多层防护。例如,既有Checks-Effects-Interactions模式,又有互斥锁。
依赖成熟库:OpenZeppelin、SafeMath等经过审计的库比你从头写的代码更可靠。
测试覆盖:单元测试、集成测试、模糊测试一个都不能少。Foundry和Hardhat提供了强大的测试框架。
专业审计:在上线前找专业团队进行代码审计,即使是小项目也不要省这笔钱。
下一篇文章我们将深入DApp后端架构,看看如何构建可靠的节点服务和中间件层。
声明:本文仅供技术学习参考,不构成任何投资建议。合约安全是严谨的技术领域,实际开发中请务必咨询专业安全审计团队。

发表回复