26 KiB
26 KiB
智能合约安全
问题
- 智能合约安全有哪些常见漏洞?
- 什么是重入攻击(Reentrancy)?如何防范?
- 什么是整数溢出/下溢?Solidity 0.8+如何处理?
- 什么是访问控制漏洞?如何设计权限管理?
- 什么是前置交易(Front-running)?如何防范?
- 什么是闪电贷攻击?如何防范?
- 什么是跨链桥安全风险?
- 智能合约如何进行安全审计?
- 使用哪些工具进行安全检测?
- 智能合约有哪些最佳实践?
标准答案
1. 常见智能合约漏洞
OWASP Top 10 - Smart Contract
1. 重入攻击(Reentrancy)
2. 整数溢出/下溢(Arithmetic Issues)
3. 访问控制失败(Access Control)
4. 未检查的低级调用(Unchecked Low-Level Calls)
5. 前置交易(Front-running / TX Origin)
6. 时间戳依赖(Timestamp Manipulation)
7. 逻辑错误(Logic Errors)
8. Gas限制和循环(Gas Limit and Loops)
9. 默认可见性(Default Visibility)
10. 竞态条件(Race Conditions)
2. 重入攻击(Reentrancy)
原理
攻击者在合约更新状态之前,递归调用提款函数,多次提取资金。
经典案例:The DAO攻击(2016)
1. 攻击者存入ETH到DAO合约
2. 攻击者调用withdraw()
3. DAO合约发送ETH给攻击者
4. 攻击者的fallback函数再次调用withdraw()
5. DAO合约未更新余额,再次发送ETH
6. 重复步骤3-5多次
7. 最终提取远超存款的ETH
损失:360万 ETH(当时价值约$7000万)
漏洞代码
// ❌ 易受攻击的合约
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
// ❌ 先转账,后更新状态(漏洞!)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// 更新状态(太晚了!)
balances[msg.sender] -= amount;
}
}
// 攻击合约
contract Attack {
VulnerableBank public bank;
constructor(address _bank) {
bank = VulnerableBank(_bank);
}
// 存入ETH
function attack() public payable {
bank.deposit{value: msg.value}();
bank.withdraw(msg.value);
}
// fallback函数递归调用
fallback() external payable {
if (address(bank).balance >= 1 ether) {
bank.withdraw(1 ether);
}
}
}
攻击流程:
1. 攻击者调用attack(),存入1 ETH
2. 调用withdraw(1 ether)
3. 合约检查:balances[msg.sender] >= 1 ether ✓
4. 合约发送1 ETH给攻击者
5. 攻击者合约的fallback()被触发
6. fallback()再次调用withdraw(1 ether)
7. 合约检查:balances[msg.sender]还是1 ether(未更新!)✓
8. 合约再次发送1 ETH
9. 重复步骤5-8,直到合约余额不足
修复方案1:检查-生效-交互模式(CEI)
// ✅ 安全的合约
contract SecureBank {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
// ✅ 先更新状态
balances[msg.sender] -= amount;
// ✅ 后交互(转账)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
修复方案2:重入锁(ReentrancyGuard)
// OpenZeppelin ReentrancyGuard
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
modifier nonReentrant() {
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
// 使用ReentrancyGuard
contract SecureBankWithGuard is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
修复方案3:Pull over Push模式
// ✅ 使用Pull模式
contract PullOverPush {
mapping(address => uint256) public balances;
mapping(address => uint256) public withdrawals;
function requestWithdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
withdrawals[msg.sender] += amount;
}
// 用户主动提取
function withdraw() public {
uint256 amount = withdrawals[msg.sender];
require(amount > 0, "Nothing to withdraw");
withdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
3. 整数溢出/下溢
原理
uint8 最大值:255
uint8 最小值:0
溢出:
255 + 1 = 0(回绕)
下溢:
0 - 1 = 255(回绕)
Solidity 0.8+ 自动检查溢出/下溢
旧版本需要使用SafeMath库
漏洞代码
// ❌ Solidity 0.8之前
contract VulnerableToken {
uint8 public totalSupply = 255;
function mint(uint8 amount) public {
// 溢出:255 + 1 = 0
totalSupply += amount;
}
}
// 攻击
// mint(1) → totalSupply = 0(溢出!)
修复方案
Solidity 0.8+(自动检查):
// ✅ Solidity 0.8+
contract SecureToken {
uint256 public totalSupply;
function mint(uint256 amount) public {
// ✅ 自动检查溢出,失败会revert
totalSupply += amount;
}
}
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;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction overflow");
return a - b;
}
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
}
contract SecureTokenWithSafeMath {
using SafeMath for uint256;
uint256 public totalSupply;
function mint(uint256 amount) public {
// ✅ 使用SafeMath检查溢出
totalSupply = totalSupply.add(amount);
}
}
4. 访问控制漏洞
常见问题
1. 函数未设置访问控制
2. 使用tx.origin认证(中间人攻击)
3. 权限提升漏洞
漏洞代码1:未保护的函数
// ❌ 任何人都可以调用
contract Vulnerable {
address public owner;
uint256 public price = 100;
function setPrice(uint256 _price) public {
// ❌ 没有权限检查
price = _price;
}
}
修复:
// ✅ 使用onlyOwner修饰符
contract Secure {
address public owner;
uint256 public price = 100;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function setPrice(uint256 _price) public onlyOwner {
// ✅ 只有owner可以调用
price = _price;
}
}
漏洞代码2:tx.origin认证(钓鱼攻击)
// ❌ 易受钓鱼攻击
contract Wallet {
address public owner;
constructor() {
owner = msg.sender;
}
function withdraw() public {
// ❌ 使用tx.origin(不安全!)
require(tx.origin == owner, "Not owner");
payable(msg.sender).transfer(address(this).balance);
}
}
// 攻击合约
contract AttackWallet {
Wallet public wallet;
constructor(address _wallet) {
wallet = Wallet(_wallet);
}
function attack() public {
// 诱导owner调用这个函数
wallet.withdraw();
}
fallback() external payable {
// 转账给攻击者
payable(msg.sender).transfer(msg.value);
}
}
攻击流程:
1. 攻击者创建AttackWallet合约
2. 攻击者诱导owner调用AttackWallet.attack()
3. attack()调用Wallet.withdraw()
4. Wallet检查:tx.origin == owner ✓(owner是调用者)
5. msg.sender是AttackWallet合约
6. 资金转给AttackWallet合约
7. AttackWallet的fallback()把资金转给攻击者
修复:
// ✅ 使用msg.sender
contract SecureWallet {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function withdraw() public onlyOwner {
// ✅ 使用msg.sender
payable(msg.sender).transfer(address(this).balance);
}
}
漏洞代码3:权限提升
// ❌ 可以提升自己为管理员
contract AccessControl {
mapping(address => bool) public admins;
address public owner;
constructor() {
owner = msg.sender;
admins[owner] = true;
}
function addToAdmin(address _user) public {
// ❌ 没有检查调用者是否是admin
admins[_user] = true;
}
}
// 攻击
// 攻击者调用addToAdmin(攻击者地址)
// 攻击者成为管理员
修复:
// ✅ 检查调用者权限
contract SecureAccessControl {
mapping(address => bool) public admins;
address public owner;
constructor() {
owner = msg.sender;
admins[owner] = true;
}
modifier onlyAdmin() {
require(admins[msg.sender], "Not admin");
_;
}
function addToAdmin(address _user) public onlyAdmin {
// ✅ 只有admin可以添加新admin
admins[_user] = true;
}
}
5. 前置交易(Front-running)
原理
攻击者看到交易在内存池(Mempool)中,复制交易并设置更高的Gas Price,让自己的交易先执行。
示例场景
1. 用户在DEX上用1 ETH买UNI(价格$10)
2. 攻击者看到这笔交易
3. 攻击者设置更高Gas Price,抢先买入
4. 用户交易执行,价格被推高到$11
5. 攻击者立即卖出,获利
用户损失:原本应该花$10,实际花了$11
攻击者获利:$1/UNI × 交易数量
代码示例
// ❌ 易受前置交易攻击
contract VulnerableDEX {
mapping(address => uint256) public prices;
function setPrice(address token, uint256 price) public {
// ❌ 可以被抢先交易
prices[token] = price;
}
function trade(address token, uint256 amount) public {
uint256 price = prices[token];
// 使用price交易
}
}
修复方案1:提交-揭示模式(Commit-Reveal)
// ✅ 使用Commit-Reveal
contract CommitReveal {
struct Commitment {
bytes32 commitHash;
uint256 revealTime;
bool revealed;
}
mapping(address => Commitment) public commitments;
mapping(address => uint256) public revealedValues;
// 1. 提交哈希(隐藏真实值)
function commit(bytes32 commitHash) public {
commitments[msg.sender] = Commitment({
commitHash: commitHash,
revealTime: block.timestamp + 1 days,
revealed: false
});
}
// 2. 揭示真实值(1天后)
function reveal(uint256 value, uint256 salt) public {
Commitment storage c = commitments[msg.sender];
require(!c.revealed, "Already revealed");
require(block.timestamp >= c.revealTime, "Too early");
// 验证哈希
bytes32 computedHash = keccak256(abi.encodePacked(value, salt));
require(computedHash == c.commitHash, "Invalid reveal");
c.revealed = true;
revealedValues[msg.sender] = value;
}
}
修复方案2:批量交易
// ✅ 批量执行交易,减少前置交易
contract BatchTrade {
struct Trade {
address user;
address token;
uint256 amount;
uint256 price;
}
Trade[] public pendingTrades;
function submitTrade(address token, uint256 amount) public {
pendingTrades.push(Trade({
user: msg.sender,
token: token,
amount: amount,
price: 0 // 后续设置
}));
}
// 批量执行所有交易
function executeBatch() public {
for (uint256 i = 0; i < pendingTrades.length; i++) {
Trade memory t = pendingTrades[i];
// 使用平均价格执行所有交易
executeTrade(t);
}
delete pendingTrades;
}
}
6. 闪电贷攻击
原理
1. 攻击者闪电贷借入大量资金
2. 使用资金操纵市场(如DEX价格)
3. 利用被操纵的价格获利(如套利、清算)
4. 还款 + 手续费
5. 如果失败,交易回滚(无风险)
攻击示例
// 闪电贷攻击DEX
contract FlashLoanAttack {
IUniswapV2Router public router;
IUniswapV2Pair public pair;
function attack() external {
// 1. 闪电贷借入10,000 ETH
flashLoan(10000 ether);
// 2. 在DEX上用10,000 ETH卖出代币
// → 价格被砸到极低
// 3. 在另一个DEX用低价买回代币
// → 赚取差价
// 4. 还款 10,000 ETH + 手续费
repayFlashLoan(10000 ether);
}
}
防范措施
1. 使用时间加权平均价格(TWAP)
- 链下预言机(Chainlink)
- 多个价格源聚合
2. 限制单笔交易规模
- 最大交易量限制
- 滑点保护
3. 闪电贷检测
- 单个区块内大额交易
- 可疑价格波动告警
4. 暂停机制
- 检测到攻击时暂停合约
- 多重签名恢复
代码示例:TWAP预言机:
// ✅ 使用TWAP预言机
contract TWAPOracle {
uint256 public price;
uint256 public lastUpdateTime;
uint256 public constant PERIOD = 1 hours;
function updatePrice(uint256 newPrice) external {
if (block.timestamp >= lastUpdateTime + PERIOD) {
// 时间到了,可以更新
price = newPrice;
lastUpdateTime = block.timestamp;
} else {
// 未到期,使用加权平均
uint256 timeElapsed = block.timestamp - lastUpdateTime;
uint256 weight = timeElapsed * 100 / PERIOD;
price = (price * (100 - weight) + newPrice * weight) / 100;
}
}
}
7. 跨链桥安全风险
常见攻击
1. 验证节点私钥泄露
- Ronin Bridge:$6.25亿被盗
- 攻击者控制了5/9个验证节点
2. 伪造签名
- Wormhole:$3.2亿被盗
- 攻击者伪造了验证者签名
3. 智能合约漏洞
- Harmony Bridge:$1亿被盗
- 漏洞允许绕过验证
4. 逻辑漏洞
- Nomad Bridge:$1.9亿被盗
- 漏洞允许任何人伪造消息
安全设计原则
// ✅ 安全的跨链桥设计
contract SecureBridge {
// 1. 多重签名验证
mapping(bytes32 => bool) public usedSignatures;
address[] public validators;
modifier validateSignature(bytes32 message, bytes memory signature) {
require(verifySignature(message, signature), "Invalid signature");
require(!usedSignatures[keccak256(signature)], "Signature used");
_;
}
// 2. 延迟提款(给用户时间取消)
mapping(address => uint256) public pendingWithdrawals;
uint256 public constant DELAY = 24 hours;
function requestWithdraw(address token, uint256 amount) public {
pendingWithdrawals[msg.sender] = block.timestamp;
emit WithdrawRequested(msg.sender, token, amount);
}
function executeWithdraw(address token, uint256 amount) public {
require(
block.timestamp >= pendingWithdrawals[msg.sender] + DELAY,
"Delay not met"
);
// 执行提款
}
// 3. 暂停机制
bool public paused;
modifier whenNotPaused() {
require(!paused, "Paused");
_;
}
function pause() public {
// 多重签名
paused = true;
}
}
8. 智能合约安全审计
审计流程
1. 文档审查
- 白皮书
- 技术文档
- 经济模型
2. 代码审查
- 手动代码审查
- 自动化工具扫描
- 测试覆盖率检查
3. 测试
- 单元测试
- 集成测试
- 模糊测试(Fuzzing)
4. 形式化验证
- 数学证明
- 不变量检查
5. 渗透测试
- 模拟攻击
- 红队演练
审计清单
✅ 访问控制
- 所有敏感函数都有访问控制
- 使用OpenZeppelin的AccessControl
✅ 重入保护
- 外部调用在状态更新之后
- 使用ReentrancyGuard
✅ 整数溢出
- 使用Solidity 0.8+
- 或使用SafeMath
✅ 外部调用
- 检查返回值
- 使用try-catch
✅ Gas优化
- 循环有限制
- 避免动态数组
✅ 测试
- 单元测试覆盖率 > 90%
- 集成测试
- 边界条件测试
✅ 文档
- NatSpec注释
- 清晰的函数说明
- 使用示例
9. 安全检测工具
静态分析工具
1. Slither
- Python开发
- 快速扫描
- 检测常见漏洞
安装:
pip install slither-analyzer
使用:
slither contract.sol
# Slither扫描示例
$ slither token.sol
...
INFO:Detectors:
Reentrancy in Token.withdraw(uint256) (token.sol#45-50):
External calls:
- msg.sender.call{value: amount}(gas()) (token.sol#48)
State variables written after the call(s):
- balances[msg.sender] -= amount (token.sol#49)
2. MythX
- 商业工具
- 深度分析
- 支持多种漏洞检测
使用:
mythx analyze contract.sol
3. Echidna
- 模糊测试工具
- 基于属性的测试
- 发现边缘情况
安装:
cargo install echidna
使用:
echidna-test contract.sol
Echidna示例:
// 测试不变量:总供应量 = 所有人余额之和
contract Token {
mapping(address => uint256) public balanceOf;
uint256 public totalSupply;
function transfer(address to, uint256 amount) public {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
}
// Echidna测试
contract TestToken is Token, Test {
function check_total_supply() public {
uint256 sum = 0;
// 计算所有余额(需要手动添加)
assert(totalSupply == sum);
}
}
4. Mythril
- 符号执行引擎
- 深度漏洞检测
- 支持攻击路径分析
安装:
pip install mythril
使用:
myth contract.sol
动态分析工具
1. Hardhat + Tenderly
- 交易模拟
- Gas分析
- 调试工具
2. Ganache
- 本地测试网络
- 快速部署测试
3. Fork测试
- Fork主网状态
- 在真实环境测试
Fork测试示例:
// hardhat.config.js
module.exports = {
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.alchemyapi.io/v2/YOUR-API-KEY",
blockNumber: 15000000, // Fork特定区块
},
},
},
};
// 测试
describe("Uniswap Fork Test", function () {
it("should swap on real Uniswap", async function () {
// 在主网Uniswap上交易(不花真钱)
const uniswap = await ethers.getContractAt("IUniswapV2Router", UNISWAP_ROUTER);
await uniswap.swapExactETHForTokens(...);
});
});
10. 智能合约最佳实践
1. 使用OpenZeppelin库
// ✅ 不要重复造轮子
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MyToken is ERC20, Ownable, ReentrancyGuard {
constructor() ERC20("My Token", "MTK") {}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
2. 检查-生效-交互模式
// ✅ CEI模式
function withdraw(uint256 amount) public {
// 1. 检查(Checks)
require(balances[msg.sender] >= amount, "Insufficient");
// 2. 生效(Effects)
balances[msg.sender] -= amount;
// 3. 交互(Interactions)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
3. 使用SafeERC20
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract SafeTransfer {
using SafeERC20 for IERC20;
function transferToken(address token, address to, uint256 amount) public {
// ✅ 自动处理返回值
IERC20(token).safeTransfer(to, amount);
}
}
4. 停止机制(Emergency Pause)
import "@openzeppelin/contracts/security/Pausable.sol";
contract PausableContract is Pausable, Ownable {
function deposit() public payable whenNotPaused {
// 正常逻辑
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
}
5. 事件日志
contract LogEvents {
event Withdrawal(address indexed user, uint256 amount, uint256 timestamp);
function withdraw(uint256 amount) public {
// ✅ 记录所有重要操作
emit Withdrawal(msg.sender, amount, block.timestamp);
// 执行提款
payable(msg.sender).transfer(amount);
}
}
6. 限流机制
contract RateLimiter {
mapping(address => uint256) public lastCallTime;
uint256 public constant COOLDOWN = 1 minutes;
modifier rateLimit() {
require(
block.timestamp >= lastCallTime[msg.sender] + COOLDOWN,
"Cooldown not met"
);
_;
lastCallTime[msg.sender] = block.timestamp;
}
function sensitiveOperation() public rateLimit {
// 限制每个用户每分钟只能调用一次
}
}
7. 多重签名钱包
// ✅ 关键操作使用多签
contract MultiSigWallet {
address[] public owners;
uint256 public required;
mapping(uint256 => Transaction) public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
struct Transaction {
address to;
uint256 value;
bytes data;
bool executed;
}
function submitTransaction(address to, uint256 value, bytes memory data)
public
{
uint256 txIndex = transactions.length;
transactions[txIndex] = Transaction({
to: to,
value: value,
data: data,
executed: false
});
confirmTransaction(txIndex);
}
function confirmTransaction(uint256 txIndex) public {
// 需要多个所有者确认
confirmations[txIndex][msg.sender] = true;
if (isConfirmed(txIndex)) {
executeTransaction(txIndex);
}
}
function isConfirmed(uint256 txIndex) public view returns (bool) {
uint256 count = 0;
for (uint256 i = 0; i < owners.length; i++) {
if (confirmations[txIndex][owners[i]]) {
count += 1;
}
}
return count >= required;
}
}
8. 升级模式
// ✅ 使用代理模式升级
contract LogicV1 {
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
}
contract LogicV2 {
uint256 public value;
uint256 public newValue;
function setValue(uint256 _value) public {
value = _value;
newValue = _value * 2;
}
}
// 使用OpenZeppelin Transparent Proxy
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
// 部署
// 1. 部署LogicV1
// 2. 部署Proxy,指向LogicV1
// 3. 用户通过Proxy交互
// 4. 升级:部署LogicV2,调用Proxy.upgradeTo(LogicV2)
结合简历的面试题
1. 电商风控 vs DeFi安全
面试官会问:
"你在电商做过风控系统,DeFi安全有什么异同?"
参考回答:
电商风控:
- 反欺诈:识别虚假订单、刷单
- 信用评估:用户信用评分
- 异常检测:异常交易行为
DeFi安全:
- 智能合约审计:代码漏洞检测
- 链上监控:异常交易监控
- 价格预言机:防止价格操纵
- MEV保护:防止三明治攻击
共同点:
- 需要实时监控系统
- 需要异常检测机制
- 需要快速响应机制
差异:
- 电商:中心化,可以人工介入
- DeFi:去中心化,只能通过智能合约
2. 高并发与Gas优化
面试官会问:
"你做过高并发系统,智能合约如何优化Gas?"
参考回答:
Web2高并发:
- 缓存热点数据
- 异步处理
- 数据库优化
Web3 Gas优化:
1. 存储优化
- 使用打包类型(uint256 vs uint8)
- 使用短字符串
- 删除不需要的数据(refund gas)
2. 循环优化
- 限制循环次数
- 使用mapping代替array查找
3. 批量操作
- 批量转账
- 批量Mint
4. 链下计算
- Merkle Tree
- 零知识证明
示例:
// ❌ 高Gas
for (uint256 i = 0; i < 1000; i++) {
users[i].reward = calculateReward(i);
}
// ✅ 低Gas(链下计算 + Merkle Proof)
bytes32 public merkleRoot;
function claimReward(uint256 amount, bytes32[] memory proof) public {
require(verifyMerkleProof(msg.sender, amount, proof), "Invalid proof");
payable(msg.sender).transfer(amount);
}
智能合约安全面试加分项
1. 实战经验
- 参与过智能合约审计
- 发现过真实漏洞
- 有漏洞修复经验
- 熟悉常见攻击手法
2. 技术深度
- 理解EVM底层机制
- 熟悉汇编语言(Yul)
- 了解形式化验证
- 掌握Gas优化技巧
3. 工具使用
- 熟练使用Slither、MythX
- 会使用Foundry测试框架
- 会使用Tenderly调试
- 了解Fuzzing测试
4. 安全意识
- 防御性编程思维
- 最小权限原则
- 深度防御策略
- 持续学习最新漏洞