Shipping a Solidity contract to mainnet? Do this 20-minute self-check first

Shipping a Solidity contract to mainnet? Do this 20-minute self-check first

准备将 Solidity 合约部署到主网?请先进行这 20 分钟的自查

You built something. Tests pass. You’re days from mainnet. Before you either skip security entirely (please don’t) or spend weeks lining up a full audit, here’s a self-check you can run in 20 minutes that catches the mistakes I see most often in first-time deployments. I run security reviews for small and new protocols, and the same handful of issues come up again and again. None of these need a tool — just your eyes and this list.

你构建了产品,测试也已通过,距离主网部署仅剩几天时间。在完全跳过安全检查(请千万别这样做)或花费数周时间排队等待全面审计之前,这里有一份 20 分钟的自查清单,可以帮你捕捉到初次部署中最常见的错误。我经常为小型和新兴协议进行安全审查,发现同样的问题反复出现。这些检查不需要任何工具,只需你的双眼和这份清单。

1. Who can call what?

1. 谁可以调用什么?

Open every external/public function that moves funds, mints, pauses, or upgrades. For each, ask: should a random address be able to call this? If not — is there an onlyOwner / onlyRole / require(msg.sender == ...) guarding it, in the function itself or in every internal function it calls? The classic bug isn’t a missing modifier. It’s a function that looks unguarded but delegates to a guarded internal one (fine), or one that looks guarded but the guard is in a branch a caller can skip (not fine). Trace the call, don’t trust the signature.

打开每一个涉及资金转移、铸造、暂停或升级的外部/公共函数。对于每一个函数,请自问:随机地址是否应该能够调用它?如果不能,那么在函数本身或它调用的每个内部函数中,是否有 onlyOwner / onlyRole / require(msg.sender == ...) 进行保护?经典的漏洞并非缺少修饰符,而是函数看起来未受保护但委托给了受保护的内部函数(没问题),或者看起来受保护但保护逻辑位于调用者可以跳过的分支中(有问题)。请追踪调用路径,不要只看函数签名。

2. The first-depositor trap (if you have a vault)

2. 首位存款人陷阱(如果你有金库合约)

If you mint shares from deposits (ERC-4626 or anything share-based), the first depositor can sometimes donate assets directly to the contract to inflate the share price, so the second depositor rounds down to zero shares and loses funds. Fix: virtual shares, a dead-shares mint at deploy, or a minimum-liquidity lock. OpenZeppelin’s ERC-4626 handles this out of the box — a hand-rolled vault usually doesn’t.

如果你通过存款铸造份额(ERC-4626 或任何基于份额的逻辑),首位存款人有时可以直接向合约捐赠资产以抬高份额价格,导致第二位存款人的份额向下取整为零并损失资金。解决方法:使用虚拟份额、在部署时铸造“死份额”(dead-shares),或设置最小流动性锁定。OpenZeppelin 的 ERC-4626 原生处理了这个问题,但手写的金库合约通常没有。

3. Reentrancy — but only the real kind

3. 重入攻击——仅限真正的重入

Not every external call is reentrancy. It’s a bug when an attacker-controlled call can re-enter and corrupt shared storage before you’ve updated it. Quick checks: Do you update state before the external transfer (checks-effects-interactions)? Is there a nonReentrant on functions that move value? Is the call target a trusted, immutable contract, or an arbitrary address the attacker supplies? A call to a protocol-owned contract, or a memory/local variable written after the call, is usually not exploitable. Don’t flag it just because Slither did.

并非所有的外部调用都是重入攻击。只有当攻击者控制的调用能够在状态更新前重入并破坏共享存储时,才构成漏洞。快速检查:你在外部转账前是否更新了状态(检查-生效-交互模式)?移动价值的函数是否有 nonReentrant 修饰?调用目标是受信任的不可变合约,还是攻击者提供的任意地址?对协议自有合约的调用,或者在调用后写入内存/局部变量的操作,通常是不可利用的。不要仅仅因为 Slither 报了警就将其标记为漏洞。

4. Where do “rescue” funds go?

4. “救援”资金流向何处?

Got a sweep / rescue / emergencyWithdraw? Check the destination. If funds can only go to a fixed, stored address (treasury/owner), permissionless is fine — the caller can’t redirect them. If the caller picks the destination, that’s a drain waiting to happen. This one is a coin-flip in the reviews I do.

有清理/救援/紧急提现功能吗?检查目标地址。如果资金只能流向固定的存储地址(金库/所有者),那么无权限调用是没问题的——调用者无法重定向资金。如果调用者可以指定目标地址,那这就是一个随时可能被掏空的漏洞。在我做的审查中,这个问题出现的概率各占一半。

5. Oracle freshness (if you read a price)

5. 预言机新鲜度(如果你读取价格)

Reading Chainlink? You need an updatedAt staleness check, not just answer > 0. But — and this is where half the “findings” you’ll read online are wrong — plenty of protocols check it one layer up or rely on a heartbeat. Look at the whole path before you panic.

正在读取 Chainlink?你需要进行 updatedAt 的陈旧度检查,而不仅仅是 answer > 0。但是——这也是网上很多“发现”错误的地方——许多协议会在上一层进行检查或依赖心跳机制。在恐慌之前,请查看完整的调用路径。

6. Upgradeable? Check the initializer.

6. 可升级合约?检查初始化器。

Proxy pattern? Make sure initialize() has the initializer modifier, can’t be called twice, and that the implementation contract itself can’t be initialized-then-selfdestructed. A left-open initializer is one of the most common takeover bugs.

使用代理模式?确保 initialize() 具有 initializer 修饰符,不能被调用两次,并且实现合约本身不能被初始化后自毁。未关闭的初始化器是最常见的接管漏洞之一。


That’s the 20-minute pass. It catches the obvious stuff — and honestly, most exploits of small protocols are the obvious stuff shipped in a hurry. What it doesn’t catch is the protocol-specific logic bug: the one where your staking math rounds the wrong way, or two functions interact in a way nobody drew on the whiteboard. That’s what a human review is for.

这就是 20 分钟的自查流程。它能捕捉到显而易见的问题——老实说,大多数针对小型协议的攻击都是因为匆忙上线而留下的低级错误。它无法捕捉的是协议特定的逻辑漏洞:比如质押数学计算取整错误,或者两个函数以没人预料到的方式产生交互。这正是人工审查的意义所在。

My rule when I do those reviews: I only flag what’s actually exploitable. I keep my scanner at zero false positives across the entire OpenZeppelin library — so when it (or I) stay silent on your code, that silence means something. A report that cries wolf 40 times trains you to ignore the one that matters.

我在进行审查时的原则是:只标记真正可利用的漏洞。我在整个 OpenZeppelin 库中保持扫描器零误报——所以当它(或我)对你的代码保持沉默时,这种沉默是有意义的。一份报了 40 次“狼来了”的报告只会让你忽略真正重要的问题。

If you want a free first pass before you ship, the scanner is open-source and runs in one command: https://github.com/juan23z/openclaw-audit. And if you’d rather have a person read the tricky parts — fast, honest, and I’ll tell you plainly when your code is solid — that’s what I do. Good luck on mainnet. 🚀

如果你想在上线前进行一次免费的初步检查,这个扫描器是开源的,只需一条命令即可运行:https://github.com/juan23z/openclaw-audit。如果你更希望有人来阅读那些棘手的部分——快速、诚实,并且我会直截了当地告诉你代码何时稳健——这正是我的工作。祝主网部署顺利。🚀