3、Solidity速通:合约结构、变量与状态、函数修饰符、事件与日志、ABI编码解码、Gas优化基础

好,咱们直接进入正题。这一章我打算带你快速过一遍 Solidity 的核心语法。别担心,我不会让你死记硬背。我会把我在实际项目中踩过的坑、总结的习惯,一股脑儿倒给你。

你想想看,写套利机器人,合约就是你的“战场”。不懂合约结构,你连阵地都摆不好。不懂 Gas 优化,你赚的钱全给矿工交学费了。所以,这一章很重要。

3.1 合约结构:一个文件就是一个世界

一个 Solidity 文件,说白了就是一个独立的模块。我习惯把每个合约想象成一个“小公司”。它有老板(状态变量)、有员工(函数)、有账本(事件)。

最基本的骨架长这样:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MyArbitrage {
    // 状态变量 —— 公司的资产
    address public owner;
    uint256 public profit;

    // 事件 —— 公司的广播
    event ProfitTaken(address indexed trader, uint256 amount);

    // 修饰符 —— 公司的门禁
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    // 构造函数 —— 公司成立那天
    constructor() {
        owner = msg.sender;
    }

    // 函数 —— 公司的业务
    function takeProfit() external onlyOwner {
        // ...
        emit ProfitTaken(msg.sender, profit);
    }
}

嗯,这里要注意:pragma solidity ^0.8.0; 这个版本号声明,我建议你尽量用 0.8.x 系列。0.8 之后内置了溢出检查,省了你很多事。我以前在 0.6 版本上写过一个套利合约,因为没加 SafeMath,结果被一笔异常交易给整溢出了,亏了一笔手续费。从那以后,我所有新项目都直接上 0.8。

3.2 变量与状态:存储位置决定命运

Solidity 里的变量,按存储位置分三种:状态变量局部变量全局变量

  • 状态变量:永久存在链上,存在合约的存储里。读写都贵。
  • 局部变量:函数执行时临时存在内存里,用完就丢。便宜。
  • 全局变量:像 msg.senderblock.timestamp 这些,Solidity 自带的,直接拿来用。

我个人习惯把状态变量按功能分组,用注释隔开:

// --- 核心配置 ---
address public router;
uint256 public slippage;

// --- 运行时数据 ---
mapping(address => uint256) public lastTradeTime;

为什么这么分?因为你在写套利机器人时,经常要热更新配置。把配置和运行时数据分开,升级合约时逻辑更清晰。

避坑指南:状态变量默认是 internal 的。如果你想让外部直接读,记得加 public。但注意,public 会自动生成一个 getter 函数,会多花一点点 Gas。不过对于套利合约来说,这点 Gas 可以忽略。

3.3 函数修饰符:你的权限守卫

修饰符(modifier)是 Solidity 里我最喜欢的功能之一。它就像一道门禁,在函数执行前先检查条件。

最常见的用法就是权限控制:

modifier onlyEOA() {
    require(tx.origin == msg.sender, "Only EOA");
    _;
}

function withdraw() external onlyEOA {
    // 只有外部账户能调用
}

我曾经在写一个闪电贷套利合约时,忘了加 onlyOwner 修饰符,结果被一个机器人抢先调用了提现函数,把利润全卷走了。嗯,那之后我写合约第一件事就是先把修饰符列好。

修饰符还可以带参数:

modifier onlyAfter(uint256 _time) {
    require(block.timestamp >= _time, "Too early");
    _;
}

function execute() external onlyAfter(block.timestamp + 1 minutes) {
    // 1分钟后才能执行
}

3.4 事件与日志:链上的广播系统

事件(Event)是 Solidity 里最被低估的功能。很多人觉得它只是用来打印日志的,其实不然。事件是合约和外部世界通信的桥梁。

你的套利机器人前端,就是靠监听事件来知道“什么时候该行动”的。

event ArbitrageExecuted(
    address indexed tokenIn,
    address indexed tokenOut,
    uint256 amountIn,
    uint256 amountOut,
    uint256 profit
);

注意 indexed 关键字。加了 indexed 的参数,可以被前端高效地过滤和搜索。我一般把 address 类型和 uint256 类型的关键数据都加上 indexed

小技巧:事件里的参数最多只能有 3 个 indexed。如果你需要记录更多数据,可以把非关键数据放在后面不加 indexed,这样也能存,只是不能过滤。

3.5 ABI编码解码:合约间的“摩斯密码”

ABI(Application Binary Interface)说白了就是合约之间通信的“协议”。当你调用另一个合约的函数时,你需要把参数编码成 ABI 格式。

最常用的两个函数是 abi.encode()abi.decode()

// 编码
bytes memory data = abi.encodeWithSignature(
    "swap(uint256,uint256,address,bytes)",
    amountIn, amountOutMin, to, path
);

// 解码
(uint256 a, uint256 b) = abi.decode(data, (uint256, uint256));

我在写套利机器人时,经常需要动态构造调用数据。比如你要调用 Uniswap V2 的 swap 函数,就得用 abi.encodeWithSignature 把参数拼好。这里有个坑:函数签名里的参数类型必须和合约里完全一致,大小写都不能错。我曾经因为把 uint256 写成了 uint,结果调了半天调不通。

注意abi.encodePacked() 虽然省 Gas,但容易产生哈希碰撞。除非你非常清楚自己在做什么,否则我建议你优先用 abi.encode()

3.6 Gas优化基础:每一笔Gas都是利润

做套利,Gas 就是你的成本。优化 Gas 就是在直接提高利润率。我总结了几个最实用的技巧:

技巧 说明 节省量
使用 uint256 而非 uint8 EVM 对 256 位操作最友好,小类型反而要额外转换 中等
变量打包 把多个小变量放在一个 struct 里,减少存储槽
使用 immutable 构造函数中赋值后不再改变的变量,用 immutable
避免重复读取状态 把状态变量读到内存里,多次使用 中等
使用 unchecked 在确定不会溢出的地方,关闭溢出检查 低但频繁

举个例子,我写套利合约时,会把常用的地址用 immutable 存起来:

address public immutable WETH;
address public immutable factory;

constructor(address _weth, address _factory) {
    WETH = _weth;
    factory = _factory;
}

这样每次读取 WETH 时,EVM 直接把它当成常量嵌入代码,比读存储便宜得多。

核心原则:存储(Storage)是最贵的,内存(Memory)次之,栈(Stack)最便宜。尽量把数据往内存和栈上挪。

知识体系总览

下面这张图,是我自己梳理的 Solidity 核心知识脉络。你可以把它当成一个“速查地图”:

Solidity 核心 合约结构 变量与状态 函数修饰符 事件与日志 ABI编码解码 Gas优化基础 SPDX + pragma import 导入 storage/memory mapping/array onlyOwner 参数化修饰符 indexed 参数 事件监听 encode/decode encodeWithSignature immutable 变量打包

这张图把本章的六个核心知识点串起来了。你可以看到,合约结构是骨架,变量与状态是血肉,函数修饰符是门禁,事件与日志是喇叭,ABI 是翻译官,Gas 优化是省钱秘籍。缺一个,你的套利机器人就跑不顺畅。

好了,这一章的内容就到这里。记住,写合约不是写诗,是写工程。每一行代码都要想清楚:它花多少 Gas?它会不会被攻击?它能不能被升级?带着这些问题去写,你很快就能上手。


专注资料整理