01 / Mental model
把合约看成状态机
合约不是运行在服务器上的普通 API。它部署在链上,有持久化状态;用户或其他合约通过交易调用函数,节点执行代码并把状态变化写入链上。
状态变量保存在链上的数据,读取通常不需要发送交易。
函数描述允许发生的状态变化和调用条件。
事件把重要变化记录为日志,供前端和索引器读取。
02 / First contract
一个最小的可读合约
下面的示例保存一个数字,只有拥有者可以更新,并在更新时发出事件。重点不是复制代码,而是观察状态、权限和日志之间的关系。
contract Counter {
address public owner;
uint256 public value;
event ValueChanged(address indexed caller, uint256 value);
constructor() {
owner = msg.sender;
}
function set(uint256 nextValue) external {
require(msg.sender == owner, "not owner");
value = nextValue;
emit ValueChanged(msg.sender, nextValue);
}
}读代码时先问:谁能调用?哪些状态会变化?外部调用在哪里?失败时状态是否已经更新?
03 / Access control
权限不是注释,是代码路径。
合约中的管理员、升级者、暂停者和资金提取者都应有明确的权限边界。不要只看函数名字中的 admin 或 safe,要沿着修饰器、继承和内部调用确认真实控制者。
| 检查点 | 要确认的问题 |
|---|---|
| 初始化 | 部署时 owner、管理员和关键地址由谁设置?是否可能被零地址或错误地址初始化? |
| 升级 | 谁能升级实现合约?升级是否需要多签或时间锁? |
| 外部调用 | 调用未知合约后,当前状态和重入防护是否正确? |
| 紧急开关 | 暂停功能能暂停哪些路径?恢复权限由谁掌握? |
04 / Test first
把假设写成测试
至少测试成功路径、权限拒绝、边界输入和重复调用。涉及资金时,再测试余额守恒、失败回滚、授权范围和时间/价格变化。
01Arrange准备账户、初始状态和输入。
02Act执行一次调用或交易。
03Assert检查状态、事件和余额。
04Fuzz用多组边界输入验证不变量。
05 / Boundary
部署前的安全底线
教程代码没有经过审计,也没有覆盖真实协议的全部复杂性。部署前需要锁定编译器版本、运行单元和集成测试、检查依赖与权限,并在测试网或本地 fork 中验证完整资金路径。
不要做:把教程代码直接部署到真实资产环境;把私钥写进仓库、前端或脚本;把管理员权限交给未经核验的地址。