入门
合约基础
Solidity 是以太坊智能合约的主要语言。pragma 设置编译器版本。合约类似于类。函数可以是 public、private、view(只读)或 payable。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 storedData;
function set(uint256 x) public {
storedData = x;
}
function get() public view returns (uint256) {
return storedData;
}
}Pragma 与编译器版本
pragma 指定编译器版本。^0.8.0 允许任何 0.8.x 补丁版本。生产环境应锁定确切版本(0.8.19)以避免意外。每个 .sol 文件顶部都应包含 pragma。
// Lock to a single version
pragma solidity 0.8.19;
// Allow patches (^0.8.0 means >=0.8.0 <0.9.0)
pragma solidity ^0.8.0;
// Range of versions
pragma solidity >=0.8.0 <0.9.0;
// Experimental features (ABIEncoderV2 in older versions)
pragma experimental ABIEncoderV2;注释与 NatSpec
NatSpec 注释(/// 或 /** */)生成文档。@notice 面向最终用户,@dev 面向开发者,@param 描述参数,@return 描述返回值。Etherscan 和 Remix 等工具会读取它们。
// Single-line comment
/* Multi-line
comment */
/// @title A Simple Contract
/// @author Alice
/// @notice This is visible to end users
/// @dev This is for developers
contract Commented {
/// @notice Sets the value
/// @param _x The new value
/// @dev Internal details here
function set(uint256 _x) public {}
}SPDX 许可证
自 Solidity 0.6.8 起,SPDX 许可证标识符在文件顶部是必需的。它帮助工具和用户了解代码如何被重用。MIT 用于宽松开源,GPL-3.0 用于 copyleft,UNLICENSED 用于私有代码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Common licenses:
// MIT - permissive
// GPL-3.0 - copyleft
// Apache-2.0 - permissive with patent grant
// UNLICENSED - private/no license
// BSD-2-Clause, BSD-3-Clause
contract Licensed {}文件结构与导入
import 引入合约、库或接口。使用 {Name} 仅导入特定项。相对路径按预期工作。'is' 关键字建立继承。文件通常有一个主合约,但也可以包含多个。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./OtherContract.sol";
import {SpecificContract, Helper} from "./Library.sol";
import * as Lib from "./Library.sol"; // namespace
import "openzeppelin/contracts/token/ERC20.sol";
contract MyContract is OtherContract {
// ...
}值类型
整数(uint / int)
uint = 无符号整数(无负数),int = 有符 号。大小从 8 到 256,步长为 8。uint 默认为 uint256。自 0.8.0 起,算术自动检查溢出/下溢并回滚。使用 type(T).max / type(T).min 获取边界。
uint256 public bigNumber = 1 ether; // 10^18
uint8 public small = 255; // max 255
int256 public signed = -100;
uint public defaultSize = 42; // uint256 by default
// Arithmetic
uint256 a = 10;
uint256 b = 3;
uint256 sum = a + b; // 13
uint256 diff = a - b; // 7 (0.8+ reverts on underflow)
uint256 product = a * b; // 30
uint256 quotient = a / b; // 3
uint256 remainder = a % b; // 1
uint256 power = a ** 2; // 100
// Type min/max (0.8+)
uint256 max = type(uint256).max;
int256 min = type(int256).min;布尔类型
bool 只能是 true 或 false。&& 和 || 短路求值,即只在需要时才评估右侧。默认值为 false。在某些上下文中(存储打包),布尔类型比 uint256 消耗更多 gas。
bool public flag = true;
bool public negated = !flag; // false
bool public and = flag && false; // false
bool public or = flag || false; // true
// Short-circuit evaluation
bool result = (flag && expensiveCall()); // skips call if flag false
bool result2 = (!flag || expensiveCall());
// Equality
bool equal = (1 == 1); // true
bool notEq = (1 != 2); // true
// Boolean defaults to false
bool public defaultValue; // false地址
address 保存 20 字节的以太坊地址。address payable 可以接收 Ether(有 transfer/send 方法)。使用 payable(x) 转换。地址有 .balance 属性(以 wei 为单位)。始终根据 EIP-55 校验和验证地址字面量以捕获拼写错误。
address public owner = msg.sender;
address payable public treasury = payable(owner);
// Balance (read-only)
uint256 bal = owner.balance; // in wei
// Send Ether (payable address only)
treasury.transfer(1 ether);
bool sent = treasury.send(1 ether);
// Convert from address to payable
address addr = 0x123...;
address payable payableAddr = payable(addr);
// Address literals (checksummed)
address public constant BURN = 0x000000000000000000000000000000000000dEaD;
// Compare addresses
bool same = (addr1 == addr2);字节与字节数组
固定字节(bytes1..bytes32)便宜,用于哈希/选择器。动态 bytes 用于原始二进制数据;string 用于 UTF-8 文本。使用 bytes32 存储哈希(keccak256 返回 bytes32)。bytes.concat()(0.8.4+)比 abi.encodePacked 更便宜。
// Fixed-size byte arrays (bytes1 to bytes32)
bytes32 public hash = keccak256(abi.encodePacked("data"));
bytes1 public b1 = 0x41; // single byte
bytes4 public selector = bytes4(keccak256("transfer(address,uint256)"));
// Dynamic byte arrays
bytes public dynamic = "hello";
bytes public empty;
// String vs bytes
string public text = "Hello"; // UTF-8 string
bytes public raw = bytes(text);
// Operations on fixed bytes
bytes32 data = keccak256(abi.encode("x"));
bytes1 first = data[0]; // access by index
uint256 length = data.length; // 32
// Concatenation
bytes memory combined = bytes.concat(bytes("a"), bytes("b"));枚举
枚举定义一组有限的命名值。它们存储为 uint8(最多 256 个成员)。默认值是第一个成员(索引 0)。使用强制转换在枚举和 uint 之间转换。type(EnumName).max 给出最高成员。
contract EnumExample {
enum Status { Pending, Active, Paused, Closed }
Status public status = Status.Pending;
function activate() public {
require(status == Status.Pending, "Not pending");
status = Status.Active;
}
function next() public {
// Cast to uint to do math
uint current = uint(status);
if (current < uint(type(Status).max)) {
status = Status(current + 1);
}
}
// Default value is the first member (Pending)
Status public defaultValue; // Pending
}类型转换与强制转换
只有在不会丢失数据时才会发生隐式转换(uint8 -> uint256)。对于窄化转换(uint256 -> uint8)或有符号/无符号转换,使用显式强制转换。将负 int 转换为 uint 会由于二进制补码而产生巨大数字。
// Implicit conversion (safe, widening)
uint8 small = 10;
uint256 big = small; // OK: uint8 fits in uint256
// Explicit conversion (narrowing, may lose data)
uint256 large = 300;
uint8 narrow = uint8(large); // truncates to 44 (300 % 256)
// Address conversion
address addr = 0xAb8483F64d9C6d1Ec37F8D2C4fA6bA5d5C5e5C5e;
address payable payableAddr = payable(addr);
// Bytes conversion
bytes32 b32 = bytes32(uint256(42));
uint256 num = uint256(b32);
bytes4 selector = bytes4(keccak256("foo()"));
// String <-> bytes
string memory s = "hi";
bytes memory bs = bytes(s);
string memory s2 = string(bs);
// int <-> uint (explicit, watch for negatives)
int256 signed = -5;
uint256 unsigned = uint256(signed); // huge number!引用类型
字符串
Solidity 中的字符串是 UTF-8 字节数组——length() 返回字节数,而不是字符数(emoji/多字节字符不同)。相等性必须通过 keccak256 哈希检查。string.concat(0.8.12+)比 abi.encodePacked 便宜。字符串 gas 开销大。
contract Strings {
string public greeting = "Hello, World!";
function concat(string memory a, string memory b)
public pure returns (string memory)
{
return string.concat(a, " ", b); // 0.8.12+
}
function length(string memory s) public pure returns (uint256) {
return bytes(s).length; // byte length, not char count
}
function equals(string memory a, string memory b)
public pure returns (bool)
{
return keccak256(bytes(a)) == keccak256(bytes(b));
}
// Storage string (state variable)
string public stored;
function set(string memory s) public { stored = s; }
}数组(固定与动态)
固定数组 [N] 有编译时大小;动态数组 [] 通过 push/pop 增长。内存数组必须用 'new Type[](size)' 创建。读/写存储数组消耗 gas;临时数据优先使用 memory。push() 在 0.6+ 中返回引用。
contract Arrays {
// Fixed-size array
uint256[5] public fixedArr = [1, 2, 3, 4, 5];
// Dynamic array
uint256[] public dynamicArr;
function pushPop() public {
dynamicArr.push(10); // append: [10]
dynamicArr.push(20); // [10, 20]
dynamicArr.pop(); // remove last: [10]
}
function getLength() public view returns (uint256) {
return dynamicArr.length;
}
function iterate() public view returns (uint256 sum) {
for (uint256 i = 0; i < dynamicArr.length; i++) {
sum += dynamicArr[i];
}
}
// Array of arrays (2D)
uint256[][] public matrix;
// Memory array (must have fixed length when created)
function memArray() public pure returns (uint256[] memory) {
uint256[] memory arr = new uint256[](3);
arr[0] = 1; arr[1] = 2; arr[2] = 3;
return arr;
}
}结构体
结构体将相关字段组合在一起。它们可以存储在 storage、memory 中,或作为函数参数传递。在文件或合约级别定义它们。使用命名字段 {field: value} 初始化更清晰;位置初始化更短。在旧版本中,映射值不能作为整体直接访问结构体。
contract Structs {
struct User {
address wallet;
uint256 balance;
bool active;
string name;
}
// Single struct (storage)
User public owner;
User[] public users; // array of structs
mapping(address => User) public userByAddr;
function createUser(string memory name) public {
User memory u = User({
wallet: msg.sender,
balance: 100,
active: true,
name: name
});
users.push(u);
}
// Shorter syntax
function createShort() public {
users.push(User(msg.sender, 50, true, "anon"));
}
// Update a field
function deactivate(uint256 index) public {
users[index].active = false; // direct storage write
}
}映射
映射是哈希表:keccak256(key) => value。它们没有长度,无法迭代。键可以是值类型(uint、address、bytes、enum);值可以是任何类型。嵌套映射允许多键查找。要迭代,需维护一个单独的键数组。delete 将键重置为其零值。
contract Mappings {
// keyType => valueType
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowance;
mapping(address => bool) public isRegistered;
function setBalance(uint256 amount) public {
balances[msg.sender] = amount;
}
function approve(address spender, uint256 amount) public {
allowance[msg.sender][spender] = amount;
}
function register() public {
isRegistered[msg.sender] = true;
}
// Iterable mapping pattern
address[] public userList;
mapping(address => bool) public exists;
function addUser(address u) public {
if (!exists[u]) {
userList.push(u);
exists[u] = true;
}
}
// CANNOT iterate or get length of a mapping directly
// CANNOT delete a mapping, only individual keys
function remove() public {
delete balances[msg.sender]; // sets to 0
}
}数据位置(storage / memory / calldata)
storage = 持久的链上状态(昂贵)。memory = 临时,在函数调用期间存在(便宜)。calldata = 只读,仅用于外部函数输入(最便宜)。对于只读取的外部函数参数,始终使用 calldata。默认位置:状态变量是 storage;函数局部复杂类型默认为 memory。
contract DataLocations {
string public stateVar; // storage (state)
function f(string calldata input) external {
// calldata: read-only, only for external function args
string memory local = input; // memory: temporary copy
stateVar = local; // writes to storage
// storage reference (only inside a function)
string storage ref = stateVar;
ref = "new value"; // modifies stateVar
}
// External params can be calldata (cheapest)
function g(string calldata s) external pure returns (bytes32) {
return keccak256(bytes(s));
}
// Public/internal params default to memory
function h(string memory s) public pure returns (uint256) {
return bytes(s).length;
}
// Assignment rules:
// storage -> storage: reference (both point to same slot)
// memory -> memory: reference (both point to same memory)
// storage <-> memory: copy
}嵌套结构
结构体可以包含数组、映射(仅在 storage 中)和其他结构体。通过 storage 引用访问嵌套结构体字段是 gas 高效的。注意深层嵌套——它增加存储槽使用和部署成本。结构体映射是最常见的链上数据库模式。
contract Nested {
struct Order {
uint256 id;
uint256 amount;
Item[] items;
}
struct Item {
string sku;
uint256 qty;
}
mapping(uint256 => Order) public orders;
Order[] public allOrders;
function addOrder(uint256 id, string memory sku, uint256 qty) public {
Order storage o = orders[id];
o.id = id;
o.amount += qty * 1 ether;
o.items.push(Item(sku, qty));
}
// Mapping of struct arrays
mapping(address => Item[]) public userItems;
// Struct in a struct
struct Profile {
User user;
uint256 score;
}
struct User {
address addr;
string name;
}
Profile public profile;
}状态变量
公开与私有状态变量
public 状态变量自动生成 getter 函数。private 限制为合约内部访问;internal 也允许子类。默认是 internal。关键:'private' 并不意味着秘密——所有区块链数据都是公开可读的。如果需要机密性,使用加密/哈希。
contract Visibility {
uint256 public publicVar = 1; // auto getter created
uint256 private privateVar = 2; // only this contract
uint256 internal internalVar = 3; // this + subclasses
// No 'external' for state variables
// internal is the default
uint256 defaultVar = 4; // internal
function readPrivate() public view returns (uint256) {
return privateVar; // readable from inside
}
// 'private' only means 'not via other contracts'.
// The data is still visible on-chain!
}
// public creates an automatic getter:
// function publicVar() external view returns (uint256) { return publicVar; }常量变量
constant 变量在编译时求值并内联到字节码中——它们在运行时不消耗 gas(无 SLOAD)。只有值类型(uint、address、bytes 等)可以是 constant。值必须是编译时表达式。对固定值如 MAX_SUPPLY、DECIMALS、地址使用 constant。
contract Constants {
// constant: evaluated at compile time, inlined into code
uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18;
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
uint8 public constant DECIMALS = 18;
// Must be value types, assigned a literal/expression
// No assignment in constructor
// Numeric separators (1_000_000) work since 0.8.0
function compute() public pure returns (uint256) {
return MAX_SUPPLY / 2; // inlined, no SLOAD
}
}不可变变量
immutable 变量在构造函数中设置一次,然后只读。它们存储在字节码中(不是 storage),因此读取比常规状态变量便宜。与 constant 不同,immutable 可以使用构造函数参数和运行时值。用于部署时设置的值,如 owner、代币名称或链特定配置。
contract Immutable {
// immutable: set once in constructor, then read-only
address public immutable owner;
uint256 public immutable creationBlock;
constructor(address _owner) {
owner = _owner;
creationBlock = block.number;
}
// Can be any value type
// Cheaper than regular state vars (no SLOAD slot, uses code)
// More flexible than constant (can use constructor args)
function check() public view returns (bool) {
return msg.sender == owner; // cheap read
}
}
// Use case: factory-deployed contracts with constructor args
contract Factory {
Child[] public children;
function create(address owner) public {
children.push(new Child(owner));
}
}
contract Child {
address public immutable owner;
constructor(address _owner) { owner = _owner; }
}全局变量(msg, tx, block)
msg 提供调用上下文:msg.sender(直接调用者)、msg.value(发送的 wei)、msg.data(原始 calldata)、msg.sig(函数选择器)。tx 给出交易信息:tx.origin(EOA,不推荐用于授权)。block 给出当前区块:block.timestamp、block.number、block.chainid、block.coinbase。永远不要使用 tx.origin 进行授权——使用 msg.sender。
contract Globals {
function msgVars() public view returns (
address sender,
bytes4 selector,
uint256 value,
bytes memory data
) {
sender = msg.sender; // caller address
selector = msg.sig; // function selector
value = msg.value; // wei sent with tx
data = msg.data; // full calldata
}
function txVars() public view returns (address origin, uint256 gasprice) {
origin = tx.origin; // EOA that started the tx
gasprice = tx.gasprice; // gas price in wei
}
function blockVars() public view returns (
uint256 number,
uint256 timestamp,
address miner,
uint256 chainId
) {
number = block.number; // current block height
timestamp = block.timestamp; // current block time (uint256 in 0.8+)
miner = block.coinbase; // block miner/validator
chainId = block.chainid; // chain ID
}
}区块与交易属性
blockhash() 只返回最近 256 个区块的哈希;更旧的返回 0。block.timestamp 由矿工设置,可能偏差约 15 秒——永远不要用于精确计时或随机数。block.basefee 来自 EIP-1559。gasleft() 替代已弃用的 msg.gas。chainid 对重放保护至关重要。
contract BlockProps {
function getBlockInfo() public view returns (
uint256 number,
uint256 timestamp,
uint256 gasLimit,
uint256 gasPrice,
uint256 baseFee
) {
number = block.number;
timestamp = block.timestamp;
gasLimit = block.gaslimit;
gasPrice = tx.gasprice;
baseFee = block.basefee; // EIP-1559 (London)
}
function getChainId() public view returns (uint256) {
return block.chainid;
}
function getPrevHash(uint256 n) public view returns (bytes32) {
require(n < 256, "out of range");
return blockhash(block.number - n); // last 256 blocks
}
function gasLeft() public view returns (uint256) {
return gasleft(); // remaining gas
}
}函数
函数语法
函数语法:function name(params) visibility mutability returns(...)。可见性(public/external/internal/private)是必需的。可变性(pure/view/payable)可选但推荐。支持多个返回值。命名返回值允许你直接赋值并跳过 return 关键字。
contract Func {
// function name(params) <visibility> <state-mutability> returns(...)
function add(uint256 a, uint256 b)
public
pure
returns (uint256)
{
return a + b;
}
// Multiple returns
function split(uint256 x)
public pure returns (uint256, uint256)
{
return (x / 2, x % 2);
}
// Named returns (no explicit return needed)
function named(uint256 x)
public pure returns (uint256 half, uint256 rest)
{
half = x / 2;
rest = x % 2;
// return statement optional
}
// Destructure returns
function caller() public pure returns (uint256) {
(uint256 h, uint256 r) = named(10);
return h + r;
}
}View 函数
view 函数可以读取状态但不能修改。通过 eth_call 外部调用时是免费的(无 gas 费用)。从修改状态的函数内部调用时消耗 gas。对只读取状态的函数使用 view——这表明意图并允许链下免费读取。
contract ViewFn {
uint256 public counter = 0;
// view: can READ state but not modify
function getCount() public view returns (uint256) {
return counter; // reading state
}
function doubleCount() public view returns (uint256) {
return counter * 2; // reading state
}
// Can call other view/pure functions
function combined() public view returns (uint256) {
return getCount() + 1;
}
// CANNOT:
// - write to state variables
// - emit events
// - call non-view functions
// - use selfdestruct
// - send Ether
// view functions can be called off-chain for free (eth_call)
}Pure 函数
pure 函数不能读取或写入状态——它们只对输入进行计算。它们是限制最多且链下调用最便宜的。对实用函数(数学、输入哈希)使用 pure。如果发现自己需要 msg.sender 或区块数据,改用 view。pure 只能调用其他 pure 函数。
contract PureFn {
// pure: cannot read OR write state
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
function square(uint256 x) public pure returns (uint256) {
return x * x;
}
// pure can only use:
// - function parameters
// - local memory variables
// - other pure functions
function combined(uint256 x) public pure returns (uint256) {
uint256 y = square(x); // calling another pure fn
return add(y, 1);
}
// CANNOT read: state variables, msg.sender, block.timestamp, etc.
// CANNOT call view functions
}Payable 函数
payable 函数可以通过 msg.value 接收 Ether。Ether 自动添加到合约余额——除非你想要按用户记账,否则不需要手动跟踪它。验证 msg.value 以确保确切或最低付款。始终退还多余的 Ether 以避免锁定在合约中。1 ether = 10^18 wei。
contract Payable {
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
// payable: function can receive Ether
function deposit() public payable {
// msg.value is the amount of wei sent
// Ether is automatically added to contract balance
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// Withdraw to owner
function withdraw() public {
require(msg.sender == owner, "not owner");
owner.transfer(address(this).balance);
}
// payable address (the contract itself can be paid)
receive() external payable {
// called when calldata is empty + value sent
}
}返回值与命名返回
函数可以通过元组返回多个值。命名返回值提高可读性,允许你直接赋值而无需 return 语句。显式 return 语句覆盖命名返回值。使用解构 (a, b) = f() 捕获多个返回值;使用 (, b) 或 (a,) 跳过不需要的值。
contract Returns {
// Single return
function one() public pure returns (uint256) {
return 42;
}
// Multiple returns
function two() public pure returns (uint256, bool) {
return (1, true);
}
// Named returns — assign directly
function named() public pure returns (uint256 a, uint256 b) {
a = 1;
b = 2;
// no return keyword needed
}
// Mixed: explicit return overrides
function mixed(bool x) public pure returns (uint256 a, uint256 b) {
a = 1;
if (x) return (10, 20); // explicit return
b = 2;
}
// Destructuring assignment
function caller() public pure returns (uint256) {
(uint256 x, uint256 y) = two();
return x + y;
}
// Skip values with commas
function skip() public pure returns (bool) {
(, bool y) = two();
return y;
}
}函数重载
Solidity 支持函数重载:多个同名但参数类型或数量不同的函数。编译器根据参数类型解析调用哪个。仅返回类型不能区分重载。重载对于接受 address 和 address payable,或不同数字范围很有用。
contract Overload {
// Same name, different parameter types/counts
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
function add(uint256 a, uint256 b, uint256 c) public pure returns (uint256) {
return a + b + c;
}
function add(string memory a, string memory b)
public pure returns (string memory)
{
return string.concat(a, b);
}
// Caller selects which via argument types
function test() public pure returns (uint256, uint256, string memory) {
return (add(1, 2), add(1, 2, 3), add("a", "b"));
}
// NOTE: return type alone does NOT distinguish overloads
// function f() returns (uint256) and f() returns (bool) — INVALID
}构造函数与特殊函数
构造函数
constructor 在合约部署时运行一次,用于初始化状态。它不能再次被调用。构造函数参数根据框架追加到部署字节码或通过 ABI 传递。对于继承,在继承列表(Owned(_owner))或修饰符样式(A() B())中传递父构造函数参数。
contract Owned {
address public owner;
// constructor runs once at deployment
constructor(address _owner) {
owner = _owner;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
// Inheritance: constructor args
contract Child is Owned {
constructor(address _owner) Owned(_owner) {}
}
// No constructor = default constructor (no args)
contract NoCtor {}
// Multiple inheritance: order matters
contract A { constructor() {} }
contract B { constructor() {} }
contract C is A, B {
constructor() A() B() {}
}Receive 函数
receive() 在没有 calldata 的调用发送 Ether 时被调用(例如 send/transfer 到合约)。它必须是 'external payable',无参数无返回值。只允许一个 receive 函数。如果不存在且没有 fallback,纯 Ether 转账会回滚(除了通过 selfdestruct 可以强制发送 Ether)。
contract Receiver {
event Received(address from, uint256 amount);
// receive: called on plain Ether transfers (no calldata)
// Must be 'external payable', no args, no returns
// Only ONE receive per contract
receive() external payable {
emit Received(msg.sender, msg.value);
}
// Triggered by: transfer(), send(), or empty calldata tx
// Use this to react to incoming Ether
function balance() public view returns (uint256) {
return address(this).balance;
}
}
// If no receive/fallback exists, the contract rejects plain Ether.
// Exception: contracts can be forced via selfdestruct (always).Fallback 函数
fallback() 在没有函数匹配选择器时调用,或当 receive 不存在且尝试纯 Ether 转账时调用。它可以是 payable。保持 fallback 逻辑最小化——通过 send()/transfer() 调用时只有 2300 gas(津贴),不足以覆盖存储写入或发出事件。
contract Fallback {
event Log(string message, address from, uint256 value, bytes data);
// fallback: called when no function matches the calldata
// OR when receive is absent and calldata is empty
fallback() external payable {
emit Log("fallback", msg.sender, msg.value, msg.data);
}
// With calldata (function not found)
// Without calldata + no receive => fallback runs
// Modern: separate receive and fallback
// receive() external payable { ... } // empty calldata + value
// fallback() external payable { ... } // unknown function
// Make fallback cheap — it can run out of gas on send()/transfer() (2300 stipend)
function safeFallback() external {
// logic here must be < 2300 gas if called via transfer/send
}
}Selfdestruct
selfdestruct 将所有剩余 Ether 发送到指定地址,并(在 Dencun 之前)删除合约代码。EIP-6780(2024 年 3 月)之后,它只在与部署合约同一笔交易中调用时才删除代码——否则只转移 Ether。永远不要依赖 selfdestruct 进行安全保护:强制 Ether 仍可到达,代码可能持续存在。
contract SelfDestruct {
address payable public owner;
constructor() { owner = payable(msg.sender); }
function destroy() public {
require(msg.sender == owner, "not owner");
// Sends all Ether to recipient, then deletes the contract
selfdestruct(payable(msg.sender));
}
}
// IMPORTANT WARNINGS:
// - After EIP-6780 (Dencun), selfdestruct no longer deletes code
// unless called in the same transaction that deployed the contract
// - It still sends all Ether to the recipient
// - The recipient could be a contract with no receive/fallback and
// it still gets the Ether (forced transfer)
// - Do NOT rely on selfdestruct for security; prefer a 'paused' flag函数选择器
每个函数都有一个 4 字节选择器 = keccak256(签名) 的前 4 字节。签名是函数名和参数类型(无空格),例如 'transfer(address,uint256)'。EVM 使用选择器路由调用。msg.sig 给出当前函数的选择器。abi.encodeWithSelector/Signature 为低级调用构建 calldata。
contract Selector {
// First 4 bytes of keccak256 of the function signature
// signature = "functionName(paramType1,paramType2,...)"
bytes4 public constant SELECTOR =
bytes4(keccak256("transfer(address,uint256)"));
function getSelector() public pure returns (bytes4) {
return bytes4(keccak256("transfer(address,uint256)")));
// = 0xa9059cbb
}
function thisSelector() public pure returns (bytes4) {
return msg.sig; // selector of the currently running function
}
// Used for low-level calls
function callTransfer(address token, address to, uint256 amount)
public returns (bool, bytes memory)
{
(bool ok, bytes memory data) = token.call(
abi.encodeWithSelector(0xa9059cbb, to, amount)
);
return (ok, data);
}
// abi.encodeWithSignature uses the string form
function altCall(address token, address to, uint256 amount)
public returns (bool, bytes memory)
{
return token.call(
abi.encodeWithSignature("transfer(address,uint256)", to, amount)
);
}
}修饰器
修饰器基础
修饰器用可重用的前置/后置条件包装函数。_; 占位符是函数体执行的位置。修饰器非常适合访问控制(onlyOwner)、验证(validInput)和重入保护。它们默认在函数体之前应用——使用 _; 控制函数体运行的位置。
contract ModBasics {
address public owner;
uint256 public count;
constructor() { owner = msg.sender; }
// Define a modifier
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_; // placeholder for the function body
}
// Apply it
function increment() public onlyOwner {
count += 1;
}
function reset() public onlyOwner {
count = 0;
}
// Anyone can read
function getCount() public view returns (uint256) {
return count;
}
}带参数的修饰器
修饰器可以接受参数,使其灵活。参数在调用点评估并在修饰器运行时绑定。常见模式:costs(price) 用于按调用付费,onlyRole(role) 用于 RBAC,rateLimit(window) 用于节流。_; 之前的逻辑是前置条件,之后是后置条件。
contract ModArgs {
modifier costs(uint256 price) {
require(msg.value >= price, "insufficient payment");
_;
// optional: refund excess after function runs
if (msg.value > price) {
payable(msg.sender).transfer(msg.value - price);
}
}
modifier onlyAddress(address allowed) {
require(msg.sender == allowed, "not allowed");
_;
}
function buyItem() public payable costs(1 ether) {
// msg.value must be >= 1 ether to enter
// excess refunded after body runs
}
function adminOnly() public onlyAddress(0xAbC...) {
// restricted to a hardcoded address
}
}多个修饰器
一个函数可以有多个修饰器。它们按顺序执行,像堆栈一样包装函数体:第一个修饰器的前置条件先运行,然后第二个,...,然后函数体,然后按相反顺序展开。顺序很重要——将最便宜/最可能失败的检查放在前面以节省 gas。
contract MultiMod {
address public owner;
bool public paused;
uint256 public value;
constructor() { owner = msg.sender; }
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
modifier whenNotPaused() {
require(!paused, "paused");
_;
}
modifier validValue(uint256 v) {
require(v > 0, "invalid value");
_;
}
// Multiple modifiers — executed in order
// Stack-like: A B body B' A'
function setValue(uint256 v)
public
onlyOwner // 1. check owner
whenNotPaused // 2. check not paused
validValue(v) // 3. validate input
{
value = v;
}
}修饰器顺序与 Gas
修饰器顺序影响 gas 和清晰度。将便宜的检查(布尔值、地址比较)放在前面,昂贵的检查(存储写入、外部调用)放在最后。还要将最可能失败的检查放在前面——这会短路并退还未使用的 gas。用注释记录预期的顺序。
contract ModOrder {
// Order modifiers from cheapest to most expensive
// OR most likely to fail first (to short-circuit)
modifier onlyOwner() { require(msg.sender == owner, "1"); _; }
modifier nonZero(uint256 x) { require(x != 0, "2"); _; }
modifier withinRange(uint256 x) {
require(x >= 1 && x <= 100, "3");
_;
}
modifier notPaused() { require(!paused, "4"); _; }
// Good order: cheap + likely-to-fail first
function good(uint256 x)
public
notPaused // 1. cheap bool check
onlyOwner // 2. address compare
nonZero(x) // 3. cheap input check
withinRange(x) // 4. more complex range check
{}
address public owner;
bool public paused;
constructor() { owner = msg.sender; }
}常见修饰器模式
常见修饰器模式:nonReentrant(在函数体前设置锁,之后取消设置)、whenNotPaused/whenPaused(断路器)、onlyRole(带 bytes32 角色的 RBAC)、rateLimit(使用时间戳的每用户节流)。重入保护是最重要的——将其应用于在状态更改后进行外部调用的任何函数。
contract Patterns {
// 1. Reentrancy guard
uint256 private _status = 1;
modifier nonReentrant() {
require(_status == 1, "reentrant");
_status = 2;
_;
_status = 1;
}
// 2. Pausable
bool public paused;
modifier whenNotPaused() {
require(!paused, "paused");
_;
}
modifier whenPaused() {
require(paused, "not paused");
_;
}
// 3. Role-based
mapping(bytes32 => bool) public hasRole;
modifier onlyRole(bytes32 role) {
require(hasRole[role], "missing role");
_;
}
// 4. Rate limit (basic)
mapping(address => uint256) public lastCall;
modifier rateLimit() {
require(block.timestamp >= lastCall[msg.sender] + 1 minutes, "rate");
lastCall[msg.sender] = block.timestamp;
_;
}
}事件
事件声明与触发
事件被记录到区块链并可被链下客户端(通过 eth_getLogs)读取。它们不能被其他合约读取。对最多 3 个参数使用 indexed 使其可过滤。事件的 gas 成本低于存储写入。始终为重要的状态更改发出事件,以便 dApp 和索引器可以反应。
contract Events {
// Declare event (outside functions, can be at file level)
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public returns (bool) {
require(balances[msg.sender] >= amount, "insufficient");
balances[msg.sender] -= amount;
balances[to] += amount;
// Emit the event
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public returns (bool) {
emit Approval(msg.sender, spender, amount);
return true;
}
}索引参数
indexed 参数成为链下客户端(dApp、The Graph、索引器)可以过滤的主题。每个事件最多 3 个索引参数(第 4 个主题保留给事件签名哈希)。索引值类型(uint、address)直接存储;索引引用类型(string、bytes、数组)被 keccak256 哈希且不可恢复。
contract Indexed {
// Up to 3 indexed params (in non-anonymous events)
// Indexed params become 'topics' that clients can filter on
event OrderPlaced(
address indexed buyer, // topic 1
address indexed token, // topic 2
uint256 indexed orderId, // topic 3
uint256 amount, // data (not filterable)
uint256 price // data
);
function placeOrder(address token, uint256 amount, uint256 price)
public returns (uint256 id)
{
id = uint256(keccak256(abi.encodePacked(msg.sender, block.number)));
emit OrderPlaced(msg.sender, token, id, amount, price);
}
// Clients can filter:
// eth_getLogs({topics: [sig, buyerAddr, tokenAddr, orderId]})
//
// Indexed arrays/strings/bytes are hashed (keccak256) —
// you can't recover the original value, only compare hashes.
}匿名事件
匿名事件跳过 topic[0] 中的事件签名哈希,释放一个槽位——因此你可以获得 4 个索引参数而不是 3 个。权衡:客户端无法按事件名称(签名)过滤,因此在合约中区分多个匿名事件更困难。很少有用;gas 节省微乎其微。
contract Anonymous {
// anonymous: skips storing the event signature as topic[0]
// So you can have up to 4 indexed params instead of 3
event Log(
address indexed from,
address indexed to,
uint256 indexed amount,
uint256 indexed timestamp, // 4th indexed (only with anonymous)
bytes data
) anonymous;
function log() public {
emit Log(msg.sender, address(0), 100, block.timestamp, "");
}
// Trade-offs:
// + 1 more indexed param allowed
// - Clients cannot filter by event signature
// - Harder to distinguish multiple anonymous events
// - Slightly cheaper gas
// Rarely used; prefer named events for clarity.
}事件最佳实践
最佳实践:在状态更改后发出事件(使事件反映最终状态),包含帮助索引器避免额外 RPC 调用的字段(如 newBalance),避免在紧密循环中发出事件(改为批量处理),并索引最常过滤的字段(地址、ID、状态码)。事件是合约与链下通信的主要渠道。
contract EventBest {
// 1. Name events with past tense or noun: Transfer, Deposit, OrderPlaced
event Transfer(address indexed from, address indexed to, uint256 value);
// 2. Emit AFTER state changes succeed
mapping(address => uint256) public balances;
function transfer(address to, uint256 amt) public {
balances[msg.sender] -= amt; // state first
balances[to] += amt;
emit Transfer(msg.sender, to, amt); // event after
}
// 3. Include enough data to reconstruct state
event Deposit(
address indexed account,
uint256 amount,
uint256 newBalance, // helps indexers avoid extra reads
uint256 timestamp
);
// 4. Don't emit in a loop — batch instead
event BatchTransfer(address[] to, uint256[] amounts);
function batch(address[] memory to, uint256[] memory amounts) public {
emit BatchTransfer(to, amounts); // one event, not N
}
// 5. Index the fields users filter on (addresses, IDs)
}事件 vs 日志 vs 存储
存储在状态 trie 中——可被其他合约和 view 函数读取,但昂贵(每个 SSTORE 约 20k gas)。事件/日志在收据 trie 中——不能被合约读取,但便宜(约 1-2k gas)且可通过 eth_getLogs 链下访问。使用存储进行链上决策,使用事件向 dApp 和索引器传达状态更改。
contract Compare {
// Storage: persistent, readable by contracts, expensive
uint256 public storedValue; // SSTORE costs ~20k gas
// Events (logs): persistent-ish, NOT readable by contracts, cheap
event ValueChanged(uint256 oldValue, uint256 newValue);
function set(uint256 newValue) public {
uint256 old = storedValue;
storedValue = newValue; // SSTORE (~20k gas)
emit ValueChanged(old, newValue); // LOG (~1-2k gas)
}
// Logs live in the block's receipt trie, NOT the state trie.
// They are NOT accessible from other contracts.
// function readLog() public view returns (...) {
// CANNOT access emitted events
// }
// Off-chain tools (dApps, The Graph) read logs via eth_getLogs.
// Use storage for on-chain logic; events for off-chain observers.
}错误处理
Require
require(cond, msg) 在 cond 为 false 时回滚交易并退还未使用的 gas。它是最常见的验证原语——用于输入检查、访问控制和前置条件。消息字符串消耗 gas(存储在部署字节码中)。为了节省 gas,使用自定义错误(revert ErrorName())代替字符串消息。
contract Require {
mapping(address => uint256) public balances;
function deposit() public payable {
require(msg.value > 0, "must send ether");
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
// require(condition, message)
// Reverts with the message if condition is false
// Refunds all unused gas to the caller (post-0.8.0 with custom errors, also refunds)
require(balances[msg.sender] >= amount, "insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
// Pre-0.8.0: require with no message was cheaper
// 0.8.4+: prefer custom errors (cheaper, see other item)
function cheap(uint256 x) public pure {
if (x == 0) revert ZeroValue(); // custom error
}
error ZeroValue();
}Revert
revert(msg) 显式回滚交易并带原因字符串。当你需要分支逻辑时在 if 语句内使用 revert()(require 只是语法糖)。无消息的 revert() 比 revert("msg") 便宜。自定义错误(revert ErrorName(args))最便宜且最具描述性——在新代码中优先使用。
contract Revert {
function explicit(uint256 x) public pure {
if (x == 0) {
revert("zero not allowed"); // explicit revert with message
}
if (x > 100) {
revert("too big"); // can place anywhere
}
}
// revert() with no message (cheapest string revert)
function silent(uint256 x) public pure {
if (x == 0) revert(); // no reason
}
// Custom error (0.8.4+) — recommended
error TooBig(uint256 given, uint256 max);
function custom(uint256 x) public pure {
if (x > 100) revert TooBig(x, 100);
}
// Inside an internal function, revert propagates up
function caller(uint256 x) public pure {
inner(x); // reverts here if x is bad
}
function inner(uint256 x) internal pure {
if (x == 0) revert("zero");
}
}Assert
assert(cond) 用于代码正确时永远不应为 false 的不变量——失败的 assert 表示存在 bug。0.8.0 之前,assert 消耗所有 gas;自 0.8.0 起它的行为类似 revert。使用 require 进行输入验证和预期错误条件;仅对表示 bug 的不可能状态使用 assert。现代自动溢出检查使用 revert,不是 assert。
contract Assert {
function check(uint256 x) public pure returns (uint256) {
uint256 result = x + 1;
// assert: for invariants that should NEVER be false
// If it fails, something is critically broken.
assert(result > x); // overflow check (auto in 0.8+)
return result;
}
// Pre-0.8.0: assert was used for overflow checks (used all gas)
// Post-0.8.0: arithmetic auto-checks use revert, not assert
//
// Modern guidance:
// - Use require/revert for input validation & expected failures
// - Use assert ONLY for invariants that indicate a code bug
// (something that should be impossible if the code is correct)
//
// Example: a state machine that should never reach an invalid state
enum State { Created, Paid, Shipped }
State public state;
function ship() public {
require(state == State.Paid, "not paid");
state = State.Shipped;
assert(state == State.Shipped); // invariant
}
}自定义错误
自定义错误(0.8.4+)是推荐的回滚方式。它们编码为 4 字节选择器加上 ABI 编码的参数——比字符串消息(在字节码中存储为完整 UTF-8)便宜得多。客户端解码选择器以知道发生了哪个错误并读取结构化参数。在文件或合约级别定义错误,然后 'revert ErrorName(args);'。
contract CustomError {
// Define errors (outside functions, can be at file level)
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
error TransferFailed(address from, address to);
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
if (balances[msg.sender] < amount) {
// Cheaper than require(..., "insufficient balance")
revert InsufficientBalance(balances[msg.sender], amount);
}
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert TransferFailed(address(this), msg.sender);
}
function adminOnly() public {
if (msg.sender != address(0x123)) {
revert Unauthorized(msg.sender);
}
}
// Why cheaper? The error selector (4 bytes) replaces a long string,
// saving bytecode size and gas per call.
// ABI decodes arguments so clients get structured data.
}Try / Catch
try/catch 仅处理外部调用的错误——它不能捕获内部函数调用的回滚。'returns' 子句捕获成功值。catch Error() 捕获带字符串消息的 revert/require;catch Panic() 捕获 assert/溢出恐慌;catch (bytes) 是自定义错误和未知回滚的后备。用于优雅地处理外部合约失败。
contract TryCatch {
interface IToken {
function transfer(address to, uint256 amount) external returns (bool);
}
IToken public token;
constructor(address _token) { token = IToken(_token); }
function safeTransfer(address to, uint256 amount)
public returns (bool success, bool tokenOk)
{
try token.transfer(to, amount) returns (bool ok) {
// Called if no revert; 'ok' captures the return value
return (ok, true);
} catch Error(string memory reason) {
// revert("reason") or require(_, "reason")
// log reason
return (false, false);
} catch Panic(uint256 code) {
// assert failures (code 0x01) or overflow (0x11)
return (false, false);
} catch (bytes memory lowLevelData) {
// Any other revert (including custom errors)
return (false, false);
}
}
// try/catch ONLY works with EXTERNAL calls (or this.f()).
// It does NOT catch errors in internal calls.
// The called function MUST return something (not void).
}错误消息与模式
现代错误处理使用自定义错误进行结构化、便宜的回滚。旧的字符串消息在 0.8.4 之前的代码中仍然常见。始终包含可操作的上下文:哪个值错误,预期是什么。避免在字符串消息中放入动态地址/数字——它们会使字节码膨胀且不可解析。如果必须使用字符串,使用带前缀的消息('INVALID:')以帮助日志索引。
contract Errors {
// 1. Custom errors (best for new code)
error InvalidAmount(uint256 given);
error NotOwner(address caller, address owner);
// 2. String errors (older style)
function old(uint256 x) public pure {
require(x > 0, "INVALID: must be > 0"); // prefix helps indexing
require(x < 100, "INVALID: must be < 100");
}
// 3. Custom error with structured data
function newStyle(uint256 x) public pure {
if (x == 0) revert InvalidAmount(x);
}
// 4. Error with multiple fields
address public owner;
constructor() { owner = msg.sender; }
function guarded() public view {
if (msg.sender != owner) {
revert NotOwner(msg.sender, owner);
}
}
// 5. Don't put dynamic data in string errors (expensive & unparseable)
// BAD: require(msg.sender == owner, "not owner: caller 0x123...");
// GOOD: revert NotOwner(msg.sender, owner);
}继承
使用 'is' 继承
继承使用 'is' 关键字。子合约继承父合约的所有状态变量、函数和修饰器。父构造函数参数在继承列表(Animal("dog"))中或通过构造函数修饰符样式传递。子合约可以覆盖父合约中标记为 virtual 的函数。Solidity 支持多重继承(通过 C3 线性化)。
contract Animal {
string public species;
constructor(string memory _species) {
species = _species;
}
function speak() public virtual returns (string memory) {
return "...";
}
}
contract Dog is Animal {
constructor() Animal("dog") {}
// Override parent function
function speak() public override returns (string memory) {
return "Woof";
}
}
// Multiple inheritance
contract Puppy is Dog {
function speak() public override returns (string memory) {
return "Yip";
}
}Virtual 与 Override
virtual 标记函数为可覆盖;override 标记函数为覆盖其父函数。默认情况下函数是非虚的(更安全)。从共享同一基类的多个父类覆盖时,列出它们:override(A, B)。要允许进一步覆盖,使用 virtual override。状态变量不能被覆盖(只能在 memory 中遮蔽)。
contract Base {
function f() public pure virtual returns (string memory) {
return "base";
}
function g() public pure virtual returns (string memory) {
return "base-g";
}
}
contract Middle is Base {
// Override a single parent
function f() public pure override returns (string memory) {
return "middle";
}
}
contract Multi is Base {
// Override (also virtual so children can override again)
function g() public pure virtual override returns (string memory) {
return "multi";
}
}
contract Combined is Middle, Multi {
// Override multiple parents (explicit list)
function f() public pure override(Middle, Base) returns (string memory) {
return "combined";
}
function g() public pure override(Multi, Base) returns (string memory) {
return "combined-g";
}
}Super 关键字
super.method() 调用父类的方法版本,允许你扩展而不是替换行为。单继承时很直观。多重继承时,super 遵循 C3 线性化——它可能调用兄弟合约,而不是词法父类。在菱形继承中使用 super 时,始终检查线性化顺序(最派生的在最后)。
contract A {
function label() public pure virtual returns (string memory) {
return "A";
}
}
contract B is A {
function label() public pure virtual override returns (string memory) {
return string.concat("B>", super.label()); // calls A.label()
}
}
contract C is B {
function label() public pure override returns (string memory) {
return string.concat("C>", super.label()); // calls B.label()
}
}
// super refers to the NEXT contract in the linearization,
// not necessarily the lexical parent. With multiple inheritance,
// super.label() may call a sibling.
contract P1 { function f() public pure virtual returns (uint) { return 1; } }
contract P2 { function f() public pure virtual returns (uint) { return 2; } }
contract Child is P1, P2 {
function f() public pure override(P1, P2) returns (uint) {
return super.f(); // calls P2.f() (last in linearization)
}
}多重继承
Solidity 通过 'is' 关键字支持多重继承。从最基类到最派生类列出父类:'is Ownable, Pausable'。C3 线性化算法产生确定性顺序,解决菱形问题。从多个父类覆盖函数时使用 'override(A, B)'。构造函数按线性化顺序运行(父类先)。
contract Ownable {
address public owner;
constructor() { owner = msg.sender; }
modifier onlyOwner() { require(msg.sender == owner); _; }
}
contract Pausable {
bool public paused;
modifier whenNotPaused() { require(!paused); _; }
}
// Multiple inheritance: list parents from most base to most derived
contract Token is Ownable, Pausable {
mapping(address => uint256) public balances;
function mint(address to, uint256 amount)
public
onlyOwner // from Ownable
whenNotPaused // from Pausable
{
balances[to] += amount;
}
}
// C3 Linearization order (for 'Token'):
// Token -> Pausable -> Ownable -> (object)
// Determined by the order in 'is' list (left to right, base first)
//
// Diamond problem is resolved by C3 — there is a single
// deterministic order, no ambiguity. But you must understand it
// when using super.构造函数继承
父构造函数参数可以在继承列表(A(42))中传递固定值,或在子构造函数中通过修饰符语法(A(_a))传递运行时值。对于多个父类,按线性化顺序(最基类先)传递它们。如果父类有无参构造函数,可以省略。构造函数在部署时运行一次,按线性化顺序。
contract A {
uint256 public a;
constructor(uint256 _a) { a = _a; }
}
// Style 1: pass args in inheritance list
contract B1 is A(42) {
// 'a' is set to 42 at deployment
}
// Style 2: pass via modifier-style in child constructor
contract B2 is A {
constructor(uint256 _b) A(_b * 2) {
// 'a' is set to _b * 2
}
}
// Style 3: child has its own constructor + parent args
contract B3 is A {
uint256 public b;
constructor(uint256 _a, uint256 _b) A(_a) {
b = _b;
}
}
// With multiple parents, order = linearization order
contract C1 { constructor(uint) {} }
contract C2 { constructor(uint) {} }
contract C3 is C1, C2 {
constructor(uint x) C1(x) C2(x+1) {}
}抽象合约与接口
抽象合约
抽象合约至少有一个没有函数体的函数(用 virtual 声明,无实现)。它不能直接部署——子合约必须实现所有抽象函数。当你想要共享状态 + 修饰器 + 部分实现时使用抽象合约。在 0.6.0 之前,如果任何函数缺少函数体,'abstract' 关键字是必需的。
abstract contract Ownable {
address public owner;
constructor() { owner = msg.sender; }
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
// abstract function — no body, must be implemented by children
function sensitiveAction() public virtual;
// Can have implemented functions too
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
contract Vault is Ownable {
function sensitiveAction() public override onlyOwner {
// implementation here
}
}接口
接口就像纯抽象合约:无状态、无构造函数、无已实现函数,所有函数都是 external。它们为与未知实现的交互定义合约的 ABI。使用接口调用其他合约而不继承逻辑。用 'I' 前缀命名(IERC20、IUniswapV2Pair)。接口中的函数不能有除 external view/payable 之外的修饰符。
interface IERC20 {
// Functions only — no implementation, no state, no constructor
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender)
external view returns (uint256);
function transferFrom(address from, address to, uint256 amount)
external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract MyToken is IERC20 {
// Must implement ALL functions declared in the interface
mapping(address => uint256) private _balances;
uint256 private _totalSupply;
function totalSupply() external view returns (uint256) {
return _totalSupply;
}
// ... implement the rest
}Virtual vs Abstract
abstract = 至少一个函数没有函数体;合约不能部署。virtual = 有函数体但子类可以覆盖的函数。函数可以同时是两者(virtual + 无函数体)。接口隐式地完全抽象——每个函数都是 virtual、无函数体且 external。对共享逻辑使用 abstract,对纯 ABI 定义使用接口。
// ABSTRACT contract: has at least one unimplemented function
// Must declare 'abstract'; cannot be deployed directly
abstract contract Counter {
uint256 public count;
function increment() public virtual; // no body -> abstract
function reset() public {
count = 0;
}
}
// VIRTUAL: function HAS a body, but CAN be overridden
contract Base {
function f() public pure virtual returns (uint) {
return 1;
}
}
contract Child is Base {
function f() public pure override returns (uint) {
return 2;
}
}
// A function can be both virtual and abstract (no body)
abstract contract A {
function f() public pure virtual; // virtual + no body
}
// Interfaces are implicitly abstract — all functions virtual+abstract
interface I {
function f() external; // implicit virtual, no body
}接口最佳实践
接口最佳实践:用 'I' 前缀命名,保持最小化(只有调用者需要的),所有函数 external,无状态/构造函数/实现,允许事件。使用接口来:(1)调用你没有源代码的其他合约,(2)定义标准(IERC20、IERC721),(3)通过不继承实现来减少字节码。实现接口迫使你的合约满足标准。
// 1. Name with 'I' prefix
interface IERC721 {
function ownerOf(uint256 tokenId) external view returns (address);
function transferFrom(address from, address to, uint256 tokenId) external;
}
// 2. Keep minimal — only what callers need
interface IPriceFeed {
function latestAnswer() external view returns (int256);
function decimals() external view returns (uint8);
}
// 3. All functions external (required)
// 4. No state variables, no constructors, no implemented functions
// 5. Events ARE allowed in interfaces
interface IEvents {
event SomethingHappened(uint256 indexed id);
}
// 6. Use to call contracts you don't have source for
contract Consumer {
IPriceFeed public feed;
constructor(address _feed) { feed = IPriceFeed(_feed); }
function price() public view returns (int256) {
return feed.latestAnswer();
}
}
// 7. Implement to satisfy a standard (ERC20, ERC721)
contract MyToken is IERC20 { /* ... */ }IERC20 示例(接口)
IERC20 定义了 ERC-20 同质化代币标准。该接口声明了 6 个必需函数(totalSupply、balanceOf、transfer、approve、allowance、transferFrom)加上可选元数据(name、symbol、decimals)和 2 个事件。任何实现此接口的合约都可以被视为 ERC-20 代币。使用接口与任何 ERC-20 交互而无需知道其内部实现。
// Canonical ERC-20 interface
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender)
external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount)
external returns (bool);
}
// Minimal interaction using the interface
contract Wallet {
IERC20 public token;
constructor(address _token) { token = IERC20(_token); }
function balance() public view returns (uint256) {
return token.balanceOf(address(this));
}
function send(address to, uint256 amount) public returns (bool) {
return token.transfer(to, amount);
}
}库
库基础
库类似于合约但不能有状态、继承或被继承。它们部署一次并通过 DELEGATECALL(external)或内联(internal)重用。internal 库函数内联到调用合约中——无需单独部署。使用库对可重用函数(数学、验证、格式化)进行分组。'using SafeMath for uint256' 指令允许你以 a.b() 方式调用方法。
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "overflow");
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "underflow");
return a - b;
}
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
uint256 c = a * b;
require(c / a == b, "overflow");
return c;
}
// Libraries CANNOT have state variables
// Libraries CANNOT inherit or be inherited
// All functions must be internal or external
// No constructor, no fallback, no receive
}
contract UsingSafeMath {
using SafeMath for uint256;
function calc(uint256 a, uint256 b) public pure returns (uint256) {
return a.add(b).mul(2); // a.add(b) calls SafeMath.add(a, b)
}
}Using For
'using Library for Type' 将库的函数附加到该类型,因此你可以调用 value.method() 而不是 Library.method(value)。库函数的第一个参数必须匹配该类型。你也可以在文件级别(0.8.13+)使用 'using {fn1, fn2} for Type;' 指令。这纯粹是语法糖——编译器将其重写为直接调用。
library Strings {
function toString(uint256 value) internal pure returns (string memory) {
// convert uint to decimal string
if (value == 0) return "0";
uint256 temp = value;
uint256 digits;
while (temp != 0) { digits++; temp /= 10; }
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + value % 10));
value /= 10;
}
return string(buffer);
}
}
contract UsingExample {
// Attach library functions to a type
using Strings for uint256;
function describe(uint256 x) public pure returns (string memory) {
// x.toString() calls Strings.toString(x)
return string.concat("Value: ", x.toString());
}
// Attach to a specific type globally (Solidity 0.8.13+)
// using {toString} for uint256; // file-level
// using SafeMath for uint256;
// using SafeMath for *; // not allowed
}内部库
内部库函数在编译时内联到调用合约的字节码中——没有运行时 DELEGATECALL、没有库部署、没有调用开销。这使它们调用基本上免费(只是操作本身的 gas)。对 pure/view 辅助函数(数学、字符串转换)使用内部库。调用者的字节码大小增长,但调用 gas 低于外部库。
library Math {
// internal functions are INLINED into the caller's bytecode
// No separate deployment needed — saves gas on each call
function max(uint256 a, uint256 b) internal pure returns (uint256) {
return a > b ? a : b;
}
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
function average(uint256 a, uint256 b)
internal pure returns (uint256)
{
// (a + b) / 2 can overflow; this avoids it
return (a & b) + ((a ^ b) / 2);
}
}
contract Calc {
using Math for uint256;
function doMath(uint256 x, uint256 y)
public pure returns (uint256, uint256, uint256)
{
return (x.max(y), x.min(y), x.average(y));
}
}
// Internal libraries are like helper functions that don't add
// runtime call overhead. Good for: math, validation, conversions.外部(已部署)库
外部库函数作为单独的合约部署并通过 DELEGATECALL 调用——它们在调用者的上下文(storage、msg.sender)中运行。这对于许多合约使用的共享实用代码很有用(节省总部署 gas)。权衡:每次调用的 DELEGATECALL 开销(约 1400 gas)且库必须先部署并在编译时链接其地址。大多数库使用内部函数;外部很少见。
library BigMath {
// external functions => library is DEPLOYED as a separate contract
// Calls use DELEGATECALL — runs in caller's context (storage/msg)
function complexOperation(uint256[] storage arr)
external view returns (uint256 sum)
{
for (uint256 i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
}
contract UsesExternal {
uint256[] public data;
using BigMath for uint256[];
function total() public view returns (uint256) {
return data.complexOperation(); // DELEGATECALL to BigMath
}
}
// External library trade-offs:
// + Shared bytecode: many contracts use one deployed library (saves deployment gas)
// + Caller bytecode stays small
// - DELEGATECALL overhead per call (~1400 gas)
// - Library must be deployed first and its address linked at compile time
//
// Most libraries are internal; external is rare (e.g., Create2BeaconProxy).SafeMath 示例(0.8.0 之前)
SafeMath 是 0.8.0 之前规范的溢出检查库。自 0.8.0 起,算术在溢出时自动回滚,使 SafeMath 对基本数学不再必要。它仍然有用:(1)阅读遗留代码,(2)显式包装 unchecked 操作,(3)作为教学示例的 'require 后置条件' 模式。OpenZeppelin 版本仍被广泛引用。
// Before 0.8.0, arithmetic did NOT auto-check overflow.
// SafeMath was essential. Now it's mostly historical,
// but the pattern is instructive and still useful for
// explicit intent.
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return sub(a, b, "SafeMath: subtraction overflow");
}
function sub(uint256 a, uint256 b, string memory errorMessage)
internal pure returns (uint256)
{
require(b <= a, errorMessage);
return a - b;
}
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
return div(a, b, "SafeMath: division by zero");
}
function div(uint256 a, uint256 b, string memory errorMessage)
internal pure returns (uint256)
{
require(b > 0, errorMessage);
return a / b;
}
}Ether 转账
Payable 与 msg.value
payable 函数通过 msg.value 接收 Ether(以 wei 为单位)。Ether 自动添加到合约余额——除非你想要按用户记账,否则不需要手动跟踪它。验证 msg.value 以确保确切或最低付款。始终退还多余的 Ether 以避免锁定在合约中。1 ether = 10^18 wei。
contract PayableContract {
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
// payable = function accepts Ether
function deposit() public payable {
// msg.value = wei sent with this call
// Ether auto-added to address(this).balance
// No need to update a balance variable here
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// Specify required payment
function buyItem() public payable {
require(msg.value == 1 ether, "must pay 1 ether");
// process purchase
}
// Send excess back
function refundExcess(uint256 price) public payable {
uint256 excess = msg.value - price;
if (excess > 0) {
(bool ok, ) = msg.sender.call{value: excess}("");
require(ok, "refund failed");
}
}
}Transfer
address.transfer(amount) 发送 Ether 并在失败时回滚。它转发固定的 2300 gas 津贴,历史上防止了重入但现在被认为脆弱(gas 成本在 2019 年伊斯坦布尔升级中改变)。现代指导:仅在发送到受信任地址(如 owner)时使用 transfer。对于任意收件人,使用 .call{value:} 和显式重入保护。
contract TransferExample {
address payable public recipient = payable(0xAbC...);
function sendViaTransfer() public payable {
// transfer: throws on failure, forwards 2300 gas stipend
// Simple and safe against reentrancy (limited gas)
recipient.transfer(1 ether);
}
function sendFromContract() public {
// address(this).balance = current contract balance
recipient.transfer(address(this).balance);
}
// Characteristics:
// - Reverts if recipient's receive/fallback uses > 2300 gas
// - 2300 gas stipend: enough for an event log, not a storage write
// - Safer than send (which returns false on failure)
// - But BEWARE: post-Istanbul (2019), some operations cost more,
// breaking contracts that relied on 2300 gas. Prefer .call{}.
}
// transfer is DEPRECATED for pushing to arbitrary addresses.
// Use .call{value:} and handle the return value.Send
address.send(amount) 返回布尔值(成功为 true,失败为 false)——它不会自动回滚。你必须检查返回值并在失败时回滚。与 transfer 一样,它只转发 2300 gas。send 容易出错(开发者忘记检查返回值),在现代代码中很少使用。新合约优先使用 .call{value:}。
contract SendExample {
address payable public recipient = payable(0xAbC...);
function sendViaSend() public payable returns (bool) {
// send: returns false on failure (does NOT revert)
// Also forwards 2300 gas stipend like transfer
bool success = recipient.send(1 ether);
require(success, "send failed"); // you MUST check the return value
return success;
}
// Common bug: forgetting to check the return value
function buggy() public payable {
recipient.send(1 ether); // BUG: silently fails
// state already updated => funds lost
}
// Correct: check + revert
function correct() public payable {
bool ok = recipient.send(1 ether);
require(ok, "send failed");
}
}
// send is rarely used in modern code.
// Prefer .call{value:} which gives more control + return data.Call(推荐)
.call{value: x}(...) 是自 0.8.0 起推荐的发送 Ether 方式。它转发所有可用 gas(受 63/64 规则约束),因此收件人可以执行复杂逻辑。它返回 (bool, bytes)——你必须检查布尔值并在失败时回滚。因为它转发所有 gas,它容易受到重入攻击——始终与非重入修饰器配对或遵循 checks-effects-interactions。对任意收件人使用 call;transfer 仅用于受信任地址。
contract CallExample {
address payable public recipient = payable(0xAbC...);
function sendViaCall() public payable returns (bool) {
// .call returns (bool success, bytes memory data)
// Forwards ALL gas (63/64 rule), so recipient can do more
(bool success, ) = recipient.call{value: 1 ether}("");
require(success, "call failed");
return success;
}
// With calldata (call a function while sending Ether)
function callWithFunction(address token, address to, uint256 amount)
public returns (bool, bytes memory)
{
(bool ok, bytes memory data) = token.call{value: 0}(
abi.encodeWithSignature("transfer(address,uint256)", to, amount)
);
return (ok, data);
}
// Forward gas explicitly
function withGas() public returns (bool) {
(bool ok, ) = recipient.call{gas: 100000, value: 0}("");
return ok;
}
// IMPORTANT: call forwards all gas, so a malicious recipient
// can re-enter your contract. Always use a reentrancy guard.
}余额检查
address.balance 返回任何地址的 wei 余额(包括合约本身的 address(this))。它是一个 view,链下调用免费。警告:任何人都可以通过 selfdestruct(强制 Ether)膨胀余额,因此永远不要依赖确切的余额相等进行逻辑——使用 >= 或维护自己的记账变量。始终交叉检查存款总和等于合约余额作为不变量。
contract BalanceCheck {
mapping(address => uint256) public deposits;
function deposit() public payable {
deposits[msg.sender] += msg.value;
}
function contractBalance() public view returns (uint256) {
// Balance of THIS contract
return address(this).balance;
}
function userBalance(address user) public view returns (uint256) {
// Balance of any address (in wei)
return user.balance;
}
function myBalance() public view returns (uint256) {
return msg.sender.balance;
}
// Verify accounting invariant
function verify() public view returns (bool) {
// Sum of deposits should equal contract balance
// (in a real contract, this would be expensive to compute)
return address(this).balance >= deposits[msg.sender];
}
// CAUTION: address.balance can be manipulated by an attacker
// who forces Ether via selfdestruct. Never use balance == X
// as a strict equality check for logic — use >= or tracking vars.
function dangerous() public view returns (bool) {
// BAD: an attacker can selfdestruct-ETH into the contract
// and break this exact check
return address(this).balance == 1 ether;
}
}Gas 优化
Storage vs Memory
存储读取(SLOAD)每次访问冷约 2100 gas / 热约 100 gas。内存读取(MLOAD)只需 3 gas。当函数多次读取状态变量(特别是在循环中)时,首先将其缓存到内存变量中。第一次 SLOAD 昂贵;后续内存读取几乎免费。这是最简单且最有影响力的优化之一。
contract StorageVsMemory {
uint256 public stateVar; // storage slot 0
function expensive(uint256 x) public view returns (uint256) {
// Each access to stateVar = SLOAD (~2100 gas cold, 100 warm)
uint256 sum = 0;
for (uint256 i = 0; i < x; i++) {
sum += stateVar; // SLOAD every iteration
}
return sum;
}
function cheap(uint256 x) public view returns (uint256) {
// Cache to memory once: MLOAD costs 3 gas
uint256 cached = stateVar; // 1 SLOAD
uint256 sum = 0;
for (uint256 i = 0; i < x; i++) {
sum += cached; // MLOAD (cheap)
}
return sum;
}
// Rules of thumb:
// - storage: persistent, ~2100 gas per cold SLOAD, ~5000-20000 per SSTORE
// - memory: temporary, ~3 gas per access, free to create
// - calldata: read-only, free (passed in)
// Always cache state variables used multiple times in a function.
}变量打包
Solidity 将连续的状态变量打包到 32 字节存储槽中。为了节省 gas,将小类型(uint8、uint64、address)组合在一起以共享槽。重新排序结构体字段使小字段紧密打包。每个节省的槽 = 首次访问约 20k gas。注意:映射和动态数组条目总是开始新槽,不能打包。
contract Packing {
// BAD: each takes a full 32-byte slot
// 3 slots = 60k gas for cold SLOADs
struct Bad {
uint64 time;
address from; // 20 bytes
uint64 amount;
}
// GOOD: pack small types together into 32 bytes
// 1 slot = 20k gas (one SLOAD)
struct Good {
uint64 time;
uint64 amount;
address from; // 20 bytes => total 8+8+20 = 36... still 2 slots
}
// Pack carefully: declare small types together
struct Packed {
uint32 a; // 4
uint32 b; // 4
uint32 c; // 4
uint32 d; // 4 => 16 bytes => can fit address (20)? No, 16+20=36 > 32
address e; // => 2 slots
}
struct Tight {
uint128 a; // 16
uint128 b; // 16 => 1 slot (32 bytes)
}
// Order matters: variables are packed in declaration order.
// A variable that doesn't fit starts a new slot.
// Mapping/dynamic-array always start a new slot.Unchecked 块
unchecked { ... } 禁用块内的溢出/下溢检查,节省 gas(每个操作约 50-80)。当你已经验证了边界时可以安全使用(例如,在 require(a >= b) 之后,a - b 不会下溢)。经典的安全用例是循环计数器(for 循环中的 i++ 永远不会溢出 2^256)。不要盲目使用 unchecked——静默溢出是安全风险。
contract UncheckedBlock {
function loopWith(uint256 n) public pure returns (uint256 sum) {
// 0.8+ checks overflow on every operation (extra gas)
for (uint256 i = 0; i < n; i++) {
sum += i;
}
}
function loopUnchecked(uint256 n) public pure returns (uint256 sum) {
unchecked {
for (uint256 i = 0; i < n; i++) {
sum += i; // skips overflow check (cheaper)
}
}
}
// Safe to use when overflow is impossible:
// - Loop counters that won't reach 2^256
// - Differences of values you've already bounds-checked
function safeSub(uint256 a, uint256 b) public pure returns (uint256) {
require(a >= b, "underflow"); // checked here
unchecked { return a - b; } // safe: can't underflow now
}
// NEVER use unchecked when overflow/underflow is possible
// unless you intentionally want wraparound (rare).
}自定义错误 vs Require
自定义错误(0.8.4+)比 require(_, "msg") 便宜,因为消息字符串存储在字节码中(每字节消耗 gas)并在每次回滚时重新编码。自定义错误只是 4 字节选择器加上 ABI 编码的参数。对于非平凡的错误消息,自定义错误每次调用节省数千 gas 并减少部署字节码大小。新代码中始终优先使用自定义错误。
contract Errors {
// BAD: string messages bloat bytecode + cost gas per call
function oldStyle(uint256 x) public pure {
require(x > 0, "value must be greater than zero");
require(x < 100, "value must be less than one hundred");
}
// GOOD: custom errors are cheap and structured
error InvalidValue(uint256 given, uint256 min, uint256 max);
function newStyle(uint256 x) public pure {
if (x == 0 || x >= 100) {
revert InvalidValue(x, 1, 99);
}
}
// Gas comparison (approximate):
// require(_, "msg") ~ 400 + 50 per byte of message
// revert CustomError() ~ 400 + small selector cost
//
// For long messages, custom errors save thousands of gas
// and reduce deployed bytecode size.
// Define errors at file level to reuse across contracts
error Unauthorized(address caller);
function guarded() public {
if (msg.sender != address(0)) revert Unauthorized(msg.sender);
}
}循环中缓存状态变量
在循环之前将数组长度缓存到局部变量中(每次迭代节省一次 SLOAD)。如果数组很小,将其复制到内存一次(每个元素一次 SLOAD,然后循环中便宜的 MLOAD)。累加器应始终是局部(内存)变量。对于存储结构体字段,如果多次使用,将整个结构体缓存到内存。