Solidity
0.8.24+EVM 系(以太坊 / Arbitrum / Base / BSC / Polygon 等)
EVM 事实标准语言,生态、工具链与审计资料最完整
Solidity 是静态类型、面向合约的语言,编译为 EVM 字节码。0.8 起算术运算默认带溢出检查,不再需要 SafeMath。学习重点是「类型与存储位置」「可见性与可变性」「外部调用的安全边界」三块。
8 个语法章节17 段可复制代码含在线练习
从 Solidity 视角看差异
如果已经会 Solidity,这张表比从头读语法快得多。
| 差异点 | 说明 |
|---|---|
| 存储位置 | storage / memory / calldata 必须显式声明,写错位置不只是报错,还直接决定 Gas 成本。 |
| 地址与转账 | address 与 address payable 分开;transfer / send 已不推荐,统一用 call{value:}。 |
| 默认可见性 | 函数必须显式标注可见性与状态可变性,编译器会强制要求,没有隐式默认。 |
| 错误处理 | 优先用自定义 error + revert,比字符串 require 省 Gas 且能被前端解出错误名。 |
1
变量与类型
3 段代码值类型直接存值,引用类型存位置。选错类型会连带影响存储布局与 Gas。
- uint 是 uint256 的别名;能用更小宽度就说明该值会被打包进同一存储槽
- address 存 20 字节地址,判断零地址用 address(0)
- bytes32 适合存哈希与定长标识;string 只适合人类可读文本
- constant 在编译期内联、immutable 在部署时写入代码段,两者都读不花 Gas
- 固定长度数组 T[k] 与动态数组 T[] 的存储布局不同,前者会连续占槽
值类型与常量
值类型与常量Solidity
结构体与枚举
结构体与枚举Solidity
单位与时间
单位与时间Solidity
区块时间戳由出块者给出,只能保证单调递增、不保证精确,别拿它做秒级精度的判断。
2
函数、可见性与状态可变性
3 段代码可见性决定谁能调用,状态可变性决定能不能读写链上状态,两者共同决定 ABI 与调用成本。
- external 比 public 便宜:外部调用时 calldata 直接读,不复制到 memory
- view 只读状态,pure 连状态都不读;二者都是 eth_call,不上链、不花 Gas
- payable 才能接收 ETH,非 payable 函数被附带 ETH 调用会直接 revert
- internal / private 只影响函数可访问性,链上数据本身仍然公开可读
可见性矩阵
可见性矩阵Solidity
返回多值、命名返回与函数重载
返回多值、命名返回与函数重载Solidity
接收 ETH:receive 与 fallback
接收 ETH:receive 与 fallbackSolidity
用 call 转账必须检查返回值 ok;忽略返回值是老代码里最典型的资金卡死原因。
3
修饰器与访问控制
2 段代码modifier 在函数体前后插入校验,是权限与重入保护的标准写法。
- modifier 可带参数,可用 _; 控制「原函数体在何处执行」
- 重入锁写成 modifier 后,所有外部入口都能统一加锁
- 权限检查优先用自定义 error,前端能拿到错误名做友好提示
- tx.origin 不能用于授权判断,会直接被钓鱼合约绕过
权限与重入锁
权限与重入锁Solidity
参数化修饰器
参数化修饰器Solidity
4
事件与日志
1 段代码事件是链上唯一「便宜且可检索」的输出通道。indexed 参数进 topics,可被高效过滤。
- 每个事件最多 3 个 indexed 参数,值类型直接进 topic,引用类型存哈希
- 非 indexed 参数存在 data 段,便宜但只能全量拉取后自己过滤
- 事件不消耗存储,但消耗日志 Gas(每 topic 375 + 每字节 8)
- 前端收事件用 eth_getLogs 或订阅 newLogs,注意重组会带来回滚
事件定义与索引
事件定义与索引Solidity
按标准定义事件不是可选项:区块浏览器、钱包、索引器都依赖字段顺序与 indexed 布局。
5
错误处理
2 段代码require 用于输入校验,revert 用于条件分支,assert 只用于「不可能发生」的内部不变量。
- 自定义 error 比字符串 require 省 Gas,且 ABI 里带参数便于定位
- assert 失败消耗掉全部剩余 Gas,是给程序员看的,不是给用户看的
- try / catch 只能包住外部调用与合约创建,不能包内部函数
- catch Error(string) 捕获 require 字符串,catch (bytes) 捕获自定义 error 与 panic
require / revert / assert
require / revert / assertSolidity
try / catch 处理外部调用
try / catch 处理外部调用Solidity
6
映射、数组与存储布局
2 段代码mapping 无法遍历,数组可遍历但删除要小心。存储槽按 32 字节对齐,打包决定成本。
- mapping 的 key 不存储,只存 value,所以没有长度也无法枚举
- 删除数组元素的标准做法是「与末尾交换后 pop」,避免留下空洞
- 同一存储槽内的小类型会打包;跨槽读写会带来额外的 SLOAD/SSTORE
- delete 只是把值置零,会退还部分 Gas,但不清除 mapping 内部条目
动态数组的安全删除
动态数组的安全删除Solidity
打包存储与嵌套映射
打包存储与嵌套映射Solidity
常见优化顺序:把 uint256 放最前、小类型扎堆,并避免在同一函数里反复跨槽读写。
7
接口与外部调用
2 段代码合约之间的调用是最容易出安全问题的地方:谁在调用、调用了什么、失败了怎么办。
- 优先用 interface 声明目标合约,编译期就能校验函数签名
- 低级调用 call 返回 (bool ok, bytes data),必须自己检查 ok
- delegatecall 在调用者上下文执行目标代码,存储布局不匹配会直接写坏状态
- 调用外部合约前先改自己的状态再转账(Checks-Effects-Interactions)
接口调用与低级调用
接口调用与低级调用Solidity
Checks-Effects-Interactions 顺序
Checks-Effects-Interactions 顺序Solidity
顺序反了就是经典重入漏洞。加 nonReentrant 是兜底,不是替代品。
8
继承、接口与库
2 段代码Solidity 支持多重继承,靠 C3 线性化确定 override 顺序。
- 父合约的构造函数按「继承列表从左到右」的顺序执行
- 多父合约存在同名函数时必须显式 override(A, B)
- 未实现的函数标 virtual + 抽象合约,接口里的函数默认就是 virtual
- library 用 internal 时被内联进调用方,用 public 时通过 delegatecall 调用
继承与 override
继承与 overrideSolidity
库与 using for
库与 using forSolidity
本章练习
这一段要让你看懂:EIP-165 的 interfaceId 是接口里所有函数选择器的 XOR(异或)。任意一个函数签名写错、或多写少写一个函数,算出来的 id 就完全不同——这也是它能用来做能力探测的原因。
▶
试一试 · 算出一个接口的 ID
可直接改代码 · 在浏览器沙箱里真跑playground.js(已注入 ethers v6)JavaScript
运行环境:iframe 沙箱(无 cookie 权限)。只支持纯 JS 片段,不支持 import / require;需要模块请用下方的完整示例复制到项目里。
检查点(跑完后逐条核对)
- 按 8 个函数算出的 id 正好是 0x80ac58cd
- 去掉 getApproved 后 id 发生变化
- 把 ownerOf 拼错成 owner 后 id 也发生变化