Solidity里一个超级硬核的主题——合约审计!在以太坊上写智能合约,钱和数据都直接挂在链上,一个小漏洞就能让黑客把你钱包掏空,项目直接翻车!审计就是给你的合约做个全身体检,找出那些藏得深的bug和安全隐患。这篇干货会用大白话把Solidity合约审计的硬核技巧讲得明明白白,从重入攻击、溢出检查到
Solidity里一个超级硬核的主题——合约审计!在以太坊上写智能合约,钱和数据都直接挂在链上,一个小漏洞就能让黑客把你钱包掏空,项目直接翻车!审计就是给你的合约做个全身体检,找出那些藏得深的bug和安全隐患。这篇干货会用大白话把Solidity合约审计的硬核技巧讲得明明白白,从重入攻击、溢出检查到权限管理、Gas陷阱,配合OpenZeppelin、Hardhat测试和Slither分析,带你一步步打造滴水不漏的合约。
先搞清楚几个关键点:
咱们用Solidity 0.8.20,结合OpenZeppelin、Hardhat和Slither,逐一分析常见漏洞和修复方案。
用Hardhat搭建开发环境,集成Slither。
mkdir contract-audit-demo
cd contract-audit-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts
npm install ethers
npm install --save-dev slither-analyzer
初始化Hardhat:
npx hardhat init
选择TypeScript项目,安装依赖:
npm install --save-dev ts-node typescript @types/node @types/mocha
目录结构:
contract-audit-demo/
├── contracts/
│ ├── Reentrancy.sol
│ ├── Overflow.sol
│ ├── AccessControl.sol
│ ├── GasTrap.sol
│ ├── LogicError.sol
│ ├── OracleDependency.sol
├── scripts/
│ ├── deploy.ts
├── test/
│ ├── Audit.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./"
},
"include": ["hardhat.config.ts", "scripts", "test"]
}
hardhat.config.ts:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 1337,
},
},
};
export default config;
跑本地节点:
npx hardhat node
跑Slither分析:
slither .
重入攻击是合约大杀器,攻击者通过回调重复调用合约,篡改状态。
contracts/Reentrancy.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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;
}
}
withdraw先发送ETH(call),后更新balances。fallback或receive函数在ETH到达时再次调用withdraw,重复提取。withdraw(1 ETH),触发call,ETH到达攻击者合约。receive再次调用withdraw,余额未更新,继续提取。slither .,提示reentrancy-eth漏洞,建议检查调用顺序。contracts/Reentrancy.sol(更新):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
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");
}
}
ReentrancyGuard。nonReentrant修饰符防止重复进入。balances),后发送ETH。nonReentrant设置锁,执行完函数释放,防止重入。reentrancy-eth警告。test/Audit.test.ts:
import { ethers } from "hardhat";
import { expect } from "chai";
import { VulnerableBank, SecureBank } from "../typechain-types";
describe("Reentrancy", function () {
let vulnerable: VulnerableBank;
let secure: SecureBank;
let attacker: any;
let owner: any;
beforeEach(async function () {
[owner, attacker] = await ethers.getSigners();
const VulnerableFactory = await ethers.getContractFactory("VulnerableBank");
vulnerable = await VulnerableFactory.deploy();
await vulnerable.deployed();
const SecureFactory = await ethers.getContractFactory("SecureBank");
secure = await SecureFactory.deploy();
await secure.deployed();
const AttackerFactory = await ethers.getContractFactory("ReentrancyAttacker");
attacker = await AttackerFactory.deploy(vulnerable.address);
await attacker.deployed();
});
it("should allow reentrancy attack on VulnerableBank", async function () {
await vulnerable.deposit({ value: ethers.utils.parseEther("1") });
await attacker.attack({ value: ethers.utils.parseEther("1") });
expect(await ethers.provider.getBalance(vulnerable.address)).to.equal(0);
});
it("should prevent reentrancy on SecureBank", async function () {
const AttackerFactory = await ethers.getContractFactory("ReentrancyAttacker");
const secureAttacker = await AttackerFactory.deploy(secure.address);
await secureAttacker.deployed();
await secure.deposit({ value: ethers.utils.parseEther("1") });
await expect(secureAttacker.attack({ value: ethers.utils.parseEther("1") })).to.be.revertedWith("ReentrancyGuard: reentrant call");
});
});
contracts/ReentrancyAttacker.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ReentrancyAttacker {
VulnerableBank public bank;
constructor(address _bank) {
bank = VulnerableBank(_bank);
}
function attack() public payable {
bank.deposit{value: msg.value}();
bank.withdraw(msg.value);
}
receive() external payable {
if (address(bank).balance >= msg.value) {
bank.withdraw(msg.value);
}
}
}
VulnerableBank发起重入攻击,掏空余额。SecureBank用nonReentrant阻止攻击,调用被revert。nonReentrant增加少量Gas(~2k),但安全性大幅提升。Solidity <0.8.0容易发生溢出/下溢,0.8.x已默认检查。
contracts/Overflow.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0; // Vulnerable version
contract VulnerableMath {
uint256 public balance;
function addBalance(uint256 amount) public {
balance += amount;
}
function subtractBalance(uint256 amount) public {
balance -= amount;
}
}
uint256溢出:2^256 - 1 + 1变0。uint256下溢:0 - 1变2^256 - 1。addBalance(2^256 - balance),使balance变0。subtractBalance(1),使balance变最大值。arithmetic漏洞,建议加检查。contracts/Overflow.sol(更新):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract SecureMath {
uint256 public balance;
function addBalance(uint256 amount) public {
balance += amount; // Safe in 0.8.20
}
function subtractBalance(uint256 amount) public {
require(balance >= amount, "Underflow");
balance -= amount;
}
}
SafeMath(0.8.x前)。require)防止下溢。arithmetic警告。test/Audit.test.ts(添加):
import { VulnerableMath, SecureMath } from "../typechain-types";
describe("Overflow", function () {
let vulnerable: VulnerableMath;
let secure: SecureMath;
beforeEach(async function () {
const VulnerableFactory = await ethers.getContractFactory("VulnerableMath", { signer: owner, libraries: {} });
vulnerable = await VulnerableFactory.deploy();
await vulnerable.deployed();
const SecureFactory = await ethers.getContractFactory("SecureMath");
secure = await SecureFactory.deploy();
await secure.deployed();
});
it("should allow overflow in VulnerableMath", async function () {
await vulnerable.addBalance(ethers.constants.MaxUint256);
await vulnerable.addBalance(1);
expect(await vulnerable.balance()).to.equal(0);
});
it("should prevent overflow in SecureMath", async function () {
await expect(secure.addBalance(ethers.constants.MaxUint256)).to.be.reverted;
});
it("should prevent underflow in SecureMath", async function () {
await expect(secure.subtractBalance(1)).to.be.revertedWith("Underflow");
});
});
VulnerableMath允许溢出,balance变0。SecureMath阻止溢出和下溢,抛出异常。权限控制不严,恶意用户可调用敏感函数。
contracts/AccessControl.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableAccess {
address public owner;
uint256 public funds;
constructor() {
owner = msg.sender;
}
function withdraw(uint256 amount) public {
require(funds >= amount, "Insufficient funds");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
funds -= amount;
}
}
withdraw无权限检查,任何用户可调用。funds。missing-access-control,建议加权限。contracts/AccessControl.sol(更新):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureAccess is Ownable {
uint256 public funds;
constructor() Ownable() {}
function deposit() public payable {
funds += msg.value;
}
function withdraw(uint256 amount) public onlyOwner {
require(funds >= amount, "Insufficient funds");
funds -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Ownable,限制withdraw为onlyOwner。missing-access-control警告。Ownable增加少量Gas(~1k),但权限安全。test/Audit.test.ts(添加):
import { VulnerableAccess, SecureAccess } from "../typechain-types";
describe("AccessControl", function () {
let vulnerable: VulnerableAccess;
let secure: SecureAccess;
let owner: any, attacker: any;
beforeEach(async function () {
[owner, attacker] = await ethers.getSigners();
const VulnerableFactory = await ethers.getContractFactory("VulnerableAccess");
vulnerable = await VulnerableFactory.deploy();
await vulnerable.deployed();
const SecureFactory = await ethers.getContractFactory("SecureAccess");
secure = await SecureFactory.deploy();
await secure.deployed();
await owner.sendTransaction({ to: vulnerable.address, value: ethers.utils.parseEther("1") });
await secure.deposit({ value: ethers.utils.parseEther("1") });
});
it("should allow unauthorized access in VulnerableAccess", async function () {
await vulnerable.connect(attacker).withdraw(ethers.utils.parseEther("1"));
expect(await ethers.provider.getBalance(vulnerable.address)).to.equal(0);
});
it("should restrict access in SecureAccess", async function () {
await expect(secure.connect(attacker).withdraw(ethers.utils.parseEther("1"))).to.be.revertedWith("Ownable: caller is not the owner");
await secure.connect(owner).withdraw(ethers.utils.parseEther("1"));
expect(await ethers.provider.getBalance(secure.address)).to.equal(0);
});
});
VulnerableAccess允许任何人提取资金。SecureAccess限制withdraw为owner,攻击者调用失败。复杂逻辑或循环可能耗尽Gas,导致拒绝服务。
contracts/GasTrap.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableLoop {
address[] public users;
function addUser(address user) public {
users.push(user);
}
function distributeFunds() public {
for (uint256 i = 0; i < users.length; i++) {
(bool success, ) = users[i].call{value: 1 ether}("");
require(success, "Transfer failed");
}
}
}
distributeFunds循环发送ETH,users数组过大耗尽Gas。addUser,增加循环成本。gas-limit,建议优化循环。distributeFunds失败。contracts/GasTrap.sol(更新):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SecureLoop {
mapping(address => bool) public users;
uint256 public userCount;
mapping(address => uint256) public pendingWithdrawals;
function addUser(address user) public {
require(!users[user], "User already added");
users[user] = true;
userCount++;
}
function deposit() public payable {}
function withdraw() public {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds");
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function distributeFunds() public {
uint256 amount = address(this).balance / userCount;
for (uint256 i = 0; i < userCount; i++) {
address user = users.keys[i]; // Assume keys tracked separately
pendingWithdrawals[user] += amount;
}
}
}
mapping代替数组,降低存储成本。withdraw)代替拉式(distributeFunds)。distributeFunds只更新pendingWithdrawals,不直接转账。gas-limit警告。mapping不直接支持遍历。test/Audit.test.ts(adding):
import { VulnerableLoop, SecureLoop } from "../typechain-types";
describe("GasTrap", function () {
let vulnerable: VulnerableLoop;
let secure: SecureLoop;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const VulnerableFactory = await ethers.getContractFactory("VulnerableLoop");
vulnerable = await VulnerableFactory.deploy();
await vulnerable.deployed();
const SecureFactory = await ethers.getContractFactory("SecureLoop");
secure = await SecureFactory.deploy();
await secure.deployed();
await vulnerable.addUser(user1.address);
await secure.addUser(user1.address);
await owner.sendTransaction({ to: vulnerable.address, value: ethers.utils.parseEther("1") });
await secure.deposit({ value: ethers.utils.parseEther("1") });
});
it("should fail with large user list in VulnerableLoop", async function () {
for (let i = 0; i < 1000; i++) {
await vulnerable.addUser(ethers.Wallet.createRandom().address);
}
await expect(vulnerable.distributeFunds()).to.be.reverted;
});
it("should handle large user list in SecureLoop", async function () {
await secure.distributeFunds();
await secure.connect(user1).withdraw();
expect(await ethers.provider.getBalance(secure.address)).to.be.lt(ethers.utils.parseEther("1"));
});
});
VulnerableLoop在大量用户时Gas超限。SecureLoop通过推式转账避免Gas问题。业务逻辑错误可能导致功能不符合预期。
contracts/LogicError.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableToken {
mapping(address => uint256) public balances;
uint256 public totalSupply;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
function mint(address to, uint256 amount) public {
totalSupply += amount;
balances[to] += amount;
}
}
mint无权限控制,任何人可铸造代币。missing-access-control和missing-events。contracts/LogicError.sol(update):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureToken is ERC20, Ownable {
constructor() ERC20("SecureToken", "STK") Ownable() {}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
ERC20和Ownable。mint限制为onlyOwner。ERC20自带Transfer事件,记录代币转移。test/Audit.test.ts(adding):
import { VulnerableToken, SecureToken } from "../typechain-types";
describe("LogicError", function () {
let vulnerable: VulnerableToken;
let secure: SecureToken;
let owner: any, attacker: any;
beforeEach(async function () {
[owner, attacker] = await ethers.getSigners();
const VulnerableFactory = await ethers.getContractFactory("VulnerableToken");
vulnerable = await VulnerableFactory.deploy();
await vulnerable.deployed();
const SecureFactory = await ethers.getContractFactory("SecureToken");
secure = await SecureFactory.deploy();
await secure.deployed();
});
it("should allow unauthorized minting in VulnerableToken", async function () {
await vulnerable.connect(attacker).mint(attacker.address, 1000);
expect(await vulnerable.balances(attacker.address)).to.equal(1000);
});
it("should restrict minting in SecureToken", async function () {
await expect(secure.connect(attacker).mint(attacker.address, 1000)).to.be.revertedWith("Ownable: caller is not the owner");
await secure.connect(owner).mint(owner.address, 1000);
expect(await secure.balanceOf(owner.address)).to.equal(1000);
});
});
VulnerableToken允许任何人铸造。SecureToken限制mint为owner。依赖不可靠预言机可能导致数据被操控。
contracts/OracleDependency.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableOracle {
address public oracle;
uint256 public price;
constructor(address _oracle) {
oracle = _oracle;
}
function updatePrice(uint256 _price) public {
require(msg.sender == oracle, "Not oracle");
price = _price;
}
function buy(uint256 amount) public payable {
require(msg.value >= amount * price, "Insufficient payment");
// Process purchase
}
}
oracle可随意更新price,无验证。oracle,可设置错误价格。unprotected-oracle。contracts/OracleDependency.sol(update):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract SecureOracle is Ownable {
AggregatorV3Interface public oracle;
uint256 public price;
constructor(address _oracle) Ownable() {
oracle = AggregatorV3Interface(_oracle);
}
function updatePrice() public onlyOwner {
(, int256 answer,,,) = oracle.latestRoundData();
require(answer > 0, "Invalid price");
price = uint256(answer);
}
function buy(uint256 amount) public payable {
require(msg.value >= amount * price, "Insufficient payment");
// Process purchase
}
}
AggregatorV3Interface获取可信价格。updatePrice限制为onlyOwner。unprotected-oracle警告。test/Audit.test.ts(adding):
import { VulnerableOracle, SecureOracle } from "../typechain-types";
describe("OracleDependency", function () {
let vulnerable: VulnerableOracle;
let secure: SecureOracle;
let owner: any, attacker: any;
beforeEach(async function () {
[owner, attacker] = await ethers.getSigners();
const VulnerableFactory = await ethers.getContractFactory("VulnerableOracle");
vulnerable = await VulnerableFactory.deploy(attacker.address);
await vulnerable.deployed();
const SecureFactory = await ethers.getContractFactory("SecureOracle");
secure = await SecureFactory.deploy("0x694AA1769357215DE4FAC081bf1f309aDC325306");
await secure.deployed();
});
it("should allow oracle manipulation in VulnerableOracle", async function () {
await vulnerable.connect(attacker).updatePrice(1);
expect(await vulnerable.price()).to.equal(1);
});
it("should restrict oracle updates in SecureOracle", async function () {
await expect(secure.connect(attacker).updatePrice()).to.be.revertedWith("Ownable: caller is not the owner");
await secure.connect(owner).updatePrice();
expect(await secure.price()).to.be.gt(0);
});
});
VulnerableOracle允许攻击者操控价格。SecureOracle限制更新并验证价格。scripts/deploy.ts:
import { ethers } from "hardhat";
async function main() {
const [owner] = await ethers.getSigners();
const VulnerableBankFactory = await ethers.getContractFactory("VulnerableBank");
const vulnerableBank = await VulnerableBankFactory.deploy();
await vulnerableBank.deployed();
console.log(`VulnerableBank deployed to: ${vulnerableBank.address}`);
const SecureBankFactory = await ethers.getContractFactory("SecureBank");
const secureBank = await SecureBankFactory.deploy();
await secureBank.deployed();
console.log(`SecureBank deployed to: ${secureBank.address}`);
const VulnerableMathFactory = await ethers.getContractFactory("VulnerableMath", { signer: owner, libraries: {} });
const vulnerableMath = await VulnerableMathFactory.deploy();
await vulnerableMath.deployed();
console.log(`VulnerableMath deployed to: ${vulnerableMath.address}`);
const SecureMathFactory = await ethers.getContractFactory("SecureMath");
const secureMath = await SecureMathFactory.deploy();
await secureMath.deployed();
console.log(`SecureMath deployed to: ${secureMath.address}`);
const VulnerableAccessFactory = await ethers.getContractFactory("VulnerableAccess");
const vulnerableAccess = await VulnerableAccessFactory.deploy();
await vulnerableAccess.deployed();
console.log(`VulnerableAccess deployed to: ${vulnerableAccess.address}`);
const SecureAccessFactory = await ethers.getContractFactory("SecureAccess");
const secureAccess = await SecureAccessFactory.deploy();
await secureAccess.deployed();
console.log(`SecureAccess deployed to: ${secureAccess.address}`);
const VulnerableLoopFactory = await ethers.getContractFactory("VulnerableLoop");
const vulnerableLoop = await VulnerableLoopFactory.deploy();
await vulnerableLoop.deployed();
console.log(`VulnerableLoop deployed to: ${vulnerableLoop.address}`);
const SecureLoopFactory = await ethers.getContractFactory("SecureLoop");
const secureLoop = await SecureLoopFactory.deploy();
await secureLoop.deployed();
console.log(`SecureLoop deployed to: ${secureLoop.address}`);
const VulnerableTokenFactory = await ethers.getContractFactory("VulnerableToken");
const vulnerableToken = await VulnerableTokenFactory.deploy();
await vulnerableToken.deployed();
console.log(`VulnerableToken deployed to: ${vulnerableToken.address}`);
const SecureTokenFactory = await ethers.getContractFactory("SecureToken");
const secureToken = await SecureTokenFactory.deploy();
await secureToken.deployed();
console.log(`SecureToken deployed to: ${secureToken.address}`);
const VulnerableOracleFactory = await ethers.getContractFactory("VulnerableOracle");
const vulnerableOracle = await VulnerableOracleFactory.deploy(owner.address);
await vulnerableOracle.deployed();
console.log(`VulnerableOracle deployed to: ${vulnerableOracle.address}`);
const SecureOracleFactory = await ethers.getContractFactory("SecureOracle");
const secureOracle = await SecureOracleFactory.deploy("0x694AA1769357215DE4FAC081bf1f309aDC325306");
await secureOracle.deployed();
console.log(`SecureOracle deployed to: ${secureOracle.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
跑部署:
npx hardhat run scripts/deploy.ts --network hardhat
slither .,检查reentrancy、arithmetic、access-control等。跑代码,体验Solidity合约审计的硬核玩法吧!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!