W3

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 也发生变化

切换其他语言