Solidity是以太坊区块链上开发智能合约的主要编程语言,因其与区块链的紧密结合,智能合约的安全性至关重要。漏洞可能导致资金被盗、合约功能异常或用户信任受损。重入攻击(Reentrancy)原理重入攻击是Solidity智能合约中最著名的漏洞之一。攻击者通过在合约调用外部合约或地址时,
Solidity 是以太坊区块链上开发智能合约的主要编程语言,因其与区块链的紧密结合,智能合约的安全性至关重要。漏洞可能导致资金被盗、合约功能异常或用户信任受损。
重入攻击是 Solidity 智能合约中最著名的漏洞之一。攻击者通过在合约调用外部合约或地址时,利用回调机制反复调用原合约函数,在状态更新前窃取资金或执行恶意逻辑。重入通常发生在使用 call
或 send
转移以太币时,外部合约可以通过 fallback
或 receive
函数重新调用原合约。
示例(易受攻击的代码):
contract Vulnerable {
mapping(address => uint256) public balances;
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;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
}
攻击方式: 攻击者部署一个恶意合约:
contract Attacker {
Vulnerable victim;
uint256 public count;
constructor(address _victim) {
victim = Vulnerable(_victim);
}
receive() external payable {
if (count < 10) {
count++;
victim.withdraw();
}
}
function attack() public payable {
victim.deposit{value: msg.value}();
victim.withdraw();
}
}
在 withdraw
函数中,msg.sender.call
在更新 balances
前发送以太币,攻击者的 receive
函数会反复调用 withdraw
,耗尽合约资金。
状态更新优先(Checks-Effects-Interactions 模式): 在调用外部合约前更新状态。
function withdraw() public {
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");
}
使用 ReentrancyGuard:
OpenZeppelin 提供的 ReentrancyGuard
修饰符防止重入。
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Secure is ReentrancyGuard {
mapping(address => uint256) public balances;
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");
}
}
限制 Gas:
使用 transfer
或 send
(限制 2300 Gas),防止 fallback
函数执行复杂逻辑。
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
避免动态调用:
尽量避免使用 call
,优先使用明确定义的函数调用。
ReentrancyGuard
增加 Gas 成本,需权衡性能。nonReentrant
修饰符。在 Solidity < 0.8.0 版本中,整数运算不进行溢出/下溢检查,可能导致意外结果。例如,uint8
最大值为 255,加 1 会变为 0。
示例(易受攻击的代码):
contract Vulnerable {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
如果 balances[msg.sender]
为 0,amount
为 2^256 - 1
,减法会导致下溢,balances[msg.sender]
变为一个巨大值。
使用 Solidity >= 0.8.0: 从 0.8.0 开始,Solidity 默认启用溢出/下溢检查,溢出会抛出异常。
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount; // 自动检查下溢
balances[to] += amount; // 自动检查溢出
}
SafeMath 库(适用于 < 0.8.0): 使用 OpenZeppelin 的 SafeMath 库。
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract Secure {
using SafeMath for uint256;
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
}
}
显式范围检查: 手动检查输入值范围。
function transfer(address to, uint256 amount) public {
require(amount <= balances[msg.sender], "Insufficient balance");
require(amount <= type(uint256).max - balances[to], "Overflow");
balances[msg.sender] -= amount;
balances[to] += amount;
}
未授权访问发生在合约未正确限制敏感函数的访问权限时,允许非授权用户调用关键函数(如提取资金或修改状态)。
示例(易受攻击的代码):
contract Vulnerable {
address public owner;
uint256 public balance;
constructor() {
owner = msg.sender;
}
function withdraw(uint256 amount) public {
require(balance >= amount, "Insufficient balance");
balance -= amount;
payable(msg.sender).transfer(amount);
}
}
任何人都可以调用 withdraw
,导致资金被盗。
使用修饰符:
定义 onlyOwner
修饰符限制访问。
contract Secure {
address public owner;
uint256 public balance;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function withdraw(uint256 amount) public onlyOwner {
require(balance >= amount, "Insufficient balance");
balance -= amount;
payable(msg.sender).transfer(amount);
}
}
OpenZeppelin AccessControl: 使用角色-based 访问控制。
import "@openzeppelin/contracts/access/AccessControl.sol";
contract Secure is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
uint256 public balance;
constructor() {
_setupRole(ADMIN_ROLE, msg.sender);
}
function withdraw(uint256 amount) public onlyRole(ADMIN_ROLE) {
require(balance >= amount, "Insufficient balance");
balance -= amount;
payable(msg.sender).transfer(amount);
}
}
多重签名: 要求多个地址授权敏感操作。
contract MultiSig {
address[] public owners;
mapping(address => bool) public isOwner;
uint256 public required;
mapping(uint256 => mapping(address => bool)) public confirmations;
uint256 public transactionCount;
constructor(address[] memory _owners, uint256 _required) {
require(_owners.length > 0 && _required <= _owners.length);
owners = _owners;
required = _required;
for (uint256 i = 0; i < _owners.length; i++) {
isOwner[_owners[i]] = true;
}
}
function submitTransaction(address to, uint256 amount) public {
require(isOwner[msg.sender], "Not owner");
uint256 txId = transactionCount++;
confirmations[txId][msg.sender] = true;
if (isConfirmed(txId)) {
executeTransaction(to, amount);
}
}
function confirmTransaction(uint256 txId) public {
require(isOwner[msg.sender], "Not owner");
confirmations[txId][msg.sender] = true;
if (isConfirmed(txId)) {
executeTransaction(address(0), 0); // 模拟执行
}
}
function isConfirmed(uint256 txId) private view returns (bool) {
uint256 count = 0;
for (uint256 i = 0; i < owners.length; i++) {
if (confirmations[txId][owners[i]]) count++;
}
return count >= required;
}
function executeTransaction(address to, uint256 amount) private {
// 执行逻辑
}
}
Ownable
或 AccessControl
简化权限管理。拒绝服务攻击通过耗尽 Gas、阻塞函数执行或使合约不可用,导致合法用户无法正常操作。常见场景包括:
示例(易受攻击的代码):
contract Vulnerable {
address[] public users;
uint256 public totalBalance;
function distribute() public {
for (uint256 i = 0; i < users.length; i++) {
payable(users[i]).transfer(totalBalance / users.length);
}
}
}
攻击者可注册大量地址或部署一个失败的 receive
函数,导致 distribute
无法完成。
拉取模式(Pull over Push): 让用户主动提取资金,避免批量发送。
contract Secure {
mapping(address => uint256) public pendingWithdrawals;
uint256 public totalBalance;
function distribute() public {
uint256 amount = totalBalance / users.length;
for (uint256 i = 0; i < users.length; i++) {
pendingWithdrawals[users[i]] += amount;
}
totalBalance = 0;
}
function withdraw() public {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds");
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
限制循环次数: 设置最大迭代次数或分页处理。
contract Secure {
address[] public users;
uint256 public totalBalance;
function distribute(uint256 start, uint256 limit) public {
require(start + limit <= users.length, "Invalid range");
for (uint256 i = start; i < start + limit; i++) {
pendingWithdrawals[users[i]] += totalBalance / users.length;
}
}
}
失败隔离: 捕获并处理外部调用失败。
contract Secure {
address[] public users;
uint256 public totalBalance;
function distribute() public {
uint256 amount = totalBalance / users.length;
for (uint256 i = 0; i < users.length; i++) {
(bool success, ) = users[i].call{value: amount}("");
if (!success) {
pendingWithdrawals[users[i]] += amount;
}
}
totalBalance = 0;
}
}
前置运行是指攻击者通过监控未确认交易池(Mempool),在目标交易执行前插入自己的交易,改变执行结果。常见于去中心化交易所(DEX)或竞拍系统。
示例(易受攻击的代码):
contract VulnerableAuction {
uint256 public highestBid;
address public highestBidder;
function bid() public payable {
require(msg.value > highestBid, "Bid too low");
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBid = msg.value;
highestBidder = msg.sender;
}
}
攻击者监控高出价交易,抢先提交更高出价,窃取资金。
提交-揭示模式(Commit-Reveal): 用户先提交加密承诺,后揭示实际出价。
contract SecureAuction {
mapping(address => bytes32) public commitments;
mapping(address => uint256) public bids;
uint256 public commitPhaseEnd;
constructor(uint256 _commitDuration) {
commitPhaseEnd = block.timestamp + _commitDuration;
}
function commitBid(bytes32 commitment) public {
require(block.timestamp <= commitPhaseEnd, "Commit phase ended");
commitments[msg.sender] = commitment;
}
function revealBid(uint256 amount, bytes32 secret) public payable {
require(block.timestamp > commitPhaseEnd, "Reveal phase not started");
require(keccak256(abi.encodePacked(amount, secret)) == commitments[msg.sender], "Invalid commitment");
require(msg.value == amount, "Incorrect amount");
bids[msg.sender] = amount;
}
}
Gas 竞价限制:
使用 tx.gasprice
限制优先级。
function bid() public payable {
require(tx.gasprice <= 100 gwei, "Gas price too high");
require(msg.value > highestBid, "Bid too low");
// ...
}
时间锁: 延迟执行敏感操作。
contract SecureAuction {
uint256 public highestBid;
address public highestBidder;
uint256 public bidLockTime;
function bid() public payable {
require(block.timestamp > bidLockTime, "Bid locked");
require(msg.value > highestBid, "Bid too low");
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBid = msg.value;
highestBidder = msg.sender;
bidLockTime = block.timestamp + 1 hours;
}
}
调用外部合约或地址(如 call
、delegatecall
)可能导致不可预测的行为,尤其是当目标地址是用户控制的恶意合约。
示例(易受攻击的代码):
contract Vulnerable {
function callExternal(address target, bytes memory data) public {
(bool success, ) = target.call(data);
require(success, "Call failed");
}
}
攻击者可传入恶意合约地址,执行任意逻辑。
限制目标地址: 只允许调用可信合约。
contract Secure {
address[] public trustedContracts;
function addTrusted(address contractAddr) public {
// 仅限管理员
trustedContracts.push(contractAddr);
}
function callExternal(address target, bytes memory data) public {
require(isTrusted(target), "Untrusted contract");
(bool success, ) = target.call(data);
require(success, "Call failed");
}
function isTrusted(address target) private view returns (bool) {
for (uint256 i = 0; i < trustedContracts.length; i++) {
if (trustedContracts[i] == target) return true;
}
return false;
}
}
使用接口调用: 定义明确接口,避免动态调用。
interface IExternal {
function doSomething() external returns (uint256);
}
contract Secure {
function callExternal(address target) public {
IExternal(target).doSomething();
}
}
Gas 限制: 限制外部调用的 Gas。
function callExternal(address target, bytes memory data) public {
(bool success, ) = target.call{gas: 50000}(data);
require(success, "Call failed");
}
delegatecall
,除非目标是可信库。在 Solidity < 0.5.0 中,storage
变量未显式初始化可能导致意外覆盖存储槽,特别是在使用代理模式(如 delegatecall
)时。
示例(易受攻击的代码):
contract Vulnerable {
address public owner;
uint256 public value;
function setValue(uint256 _value) public {
value = _value;
}
}
如果通过代理调用,value
可能覆盖 owner
。
显式初始化: 确保所有存储变量初始化。
contract Secure {
address public owner = address(0);
uint256 public value = 0;
function setValue(uint256 _value) public {
value = _value;
}
}
使用结构体: 组织存储变量,减少误操作。
contract Secure {
struct Data {
address owner;
uint256 value;
}
Data public data;
constructor() {
data.owner = msg.sender;
data.value = 0;
}
}
避免低版本 Solidity: 使用 >= 0.5.0,确保存储指针行为可预测。
Proxy
)。交易顺序依赖发生在合约逻辑依赖于交易的执行顺序,而矿工可操纵交易顺序。例如,多个用户同时调用函数,可能导致意外结果。
示例(易受攻击的代码):
contract Vulnerable {
uint256 public price;
function buy() public payable {
require(msg.value >= price, "Insufficient payment");
price = msg.value; // 更新价格
// 分配代币
}
}
攻击者可观察交易池,提交更高 Gas 费用的交易,抢先购买。
固定价格或时间窗口: 使用固定价格或时间限制。
contract Secure {
uint256 public price;
uint256 public saleEnd;
constructor(uint256 _price, uint256 _duration) {
price = _price;
saleEnd = block.timestamp + _duration;
}
function buy() public payable {
require(block.timestamp <= saleEnd, "Sale ended");
require(msg.value >= price, "Insufficient payment");
// 分配代币
}
}
批量处理: 收集所有交易后统一处理。
contract Secure {
struct Bid {
address bidder;
uint256 amount;
}
Bid[] public bids;
uint256 public biddingEnd;
function submitBid() public payable {
require(block.timestamp <= biddingEnd, "Bidding ended");
bids.push(Bid(msg.sender, msg.value));
}
function processBids() public {
require(block.timestamp > biddingEnd, "Bidding not ended");
// 按出价排序并分配
}
}
在区块链上生成随机数具有挑战性,因为所有数据(如 blockhash
、block.timestamp
)是公开的,攻击者可预测或操纵随机结果。
示例(易受攻击的代码):
contract Vulnerable {
function getRandomNumber() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender)));
}
}
攻击者可预测 block.timestamp
和 msg.sender
,操纵结果。
使用 Chainlink VRF: Chainlink 提供可验证的随机函数。
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract Secure is VRFConsumerBase {
bytes32 internal keyHash;
uint256 internal fee;
constructor(address _vrfCoordinator, address _link, bytes32 _keyHash, uint256 _fee)
VRFConsumerBase(_vrfCoordinator, _link) {
keyHash = _keyHash;
fee = _fee;
}
function getRandomNumber() public returns (bytes32 requestId) {
require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
return requestRandomness(keyHash, fee);
}
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
// 使用随机数
}
}
提交-揭示随机数: 用户提交哈希,后揭示种子。
contract Secure {
mapping(address => bytes32) public commitments;
mapping(address => uint256) public randomNumbers;
function commit(bytes32 commitment) public {
commitments[msg.sender] = commitment;
}
function reveal(uint256 seed) public {
require(keccak256(abi.encodePacked(seed)) == commitments[msg.sender], "Invalid seed");
randomNumbers[msg.sender] = uint256(keccak256(abi.encodePacked(seed, block.number)));
}
}
blockhash
)作为随机源。在代理模式中,代理合约通过 delegatecall
调用逻辑合约,但存储布局不一致可能导致数据覆盖。
示例(易受攻击的代码):
contract Proxy {
address public implementation;
function upgrade(address _newImpl) public {
implementation = _newImpl;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success);
}
}
contract LogicV1 {
address public owner;
uint256 public value;
}
contract LogicV2 {
uint256 public value; // 存储槽变化
address public owner;
}
升级到 LogicV2
后,value
和 owner
的存储槽对调,导致数据混乱。
固定存储布局: 确保所有版本的逻辑合约存储布局一致。
contract LogicV1 {
address public owner;
uint256 public value;
}
contract LogicV2 {
address public owner; // 保持相同顺序
uint256 public value;
uint256 public newValue; // 新增变量放在末尾
}
使用 OpenZeppelin 升级代理:
OpenZeppelin 的 TransparentUpgradeableProxy
确保存储安全。
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
contract SecureProxy is TransparentUpgradeableProxy {
constructor(address _logic, address _admin, bytes memory _data)
TransparentUpgradeableProxy(_logic, _admin, _data) {}
}
存储间隙(Storage Gap): 为未来扩展预留存储槽。
contract Logic {
address public owner;
uint256 public value;
uint256[50] private __gap; // 预留 50 个存储槽
}
slither
)检查存储布局。合约依赖 block.timestamp
进行逻辑判断可能被矿工操纵,因为矿工可在一定范围内调整时间戳。
示例(易受攻击的代码):
contract Vulnerable {
uint256 public deadline;
function startAuction() public {
deadline = block.timestamp + 1 days;
}
function bid() public payable {
require(block.timestamp < deadline, "Auction ended");
// 竞拍逻辑
}
}
矿工可调整 block.timestamp
延长或缩短竞拍时间。
使用区块号: 区块号更难操纵。
contract Secure {
uint256 public endBlock;
function startAuction() public {
endBlock = block.number + 1000; // 约一天
}
function bid() public payable {
require(block.number < endBlock, "Auction ended");
// 竞拍逻辑
}
}
Chainlink Keepers: 使用去中心化定时任务。
import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol";
contract Secure is KeeperCompatibleInterface {
uint256 public deadline;
function checkUpkeep(bytes calldata) external override returns (bool upkeepNeeded, bytes memory) {
upkeepNeeded = block.timestamp >= deadline;
}
function performUpkeep(bytes calldata) external override {
// 执行定时任务
}
}
block.timestamp
用于关键逻辑。Solidity 函数默认可见性为 public
,可能导致敏感函数被外部调用。
示例(易受攻击的代码):
contract Vulnerable {
uint256 balance;
function withdraw() { // 默认为 public
payable(msg.sender).transfer(balance);
}
}
任何人都可以调用 withdraw
。
显式声明可见性:
使用 private
或 internal
。
contract Secure {
uint256 balance;
function withdraw() private {
payable(msg.sender).transfer(balance);
}
}
代码审计工具: 使用 Slither 或 Mythril 检测默认可见性问题。
slither contract.sol --checklist
合约依赖不受控的外部数据(如用户输入或链上数据)可能导致逻辑错误或攻击。
示例(易受攻击的代码):
contract Vulnerable {
function setPrice(address oracle) public {
(bool success, bytes memory data) = oracle.call(abi.encodeWithSignature("getPrice()"));
require(success, "Call failed");
uint256 price = abi.decode(data, (uint256));
// 使用 price
}
}
攻击者可提供恶意 Oracle 地址,返回错误数据。
使用可信 Oracle: 如 Chainlink 数据源。
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract Secure {
AggregatorV3Interface priceFeed;
constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}
function getPrice() public view returns (uint256) {
(, int256 price, , , ) = priceFeed.latestRoundData();
return uint256(price);
}
}
输入验证: 检查外部数据的合理性。
function setPrice(address oracle) public {
require(isTrusted(oracle), "Untrusted oracle");
(bool success, bytes memory data) = oracle.call(abi.encodeWithSignature("getPrice()"));
require(success, "Call failed");
uint256 price = abi.decode(data, (uint256));
require(price > 0 && price < 1e18, "Invalid price");
// 使用 price
}
Gas 成本过高或循环逻辑可能导致交易失败,甚至被攻击者利用制造拒绝服务。
示例(易受攻击的代码):
contract Vulnerable {
address[] public users;
function refundAll() public {
for (uint256 i = 0; i < users.length; i++) {
payable(users[i]).transfer(1 ether);
}
}
}
大量用户会导致 Gas 超限。
分页处理: 分批执行循环。
contract Secure {
address[] public users;
uint256 public lastProcessed;
function refund(uint256 limit) public {
uint256 end = lastProcessed + limit;
if (end > users.length) end = users.length;
for (uint256 i = lastProcessed; i < end; i++) {
payable(users[i]).transfer(1 ether);
}
lastProcessed = end;
}
}
优化存储:
使用 memory
替代 storage
。
function processUsers(address[] memory _users) public {
for (uint256 i = 0; i < _users.length; i++) {
// 处理
}
}
Gas 估计: 在调用前估计 Gas。
function callExternal(address target, bytes memory data) public {
uint256 gasLimit = gasleft() / 2; // 保留一半 Gas
(bool success, ) = target.call{gas: gasLimit}(data);
require(success, "Call failed");
}
合约在初始化时未正确设置状态(如所有者),可能被攻击者抢占控制权。
示例(易受攻击的代码):
contract Vulnerable {
address public owner;
function initialize() public {
owner = msg.sender;
}
}
攻击者可抢先调用 initialize
。
在构造函数中初始化:
contract Secure {
address public owner;
constructor() {
owner = msg.sender;
}
}
使用初始化标志:
contract Secure {
address public owner;
bool public initialized;
function initialize() public {
require(!initialized, "Already initialized");
initialized = true;
owner = msg.sender;
}
}
OpenZeppelin Initializable:
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract Secure is Initializable {
address public owner;
function initialize() public initializer {
owner = msg.sender;
}
}
事件用于记录链上操作,但依赖事件进行关键逻辑可能导致错误,因为事件可能被忽略或伪造。
示例(易受攻击的代码):
contract Vulnerable {
event BalanceUpdated(address user, uint256 balance);
function updateBalance(uint256 amount) public {
balances[msg.sender] = amount;
emit BalanceUpdated(msg.sender, amount);
}
}
前端可能仅依赖事件更新 UI,忽略实际状态。
状态优先: 客户端应直接查询链上状态。
contract Secure {
mapping(address => uint256) public balances;
function getBalance(address user) public view returns (uint256) {
return balances[user];
}
}
事件仅用于日志: 不要将事件作为状态依据。
contract Secure {
event BalanceUpdated(address user, uint256 balance);
function updateBalance(uint256 amount) public {
balances[msg.sender] = amount;
emit BalanceUpdated(msg.sender, amount);
}
}
使用未更新的库或引用旧合约可能引入已知漏洞。
示例(易受攻击的代码):
import "old-library/OldMath.sol";
contract Vulnerable {
function calculate(uint256 a, uint256 b) public returns (uint256) {
return OldMath.add(a, b); // 可能包含溢出漏洞
}
}
使用最新库: 如 OpenZeppelin 最新版本。
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract Secure {
using SafeMath for uint256;
function calculate(uint256 a, uint256 b) public returns (uint256) {
return a.add(b);
}
}
固定版本号:
在 package.json
或 hardhat.config.js
中指定版本。
{
"dependencies": {
"@openzeppelin/contracts": "^4.9.0"
}
}
定期审计依赖: 使用工具检查依赖漏洞。
npm audit
可升级合约(如代理模式)如果未正确实现,可能导致逻辑错误或权限滥用。
示例(易受攻击的代码):
contract VulnerableProxy {
address public implementation;
function upgrade(address _newImpl) public {
implementation = _newImpl;
}
}
任何人都可以升级合约。
限制升级权限:
使用 onlyOwner
修饰符。
contract SecureProxy {
address public implementation;
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function upgrade(address _newImpl) public onlyOwner {
implementation = _newImpl;
}
}
使用 OpenZeppelin Upgrades:
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract SecureProxy is UUPSUpgradeable {
function initialize() public initializer {
__UUPSUpgradeable_init();
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
使用 Hardhat 和 Mocha 编写测试:
// test/Secure.js
const { expect } = require('chai');
describe('Secure', () => {
let secure, owner, user;
beforeEach(async () => {
const Secure = await ethers.getContractFactory('Secure');
[owner, user] = await ethers.getSigners();
secure = await Secure.deploy();
await secure.deployed();
});
it('prevents reentrancy', async () => {
await expect(
secure.connect(user).withdraw({ value: ethers.utils.parseEther('1') })
).to.be.revertedWith('No balance');
});
});
使用 Slither 检测漏洞:
slither contract.sol
使用 Certora 或 Scribble 验证合约逻辑:
/// @notice invariant balances[msg.sender] >= 0
contract Secure {
mapping(address => uint256) public balances;
}
使用 Echidna 进行模糊测试:
contract Secure {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// Echidna 测试
function echidna_balance_positive() public view returns (bool) {
return balances[msg.sender] >= 0;
}
}
以下是一个安全的 ERC20 代币合约,综合应用上述防御措施:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureToken is ERC20, Ownable, ReentrancyGuard {
mapping(address => uint256) public pendingWithdrawals;
constructor(string memory name, string memory symbol)
ERC20(name, symbol)
Ownable(msg.sender)
{
_mint(msg.sender, 1000000 * 10**decimals());
}
function deposit() public payable nonReentrant {
pendingWithdrawals[msg.sender] += msg.value;
}
function withdraw() public nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds to withdraw");
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
function transfer(address to, uint256 amount)
public
override
returns (bool)
{
require(to != address(0), "Invalid address");
return super.transfer(to, amount);
}
function burn(uint256 amount) public onlyOwner {
_burn(msg.sender, amount);
}
}
安全特性:
ERC20
、Ownable
和 ReentrancyGuard
。nonReentrant
)。burn
)为仅限所有者。如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!