区块链从小白到精通(十):合约安全与上线——重入攻击、Gas 优化与最佳实践

📚 本文是「区块链从小白到精通」系列第 10 篇 | 上一篇:《区块链从小白到精通(九):手把手写一个 NFT 合约------ERC-721 与元数据全解析》 | 下一篇:系列完结

前言

传统软件出了 Bug,可以发个补丁;网站被黑了,可以回滚数据库。但智能合约不行------代码一旦部署即冻结,任何人(包括你自己)都无法修改。它锁在链上,直面全世界黑客的火力侦察,而它后面守着的往往是真金白银。

这不是理论风险:区块链历史上因合约漏洞被盗的金额累计已达数十亿美元,单笔最著名的损失就是本篇开头要讲的 The DAO 事件------它甚至改写了以太坊的历史。

所以这一篇是全系列的"毕业考试":重入攻击的原理与修复、高频漏洞清单、Gas 优化技巧、上线前检查清单。读完它,你写出的合约才配得上"敢部署"三个字。

The DAO 事件:一次盗窃,改写一条链的历史

2016 年,一个叫 The DAO 的众筹项目在以太坊上募集资金,短短几周锁进了约 360 万 ETH(当时约占全网 ETH 的 14%)。它本质上就是一个巨大的链上基金:出资人拥有份额,可以随时申请赎回。

当年 6 月,攻击者利用其赎回函数的一个漏洞,反复、递归地把 ETH 转出,卷走约 360 万 ETH。社区激烈争论后,以太坊通过一次有争议的硬分叉回滚了这笔盗窃------部分社区成员拒绝接受,继续维护原链,这就是今天以太坊经典(Ethereum Classic, ETC)的由来。

一笔代码漏洞,直接催生了一条新链。而这背后的漏洞,正是区块链安全的头号通缉犯:重入攻击(Reentrancy)。

重入攻击原理:先转钱,后清账

看一个"标准错误示范"------几乎所有新手都写过这个版本的提款函数:

solidity 复制代码
// ❌ 漏洞版:先转账,后清账
contract VulnerableVault {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "nothing to withdraw");

        (bool ok, ) = msg.sender.call{value: amount}("");  // ① 先转账
        require(ok, "transfer failed");
        balances[msg.sender] = 0;                          // ② 后清账
    }
}

逻辑看起来无懈可击:查询余额 → 转账 → 清零。漏洞藏在 ①:call 会把控制权交到对方合约手里 ------如果 msg.sender 是一个恶意合约,它的 receive() 会在收到转账的瞬间被执行,此时账还没清(②还没跑),余额依然是 amount。

于是攻击合约写一个 receive,里面再调一次 withdraw:

solidity 复制代码
// 攻击合约:在收到转账的瞬间再次提款
contract Attacker {
    VulnerableVault public target;

    constructor(address _target) { target = VulnerableVault(_target); }

    function attack() external payable {
        target.deposit{value: 1 ether}();
        target.withdraw();              // 点火
    }

    // 每次收到转账都会再触发一次提款,直到 Gas 耗尽/合约余额清空
    receive() external payable {
        if (address(target).balance >= 1 ether) {
            target.withdraw();
        }
    }
}

执行轨迹变成递归:

text 复制代码
withdraw → 转账 → 攻击者 receive() → withdraw → 转账 → receive() → ...
(此时 balances 一直没被清零,每次都按原额提款)

存 1 个 ETH,取走 N 个。"重入"这个名字就是"执行中途重新进入合约"的意思。

修复三件套

第一件:Checks-Effects-Interactions 模式(最重要)

把函数体调整为固定的三段顺序:先校验(Checks)、再改状态(Effects)、最后外部调用(Interactions):

solidity 复制代码
// ✅ 修复版:先清账,后转账
function withdraw() external {
    uint256 amount = balances[msg.sender];   // Checks
    require(amount > 0, "nothing to withdraw");

    balances[msg.sender] = 0;                // Effects:先清账!

    (bool ok, ) = msg.sender.call{value: amount}("");  // Interactions
    require(ok, "transfer failed");
}

攻击者再次重入时,balances 已是 0,require(amount > 0) 直接拦截。这一条顺序习惯,能消掉 90% 的重入风险,请形成肌肉记忆。

第二件:重入锁

OpenZeppelin 提供的 ReentrancyGuard 用一个锁变量保证同一函数不会被并发重入:

solidity 复制代码
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract SafeVault is ReentrancyGuard {
    function withdraw() external nonReentrant {
        // ...
    }
}

第三件:优先用内置转账

简单转账场景优先使用 transfer/send(固定 2300 Gas,低到不够对方再重入)或直接调用 call 但坚持第一件套。记住原则:任何把控制权交给外部合约的位置,都要假设对面站着攻击者。

高频漏洞清单:每一条都真实吞过钱

1. 整数溢出

0.8 版本之前,uint256 加减乘会静默回绕(0 - 1 = 2^256 - 1),真实项目因此损失惨重。0.8 起编译器内置了溢出检查,溢出直接 revert------但你仍需知道历史,且注意 unchecked 块里检查是被关闭的。

2. tx.origin 鉴权钓鱼

solidity 复制代码
// ❌ 用 tx.origin 做权限判断
function withdraw() external {
    require(tx.origin == owner);   // 危险!
}

tx.origin 是整笔交易的最原始发起人(永远是 EOA),而 msg.sender 才是直接调用者。如果用户被诱导调用了恶意合约,恶意合约再调你的合约,tx.origin 依然是用户本人------鉴权通过,资金被转移。权限判断永远用 msg.sender。

3. 权限中心化

onlyOwner 合约的 owner 能冻结转账、增发代币、提走资金。owner 是不是多签?能不能放弃权限?审计报告里这永远是第一问。生产项目应使用多签钱包(如 Safe)或时间锁管理特权操作。

4. 未检查的外部调用返回值

token.transfer(...) 返回 bool 而不是 revert(部分老代币如此)。不检查返回值,转账失败你的合约却以为成功了,账目就此错乱。用 SafeERC20 的 safeTransfer 一律处理。

5. 前端假地址

合约没问题,但前端页面被换上了假合约地址,或用户没核对就复制了钓鱼地址。这类损失不在合约层,防线是:官方渠道核对地址 + 用户签名前看清目标。

⚠️ 提醒:漏洞清单没有"学完"的一天。保持阅读 SlowMist、rekt 等安全媒体的事故复盘,是合约开发者最低成本的持续学习。

Gas 优化:省下来都是用户的真金白银

EVM 里最贵的资源是 storage(链上存储),SSTORE 写一个新槽约 2 万 Gas,而内存操作只要 3 Gas。实用优化按性价比排序:

1. storage 变量打包(Packaging)

EVM 以 32 字节为一个槽。把小于 32 字节的变量放同一个 struct 里声明时会尽量共用槽位:

solidity 复制代码
// ❌ 占两个槽:uint256 + uint256
struct Bad { uint256 amount; uint128 price; }

// ✅ 挤进一个槽:uint128 + uint128
struct Good { uint128 amount; uint128 price; }

2. calldata 替代 memory

外部函数的数组参数用 calldata 只读不拷贝,省一份拷贝的 Gas:

solidity 复制代码
function sum(uint256[] calldata nums) external pure returns (uint256 total) { ... }

3. 自定义 error 替代 require 字符串

字符串会永久存进字节码并在 revert 时消耗大量 Gas:

solidity 复制代码
// ❌ require(amount > 0, "amount must be greater than zero");
// ✅
error ZeroAmount();
if (amount == 0) revert ZeroAmount();

4. 缓存 storage 读取到内存

循环里反复读 storage 变量,先读进局部变量再用:

solidity 复制代码
uint256 len = users.length;      // 读一次 storage
for (uint256 i = 0; i < len; ++i) { ... }

5. 事件代替存储

只需展示、不需要被合约读取的数据(历史记录类),用 emit 存日志而非写 storage------日志便宜几个数量级,只是合约本身读不到。

上线前检查清单

检查项 说明
测试网全流程回归 部署 + 全部函数 + 边界值 + 异常路径,手工与脚本各跑一遍
自动化测试 Foundry/Hardhat 写单元测试,覆盖正常流、权限、失败路径
静态分析 跑一遍 Slither 等工具,逐条人工确认告警
权限方案 owner 是否多签(Safe)?是否需要时间锁、紧急暂停(Pausable)?
事件完整性 所有状态变更都有事件,方便链下索引与对账
源码验证 区块浏览器 Verify & Publish,让任何人可审计、钱包可解析调用
Gas 压测 关键路径在极端输入下的 Gas 上限是否可控
冷静期 上线前放下代码睡一觉,第二天以攻击者视角再读一遍

全系列回顾:从记账本到上线

十篇走完,回看这条路,其实每一步都踩在前一步上:

篇目 你学会了什么
一 区块链是什么:分布式账本、双花、三大特性
二 内部骨架:哈希、区块结构、链式存储、Merkle 树
三 共识机制:PoW / PoS / DPoS 的取舍
四 比特币:UTXO、挖矿、交易脚本
五 以太坊:账户模型、Gas、EVM、世界计算机
六 第一个合约:Remix、编译部署调用、测试网
七 Solidity 语法:类型、数据位置、modifier、event、错误处理
八 ERC-20:approve/transferFrom、手写实现、OpenZeppelin
九 ERC-721:归属模型、tokenURI、IPFS、生产级 NFT
十 安全与上线:重入、漏洞清单、Gas 优化、检查清单

进阶方向给你四个:① 工程化开发 ------从 Remix 迁移到 Foundry 或 Hardhat,写测试、跑脚本、CI/CD;② 读经典源码 ------Uniswap V2 的核心合约只有几百行,是优雅与安全的教科书;③ Layer2 开发 ------合约层面几乎零成本迁移,生态增长最快;④ 深读 EIP------以太坊的所有演进都发生在 EIP 里,读原始提案是最高效的进阶方式。

写在最后

系列完结,最后送你三句话:

  1. 区块链的本质是"用机制替代信任"------前五篇的所有数学与共识,都是在回答"没有村长,怎么让大家相信一本账";
  2. 智能合约的本质是"代码即法律,部署即冻结"------所以安全不是加分项,是及格线;
  3. 学习的方式是动手------Remix 里那个 Counter 合约你 10 分钟就能部署,别让"等学完再做"偷走你的第一行链上代码。

从第一篇的那个记账本故事,到这里能独立发行代币、铸造 NFT、防御重入攻击------十篇的旅程结束了,你的链上之旅才刚刚开始。部署你的第一个真正合约那天,记得回来留个言。

祝 Gas 长低,永无 revert。(完)

相关推荐
flash俊杰1 小时前
v0.5-01 让桌面 AI 助手记得住:预算组装、递归摘要、长期记忆与评估基线
数据库·安全·架构
五萝卜1 小时前
webshell流量分析靶场WP
安全
杨凯凡17 小时前
IT日报|2026-11-13:GitHub 大规模凭据攻击、Signal 形式化验证、Claude 新模型与 AI 安全诉讼
人工智能·安全·github
P1Browser18 小时前
账号触发平台安全审核?拆解标准申诉流程与材料清单
大数据·人工智能·安全·多账号管理·指纹浏览器·账户安全·运营合规
OxYGC1 天前
[Web3] 智能合约 04: 高级开发实战与 OpenZeppelin
web3·区块链·智能合约
Fnetlink11 天前
2026年企业安全组网供应商方案推荐
安全
如夏倾城1 天前
基于格的密码学
安全·密码学·量子计算·可信计算技术
如夏倾城1 天前
QUIC——HTTP/3 的唯一底层传输协议 及 0-RTT恢复
网络·网络协议·tcp/ip·安全·http
笑容掩饰我的失落1 天前
区块链从小白到精通(七):Solidity 语法精讲——数据类型、函数、修饰器与事件
算法·区块链·php·智能合约·哈希算法