Skip to content

Solidity 速查表

用于在以太坊上编写智能合约的面向对象语言。

01

入门

合约基础

Solidity 是以太坊智能合约的主要语言。pragma 设置编译器版本。合约类似于类。函数可以是 public、private、view(只读)或 payable。

solidity
// 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。

solidity
// 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 等工具会读取它们。

solidity
// 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 用于私有代码。

solidity
// 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' 关键字建立继承。文件通常有一个主合约,但也可以包含多个。

solidity
// 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 {
    // ...
}
02

值类型

整数(uint / int)

uint = 无符号整数(无负数),int = 有符号。大小从 8 到 256,步长为 8。uint 默认为 uint256。自 0.8.0 起,算术自动检查溢出/下溢并回滚。使用 type(T).max / type(T).min 获取边界。

solidity
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。

solidity
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 校验和验证地址字面量以捕获拼写错误。

solidity
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 更便宜。

solidity
// 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 给出最高成员。

solidity
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 会由于二进制补码而产生巨大数字。

solidity
// 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!
03

引用类型

字符串

Solidity 中的字符串是 UTF-8 字节数组——length() 返回字节数,而不是字符数(emoji/多字节字符不同)。相等性必须通过 keccak256 哈希检查。string.concat(0.8.12+)比 abi.encodePacked 便宜。字符串 gas 开销大。

solidity
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+ 中返回引用。

solidity
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} 初始化更清晰;位置初始化更短。在旧版本中,映射值不能作为整体直接访问结构体。

solidity
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 将键重置为其零值。

solidity
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。

solidity
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 高效的。注意深层嵌套——它增加存储槽使用和部署成本。结构体映射是最常见的链上数据库模式。

solidity
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;
}
04

状态变量

公开与私有状态变量

public 状态变量自动生成 getter 函数。private 限制为合约内部访问;internal 也允许子类。默认是 internal。关键:'private' 并不意味着秘密——所有区块链数据都是公开可读的。如果需要机密性,使用加密/哈希。

solidity
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。

solidity
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、代币名称或链特定配置。

solidity
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。

solidity
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 对重放保护至关重要。

solidity
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
    }
}
05

函数

函数语法

函数语法:function name(params) visibility mutability returns(...)。可见性(public/external/internal/private)是必需的。可变性(pure/view/payable)可选但推荐。支持多个返回值。命名返回值允许你直接赋值并跳过 return 关键字。

solidity
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——这表明意图并允许链下免费读取。

solidity
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 函数。

solidity
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。

solidity
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,) 跳过不需要的值。

solidity
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,或不同数字范围很有用。

solidity
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
}
06

构造函数与特殊函数

构造函数

constructor 在合约部署时运行一次,用于初始化状态。它不能再次被调用。构造函数参数根据框架追加到部署字节码或通过 ABI 传递。对于继承,在继承列表(Owned(_owner))或修饰符样式(A() B())中传递父构造函数参数。

solidity
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)。

solidity
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(津贴),不足以覆盖存储写入或发出事件。

solidity
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 仍可到达,代码可能持续存在。

solidity
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。

solidity
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)
        );
    }
}
07

修饰器

修饰器基础

修饰器用可重用的前置/后置条件包装函数。_; 占位符是函数体执行的位置。修饰器非常适合访问控制(onlyOwner)、验证(validInput)和重入保护。它们默认在函数体之前应用——使用 _; 控制函数体运行的位置。

solidity
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) 用于节流。_; 之前的逻辑是前置条件,之后是后置条件。

solidity
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。

solidity
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。用注释记录预期的顺序。

solidity
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(使用时间戳的每用户节流)。重入保护是最重要的——将其应用于在状态更改后进行外部调用的任何函数。

solidity
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;
        _;
    }
}
08

事件

事件声明与触发

事件被记录到区块链并可被链下客户端(通过 eth_getLogs)读取。它们不能被其他合约读取。对最多 3 个参数使用 indexed 使其可过滤。事件的 gas 成本低于存储写入。始终为重要的状态更改发出事件,以便 dApp 和索引器可以反应。

solidity
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 哈希且不可恢复。

solidity
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 节省微乎其微。

solidity
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、状态码)。事件是合约与链下通信的主要渠道。

solidity
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 和索引器传达状态更改。

solidity
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.
}
09

错误处理

Require

require(cond, msg) 在 cond 为 false 时回滚交易并退还未使用的 gas。它是最常见的验证原语——用于输入检查、访问控制和前置条件。消息字符串消耗 gas(存储在部署字节码中)。为了节省 gas,使用自定义错误(revert ErrorName())代替字符串消息。

solidity
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))最便宜且最具描述性——在新代码中优先使用。

solidity
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。

solidity
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);'。

solidity
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) 是自定义错误和未知回滚的后备。用于优雅地处理外部合约失败。

solidity
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:')以帮助日志索引。

solidity
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);
}
10

继承

使用 'is' 继承

继承使用 'is' 关键字。子合约继承父合约的所有状态变量、函数和修饰器。父构造函数参数在继承列表(Animal("dog"))中或通过构造函数修饰符样式传递。子合约可以覆盖父合约中标记为 virtual 的函数。Solidity 支持多重继承(通过 C3 线性化)。

solidity
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 中遮蔽)。

solidity
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 时,始终检查线性化顺序(最派生的在最后)。

solidity
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)'。构造函数按线性化顺序运行(父类先)。

solidity
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))传递运行时值。对于多个父类,按线性化顺序(最基类先)传递它们。如果父类有无参构造函数,可以省略。构造函数在部署时运行一次,按线性化顺序。

solidity
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) {}
}
11

抽象合约与接口

抽象合约

抽象合约至少有一个没有函数体的函数(用 virtual 声明,无实现)。它不能直接部署——子合约必须实现所有抽象函数。当你想要共享状态 + 修饰器 + 部分实现时使用抽象合约。在 0.6.0 之前,如果任何函数缺少函数体,'abstract' 关键字是必需的。

solidity
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 之外的修饰符。

solidity
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 定义使用接口。

solidity
// 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)通过不继承实现来减少字节码。实现接口迫使你的合约满足标准。

solidity
// 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 交互而无需知道其内部实现。

solidity
// 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);
    }
}
12

库基础

库类似于合约但不能有状态、继承或被继承。它们部署一次并通过 DELEGATECALL(external)或内联(internal)重用。internal 库函数内联到调用合约中——无需单独部署。使用库对可重用函数(数学、验证、格式化)进行分组。'using SafeMath for uint256' 指令允许你以 a.b() 方式调用方法。

solidity
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;' 指令。这纯粹是语法糖——编译器将其重写为直接调用。

solidity
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 低于外部库。

solidity
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)且库必须先部署并在编译时链接其地址。大多数库使用内部函数;外部很少见。

solidity
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 版本仍被广泛引用。

solidity
// 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;
    }
}
13

Ether 转账

Payable 与 msg.value

payable 函数通过 msg.value 接收 Ether(以 wei 为单位)。Ether 自动添加到合约余额——除非你想要按用户记账,否则不需要手动跟踪它。验证 msg.value 以确保确切或最低付款。始终退还多余的 Ether 以避免锁定在合约中。1 ether = 10^18 wei。

solidity
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:} 和显式重入保护。

solidity
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:}。

solidity
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 仅用于受信任地址。

solidity
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)膨胀余额,因此永远不要依赖确切的余额相等进行逻辑——使用 >= 或维护自己的记账变量。始终交叉检查存款总和等于合约余额作为不变量。

solidity
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;
    }
}
14

Gas 优化

Storage vs Memory

存储读取(SLOAD)每次访问冷约 2100 gas / 热约 100 gas。内存读取(MLOAD)只需 3 gas。当函数多次读取状态变量(特别是在循环中)时,首先将其缓存到内存变量中。第一次 SLOAD 昂贵;后续内存读取几乎免费。这是最简单且最有影响力的优化之一。

solidity
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。注意:映射和动态数组条目总是开始新槽,不能打包。

solidity
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——静默溢出是安全风险。

solidity
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 并减少部署字节码大小。新代码中始终优先使用自定义错误。

solidity
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)。累加器应始终是局部(内存)变量。对于存储结构体字段,如果多次使用,将整个结构体缓存到内存。

solidity
contract Cache {
    uint256[] public data;
    mapping(address => uint256) public balances;
    address[] public users;

    // BAD: reads mapping in every iteration (SLOAD each time)
    function sumBad() public view returns (uint256) {
        uint256 total = 0;
        for (uint256 i = 0; i < users.length; i++) {
            total += balances[users[i]];  // SLOAD each iteration
        }
        return total;
    }

    // GOOD: cache length, use local var for accumulator
    function sumBetter() public view returns (uint256) {
        uint256 total = 0;
        uint256 len = users.length;  // cache length
        for (uint256 i = 0; i < len; i++) {
            address u = users[i];     // SLOAD once
            total += balances[u];     // still SLOAD, but unavoidable
        }
        return total;
    }

    // BEST: cache array to memory (if small enough)
    function sumMemory() public view returns (uint256) {
        address[] memory memUsers = users;  // copy to memory once
        uint256 total = 0;
        uint256 len = memUsers.length;
        for (uint256 i = 0; i < len; i++) {
            total += balances[memUsers[i]];  // MLOAD each iteration
        }
        return total;
    }
}

短路与循环优化

短路求值:在 && 和 || 中将便宜的检查放在前面。使用 ++i(前缀)而不是 i++(稍微便宜——无临时变量)。将循环计数器包装在 unchecked 中(i 实际上不会溢出 2^256)。找到所需内容时提前退出循环。避免循环内的昂贵操作(外部调用、存储写入)——在循环外批量处理。

solidity
contract ShortCircuit {
    // && and || short-circuit: right side only evaluated if needed
    function cheap(address a, address b) public view returns (bool) {
        // Put the cheap check first
        return (a == b || expensiveCheck(a));
    }

    function expensiveCheck(address) internal view returns (bool) {
        // imagine a heavy computation
        return true;
    }

    // Loop optimization: prefer prefix increment
    function good() public pure returns (uint256) {
        uint256 sum = 0;
        for (uint256 i = 0; i < 10; ++i) {  // ++i slightly cheaper than i++
            sum += i;
        }
        return sum;
    }

    // Use unchecked for loop counters (always safe)
    function best() public pure returns (uint256) {
        uint256 sum = 0;
        uint256 i = 0;
        unchecked {
            while (i < 10) {
                sum += i;
                ++i;
            }
        }
        return sum;
    }

    // Avoid unnecessary work: precompute, batch, early-exit
    function find(uint256[] memory arr, uint256 target)
        public pure returns (int256)
    {
        for (uint256 i = 0; i < arr.length; ++i) {
            if (arr[i] == target) return int256(i);  // early exit
        }
        return -1;
    }
}
15

时间单位与区块属性

block.timestamp

block.timestamp(0.7.0 之前的别名 'now')是当前区块的 Unix 时间戳(秒)。它由矿工/验证者设置,可被操纵约 15 秒——永远不要用于随机数或精确计时。它适用于粗略持续时间(>=1 分钟)和排序。常见用途:时间锁、归属计划、截止日期。对更严格的保证使用 block.number。

solidity
contract Time {
    uint256 public startTime;
    uint256 public constant DURATION = 1 days;

    constructor() {
        startTime = block.timestamp;  // uint256 in 0.8+
    }

    function isExpired() public view returns (bool) {
        return block.timestamp >= startTime + DURATION;
    }

    function elapsed() public view returns (uint256) {
        return block.timestamp - startTime;
    }

    // WARNING: block.timestamp is set by the miner/validator.
    // They can manipulate it by ~15 seconds. NEVER use it for:
    //   - Randomness (keccak256(block.timestamp) is exploitable)
    //   - Precise timing (use block.number as a fallback)
    // It's fine for:
    //   - Coarse durations (>= 1 minute)
    //   - Deadlines that tolerate 15s drift
    //   - Order of events (timestamp comparison)

    // Time locks: require(block.timestamp > unlockTime)
    function lock() public payable returns (uint256) {
        return block.timestamp + 1 weeks;
    }
}

block.number 与 blockhash

block.number 是当前区块高度。blockhash(n) 返回区块 n 的哈希,但只返回最近 256 个区块(更旧的返回 0)且不是当前区块(返回 0)。block.number 和 blockhash 都可被矿工影响,因此不要用于高风险随机数。对于真正的随机数,使用 Chainlink VRF 或带用户提供的熵的 commit-reveal 方案。

solidity
contract BlockInfo {
    function currentBlock() public view returns (uint256) {
        return block.number;
    }

    function prevHash(uint256 n) public view returns (bytes32) {
        // Only the last 256 blocks' hashes are available
        require(n > 0 && n <= 256, "out of range");
        return blockhash(block.number - n);
    }

    // blockhash returns 0 for current block, future blocks, or >256 old
    function currentHash() public view returns (bytes32) {
        return blockhash(block.number);  // returns 0!
    }

    // Use case: weak randomness (still exploitable by miners)
    function pseudoRandom() public view returns (uint256) {
        // A miner CAN see this and choose to withhold blocks
        // to manipulate the result. Don't use for high value.
        return uint256(keccak256(abi.encodePacked(
            blockhash(block.number - 1),
            block.timestamp,
            msg.sender
        )));
    }

    // For real randomness, use Chainlink VRF or a commit-reveal scheme.
}

时间单位(seconds/minutes/hours/days/weeks)

时间单位后缀(seconds、minutes、hours、days、weeks)转换为秒作为 uint256。'years' 在 0.5.0 中因闰年歧义被移除。Ether 单位(wei、gwei、ether)是分开的:1 ether = 10^18 wei,1 gwei = 10^9 wei。始终以 wei 存储金额,在计算中使用后缀以提高可读性。

solidity
contract TimeUnits {
    // Suffixes convert to seconds (uint256):
    uint256 public oneSecond = 1 seconds;     // 1
    uint256 public oneMinute = 1 minutes;     // 60
    uint256 public oneHour = 1 hours;         // 3600
    uint256 public oneDay = 1 days;           // 86400
    uint256 public oneWeek = 1 weeks;         // 604800

    // 'years' was removed in 0.5.0 (leap years made it ambiguous)

    function deadline() public view returns (uint256) {
        return block.timestamp + 7 days;
    }

    function hourly() public pure returns (uint256) {
        return 24 hours;  // 86400
    }

    // Ether units (different from time units!):
    uint256 public oneWei = 1 wei;            // 1
    uint256 public oneGwei = 1 gwei;          // 10^9
    uint256 public oneEther = 1 ether;         // 10^18

    function fromEther(uint256 e) public pure returns (uint256) {
        return e * 1 ether;  // convert to wei
    }

    function toEther(uint256 weiAmount) public pure returns (uint256) {
        return weiAmount / 1 ether;
    }
}

Gas 与 gasleft()

gasleft() 返回调用点的剩余 gas(替代已弃用的 msg.gas)。它用于测量代码消耗多少 gas。63/64 规则:外部调用转发剩余 gas 的 63/64,保留 1/64 以便调用者可以处理返回。不要依赖跨硬分叉的精确 gas 值——操作码成本会改变。

solidity
contract GasTrack {
    function checkGas() public returns (uint256, uint256) {
        uint256 start = gasleft();   // gas remaining at this point
        // ...do some work...
        uint256 sum = 0;
        for (uint256 i = 0; i < 100; i++) {
            sum += i;
        }
        uint256 end = gasleft();
        return (start, start - end);  // (initial, used)
    }

    // 63/64th rule: when an external call is made, only 63/64 of
    // remaining gas is forwarded; 1/64 is reserved so the caller
    // can finish processing (e.g., revert handling).

    function forwardGas(address target) public returns (bool, uint256) {
        uint256 before = gasleft();
        (bool ok, ) = target.call{gas: 100000}("");
        uint256 after = gasleft();
        return (ok, before - after);
    }

    // gasleft() replaces the deprecated msg.gas.
    // It's a view, so it can be used in modifiers to enforce budgets.

    modifier usesLessThan(uint256 maxGas) {
        uint256 start = gasleft();
        _;
        require(start - gasleft() <= maxGas, "too much gas");
    }
}

链 ID 与网络检测

block.chainid 返回当前链 ID。它对重放保护(EIP-155)和 EIP-712 类型化数据签名(域分隔符包含 chainId)至关重要。在构造函数中将 chainId 缓存为 immutable 以提高 gas 效率,但对于 EIP-712,如果合约被分叉到新链,可能需要重新计算。始终在签署授权的任何哈希中包含 chainId。

solidity
contract Chain {
    function getChainId() public view returns (uint256) {
        return block.chainid;   // current chain ID
    }

    // Common chain IDs:
    // 1 = Ethereum mainnet
    // 5 = Goerli testnet (deprecated)
    // 11155111 = Sepolia testnet
    // 137 = Polygon
    // 42161 = Arbitrum One
    // 10 = Optimism
    // 56 = BNB Smart Chain

    modifier onlyMainnet() {
        require(block.chainid == 1, "only mainnet");
        _;
    }

    // Prevent cross-chain replay attacks by including chainId in hashes
    function domainSeparator() public view returns (bytes32) {
        return keccak256(abi.encode(
            keccak256("EIP712Domain(...)"),
            block.chainid,
            address(this)
        ));
    }

    // For multi-chain deployments, store chainId at construction
    uint256 public immutable chainId;
    constructor() {
        chainId = block.chainid;
    }

    // NOTE: chainId can change during a fork (rare). For EIP-712
    // signatures, cache it but allow re-initialization on mismatch.
}
16

可见性修饰符

Public

public 函数可以从任何地方调用:外部(由用户或其他合约)、内部(从合约内)以及从派生合约调用。public 状态变量自动获得 getter 函数。public 是最宽松的,但会增加字节码(函数必须支持内部和外部调用约定)。当通过 'this.f()' 内部调用 public 函数时,它成为昂贵的外部调用。

solidity
contract Public {
    uint256 public counter;   // public state var => auto getter

    // public function: callable from anywhere
    // - externally (via transaction or call)
    // - internally (from this contract)
    // - from derived contracts
    function increment() public {
        counter += 1;
    }

    function caller() public {
        increment();         // internal call (cheap)
        this.increment();    // external call (expensive, creates a msg)
    }

    // Public is the most permissive. Auto-getters for state vars
    // are 'external' view functions generated by the compiler.
    //
    // Cost: public functions are part of the ABI and add bytecode.
    // Internal calls to public functions skip the external wrapper.
}

External

external 函数只能从合约外部调用——不能在内部调用。它们比 public 稍便宜,因为直接从 calldata 读取(无内存复制)。对只应是入口点的函数使用 external。如果需要同时内部和外部调用函数,将其设为 public 或拆分为外部包装器 + 内部实现。

solidity
contract External {
    // external: ONLY callable from outside the contract
    // CANNOT be called internally (this.f() works but is expensive)
    function doWork(uint256 x) external pure returns (uint256) {
        return x * 2;
    }

    function caller() public pure returns (uint256) {
        // doWork(5);  // COMPILER ERROR: external not callable internally
        return this.doWork(5);  // works but creates an external call (expensive)
    }

    // external with calldata params is cheapest for inputs
    function process(string calldata input)
        external
        pure
        returns (bytes32)
    {
        return keccak256(bytes(input));
    }

    // Use external when:
    // - The function is only meant to be called by users/other contracts
    // - You want to read calldata directly (cheaper than memory)
    //
    // State variables CANNOT be external.
}

Internal

internal 函数可以从合约内和任何派生合约(子类)调用。它是状态变量的默认值。internal 比 public 便宜,因为它不需要外部调用包装器。对子类应该能够调用或覆盖的辅助函数和核心逻辑使用 internal。约定是用下划线前缀内部函数(_helper)。

solidity
contract Internal {
    uint256 internal secret;   // internal state var (default)

    // internal: callable from this contract AND derived contracts
    function _helper(uint256 x) internal pure returns (uint256) {
        return x + 1;
    }

    function caller() public pure returns (uint256) {
        return _helper(5);   // internal call works
    }

    // Cannot be called externally:
    // contract Other { function f() public { Internal._helper(5); } }  // ERROR
}

contract Child is Internal {
    function useParent() public pure returns (uint256) {
        return _helper(10);   // OK: child can call internal
    }

    function readSecret() public view returns (uint256) {
        return secret;        // OK: child can read internal
    }
}

// internal is the DEFAULT for state variables.
// Use internal when:
//   - You want subclasses to use/override it
//   - You don't need external access
// It's cheaper than public (no external wrapper).

Private

private 函数和状态变量只能在定义合约内访问——即使是子类也不行。它是最严格的可见性。关键:'private' 并不意味着秘密——所有区块链数据都可以通过 eth_getStorageAt 和字节码分析公开读取。使用 private 仅防止其他合约调用/继承;使用加密/哈希进行实际机密性。

solidity
contract Private {
    uint256 private counter;   // private state var

    // private: ONLY callable from THIS contract (not even subclasses)
    function _secret(uint256 x) private pure returns (uint256) {
        return x * 42;
    }

    function caller() public pure returns (uint256) {
        return _secret(5);   // OK: same contract
    }
}

contract Sub is Private {
    // function useIt() public pure returns (uint256) {
    //     return _secret(5);  // COMPILER ERROR: not accessible
    // }

    // function readCounter() public view returns (uint256) {
    //     return counter;     // COMPILER ERROR: not accessible
    // }
}

// IMPORTANT: 'private' does NOT mean hidden on-chain.
// All storage and bytecode are publicly readable via RPC.
// 'private' only means 'not part of the contract's ABI'
// and 'not callable/inheritable by other contracts'.
// For real confidentiality, use encryption or zero-knowledge proofs.

可见性最佳实践

最佳实践:使用有效的最严格可见性(private > internal > external > public)。避免 public 状态变量,除非你想要自动 getter——显式 getter 允许你稍后添加验证。对入口点使用 external+calldata。用下划线前缀 internal/private 辅助函数。每个 public/external 函数都会增加字节码;最小化 ABI 可减少部署和调用 gas。

solidity
contract Best {
    // 1. Default to the MOST restrictive visibility that works.
    //    Order: private < internal < external < public

    // 2. State variables: NEVER public unless you want a free getter.
    //    Prefer private/internal + explicit getter functions,
    //    so you can add validation/logic later.
    uint256 private _balance;
    function balance() public view returns (uint256) {
        return _balance;
    }

    // 3. External functions: use 'external' + 'calldata' for entry points
    function buy(uint256[] calldata ids) external payable {
        for (uint256 i = 0; i < ids.length; i++) {
            _buyOne(ids[i]);
        }
    }

    // 4. Internal helper: prefix with underscore convention
    function _buyOne(uint256 id) internal {
        // ...
    }

    // 5. Private for truly internal-only logic
    function _computeFee(uint256 amount) private pure returns (uint256) {
        return amount / 100;
    }

    // 6. Avoid public functions that are never called externally
    //    (wastes bytecode). Make them internal.
}

// Summary: visibility is part of the contract's API design.
// Smaller ABI = less bytecode = lower deployment + call gas.
17

合约交互

调用其他合约

要调用另一个合约,将其地址强制转换为接口类型:IToken(addr).method()。view 调用链下免费,但从交易调用时消耗 gas。始终检查状态更改调用的返回值(token.transfer 返回 bool)。将地址强制转换为接口不会验证合约实际实现了它——如果函数不存在,调用会回滚。

solidity
interface IToken {
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

contract Caller {
    IToken public token;
    address public owner;

    constructor(address _token) {
        token = IToken(_token);
        owner = msg.sender;
    }

    // Call a view function on another contract
    function getBalance(address who) public view returns (uint256) {
        return token.balanceOf(who);  // external call
    }

    // Call a state-changing function
    function sendTokens(address to, uint256 amount)
        public returns (bool)
    {
        require(msg.sender == owner, "not owner");
        bool ok = token.transfer(to, amount);
        require(ok, "transfer failed");
        return true;
    }

    // Cast any address to an interface (no validation!)
    function checkAny(address maybeToken)
        public view returns (uint256)
    {
        return IToken(maybeToken).balanceOf(address(this));
    }
}

基于接口的调用

使用接口与任何你知道 ABI 的合约交互,无需其源代码。这将调用者与实现解耦,并适用于遵循标准的任何代币/协议(ERC20、ERC721、Chainlink 数据源)。接口保持字节码小(无继承逻辑)并允许你编写通用代码(适用于任何 ERC20 的钱包)。外部调用始终优先使用接口而不是具体合约。

solidity
interface IERC20 {
    function name() external view returns (string memory);
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

contract Wallet {
    IERC20 public immutable token;
    address public owner;

    constructor(address _token) {
        token = IERC20(_token);
        owner = msg.sender;
    }

    function tokenBalance() public view returns (uint256) {
        return token.balanceOf(address(this));
    }

    function withdraw(uint256 amount) public {
        require(msg.sender == owner, "not owner");
        token.transfer(owner, amount);
    }

    function tokenName() public view returns (string memory) {
        return token.name();
    }
}

// Why use interfaces?
// - Decouple: Wallet doesn't need to know token implementation
// - Reusability: works with ANY ERC20 (USDC, DAI, WETH, ...)
// - Smaller bytecode: don't inherit the token's code
// - Standardization: IERC20 is a documented ABI

低级 call

address.call(payload) 是最低级别的调用,返回 (bool, bytes)。当你没有接口、想要转发任意 calldata 或需要精细控制(gas、value)时使用它。用 abi.encodeWithSignature/Selector/Call 构建 calldata。关键:'ok' 仅在 EVM 失败(revert、out-of-gas)时为 false,逻辑失败不为 false——解码 'data' 以检查回滚原因或自定义错误。

solidity
contract LowLevel {
    // call: lowest-level call, returns (bool, bytes)
    function callFn(address target, bytes memory payload)
        public returns (bool, bytes memory)
    {
        (bool ok, bytes memory data) = target.call(payload);
        require(ok, "call failed");
        return (ok, data);
    }

    // With Ether
    function callWithEth(address target, bytes memory payload)
        public payable returns (bool, bytes memory)
    {
        (bool ok, bytes memory data) = target.call{value: msg.value}(payload);
        return (ok, data);
    }

    // With gas limit
    function callWithGas(address target, bytes memory payload, uint256 gas)
        public returns (bool, bytes memory)
    {
        return target.call{gas: gas}(payload);
    }

    // Helpers to build calldata:
    // abi.encodeWithSignature("transfer(address,uint256)", to, amount)
    // abi.encodeWithSelector(bytes4(selector), to, amount)
    // abi.encodeWithCall(IERC20.transfer.selector, to, amount)

    // WARNING: low-level call returns true even if the called function
    // reverts internally — you only get false on EVM-level failure.
    // Decode 'data' to inspect custom errors or return values.
    function safeCall(address target) public returns (uint256) {
        (bool ok, bytes memory data) = target.call(
            abi.encodeWithSignature("getValue()")
        );
        require(ok, "call reverted");
        return abi.decode(data, (uint256));
    }
}

Delegatecall

delegatecall 在调用者的存储上下文中运行目标代码——因此状态更改影响调用者,而不是目标。msg.sender 和 msg.value 被保留。这是可升级代理(EIP-1967、UUPS)和钻石模式(EIP-2535)的基础。关键:调用者和目标之间的存储布局必须匹配,否则状态会损坏。始终使用共享存储布局或 AppStorage 模式。

solidity
contract Delegate {
    // delegatecall: executes target's code in CALLER's context
    // - msg.sender is the ORIGINAL caller (preserved)
    // - msg.value is preserved
    // - Storage reads/writes affect the CALLER, not the target
    // - address(this) is the CALLER

    uint256 public num;
    address public sender;

    function delegateSet(address target, uint256 value) public {
        // target's setNum() runs in THIS contract's storage
        (bool ok, ) = target.delegatecall(
            abi.encodeWithSignature("setNum(uint256)", value)
        );
        require(ok, "delegatecall failed");
    }
}

contract Target {
    // MUST have the same storage layout as Delegate for sane behavior!
    uint256 public num;
    address public sender;

    function setNum(uint256 value) public {
        num = value;
        sender = msg.sender;  // this is the ORIGINAL caller, not Delegate
    }
}

// Use cases:
// - Proxy patterns (EIP-1967, UUPS, Transparent): upgradeable logic
// - Diamond pattern (EIP-2535): modular facets
//
// DANGER: storage layout mismatch => corrupted state.
// Both contracts MUST declare variables in the same order/types.
// Use a shared storage contract or app storage pattern.

Staticcall

staticcall 执行目标代码,但如果发生任何状态修改则回滚——这是从不受信任合约安全读取的方式。通过接口调用 view/pure 函数时,编译器自动使用 staticcall。当你想要确保对可能谎称是 view 的合约进行只读调用时,直接使用 staticcall。它像 call 一样返回 (bool, bytes)。

solidity
contract Static {
    // staticcall: like call, but REVERTS if the target modifies state.
    // Used to safely READ from untrusted contracts.

    function safeRead(address target, bytes memory payload)
        public view returns (bytes memory)
    {
        (bool ok, bytes memory data) = target.staticcall(payload);
        require(ok, "staticcall reverted");
        return data;
    }

    function getBalance(address token, address who)
        public view returns (uint256)
    {
        (bool ok, bytes memory data) = token.staticcall(
            abi.encodeWithSignature("balanceOf(address)", who)
        );
        require(ok, "balanceOf failed");
        return abi.decode(data, (uint256));
    }

    // Use cases:
    // - Read from untrusted contracts without risk of state changes
    // - Verify a contract behaves as a view function
    // - Compose view-only queries across multiple contracts

    // Distinguish from call():
    // - call: allows state changes (target can write)
    // - staticcall: forbids state changes (reverts if target writes)

    // view/pure functions are called via staticcall automatically
    // by the compiler when you call them normally.
    function normalCall(IToken t, address who) public view returns (uint256) {
        return t.balanceOf(who);  // compiler emits staticcall
    }
}

interface IToken { function balanceOf(address) external view returns (uint256); }
18

安全模式

重入保护

重入保护是一个互斥锁,防止函数在仍执行时被重入。在函数体前设置标志,之后取消设置。这防止了恶意合约的 receive/fallback 在余额清零之前重新调用 withdraw() 的经典攻击(DAO 黑客模式)。将 nonReentrant 应用于在状态更改后进行外部调用的任何函数——即使你也遵循 checks-effects-interactions(纵深防御)。

solidity
contract ReentrancyGuard {
    // Classic mutex pattern using a storage flag
    uint256 private _status = 1;  // 1 = NOT_ENTERED, 2 = ENTERED

    modifier nonReentrant() {
        require(_status == 1, "REENTRANT");
        _status = 2;
        _;
        _status = 1;
    }

    mapping(address => uint256) public balances;

    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "no balance");

        // CRITICAL: update state BEFORE the external call
        balances[msg.sender] = 0;

        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
    }

    // Why needed: if we sent Ether FIRST, the attacker's receive()
    // could call withdraw() again before balances was zeroed,
    // draining the contract. This is the DAO hack pattern.
    //
    // Even with checks-effects-interactions, the guard is defense-in-depth.
    // Apply nonReentrant to ANY function that:
    //   - Sends Ether to msg.sender
    //   - Calls an external contract
    //   - Then continues executing
}

Checks-Effects-Interactions

Checks-Effects-Interactions(CEI)是最重要的安全模式:(1)运行所有 require/验证检查,(2)更新所有状态变量,(3)然后进行外部调用。如果外部调用重入你的合约,状态已经更新,因此攻击者无法双重消费。CEI 单独防止了大多数重入漏洞;在关键函数上与非重入修饰器配对以进行纵深防御。

solidity
contract CEI {
    mapping(address => uint256) public balances;

    // BAD: interaction BEFORE effect => reentrancy vulnerability
    function vulnerable() public {
        uint256 amount = balances[msg.sender];
        (bool ok, ) = msg.sender.call{value: amount}("");  // INTERACTION
        require(ok);
        balances[msg.sender] = 0;   // EFFECT (too late!)
        // Attacker's receive() re-called this; balances still has funds.
    }

    // GOOD: checks, then effects, then interactions
    function safe() public {
        // 1. CHECKS: validate conditions
        uint256 amount = balances[msg.sender];
        require(amount > 0, "no balance");

        // 2. EFFECTS: update state BEFORE external calls
        balances[msg.sender] = 0;

        // 3. INTERACTIONS: external calls LAST
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
        // If attacker re-enters, balances[msg.sender] is already 0.
    }

    // Pattern: order your code as
    //   require() checks first,
    //   state updates second,
    //   external calls last.
    // This single rule prevents most reentrancy bugs.
    // Pair with nonReentrant for defense-in-depth.
}

Pull over Push

Pull over Push:不是在一笔交易中将 Ether 推送给多个收件人(其中一个坏收件人会阻止所有人),而是记入每个用户的余额并让他们通过 withdraw() 拉取资金。这隔离了失败(一个用户的坏 receive/fallback 不会阻止其他人)并减少每笔交易的 gas。用于空投、交易所、拍卖付款、退款。在 withdraw() 上结合 nonReentrant。

solidity
contract PullOverPush {
    // BAD (push): send Ether to many recipients in one tx
    // If any recipient reverts (or is a contract that blocks), the WHOLE tx fails.
    mapping(address => uint256) public pendingWithdrawals;

    function pushPay(address[] memory recipients) public payable {
        uint256 share = msg.value / recipients.length;
        for (uint256 i = 0; i < recipients.length; i++) {
            // If recipients[i].call fails, the whole loop reverts.
            (bool ok, ) = recipients[i].call{value: share}("");
            require(ok, "push failed");
        }
    }

    // GOOD (pull): credit each user, let them withdraw individually
    function credit(address[] memory recipients) public payable {
        uint256 share = msg.value / recipients.length;
        for (uint256 i = 0; i < recipients.length; i++) {
            pendingWithdrawals[recipients[i]] += share;  // just update state
        }
    }

    function withdraw() public {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "nothing to withdraw");
        pendingWithdrawals[msg.sender] = 0;  // effect
        (bool ok, ) = msg.sender.call{value: amount}("");  // interaction
        require(ok, "withdraw failed");
    }

    // Pull pattern isolates failures: one user's failed withdrawal
    // doesn't block others. Common in: airdrops, exchanges, auctions.
}

Pausable 模式

Pausable 模式添加紧急停止:一个布尔值 'paused',通过 whenNotPaused 修饰器控制关键函数。owner 可以暂停/取消暂停。当检测到 bug 或攻击时将其用作断路器——暂停以阻止进一步损害,同时准备修复。通常与可升级代理结合(暂停,然后部署修补逻辑)。OpenZeppelin 提供经过实战测试的 Pausable 合约。

solidity
contract Pausable {
    bool public paused;
    address public owner;

    constructor() { owner = msg.sender; }

    modifier whenNotPaused() {
        require(!paused, "contract is paused");
        _;
    }
    modifier whenPaused() {
        require(paused, "contract is not paused");
        _;
    }
    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function pause() public onlyOwner whenNotPaused {
        paused = true;
    }
    function unpause() public onlyOwner whenPaused {
        paused = false;
    }

    // Critical functions check whenNotPaused
    function deposit() public payable whenNotPaused {
        // ...
    }

    function withdraw(uint256 amount) public whenNotPaused {
        // ...
    }

    // Use case: emergency stop. If a bug is discovered, pause the
    // contract to prevent further damage while a fix is prepared.
    // Often combined with upgradeable proxies to deploy a patched version.
}

// OpenZeppelin provides Pausable as a reusable contract.

访问控制

访问控制模式:(1)Ownable——带 onlyOwner 修饰器的单一 owner(简单、集中)。(2)RBAC——带 bytes32 角色和 onlyRole 修饰器的基于角色(灵活、多管理员)。(3)多重签名或时间锁——对于高价值合约,使用 Gnosis Safe 或 OpenZeppelin Timelock。关键:授权始终使用 msg.sender,永远不要使用 tx.origin(可被恶意中间合约欺骗)。OpenZeppelin 提供 Ownable 和 AccessControl。

solidity
contract AccessControl {
    // 1. Simple owner (Ownable pattern)
    address public owner;
    constructor() { owner = msg.sender; }
    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }
    function setOwner(address newOwner) public onlyOwner {
        require(newOwner != address(0), "zero address");
        owner = newOwner;
    }

    // 2. Role-based (RBAC)
    mapping(bytes32 => mapping(address => bool)) public roles;
    bytes32 public constant ADMIN = keccak256("ADMIN");
    bytes32 public constant MINTER = keccak256("MINTER");

    modifier onlyRole(bytes32 role) {
        require(roles[role][msg.sender], "missing role");
        _;
    }

    function grantRole(bytes32 role, address account)
        public onlyRole(ADMIN)
    {
        roles[role][account] = true;
    }
    function revokeRole(bytes32 role, address account)
        public onlyRole(ADMIN)
    {
        roles[role][account] = false;
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER) {
        // ...
    }

    // 3. Multi-sig / time-locked (defer to Gnosis Safe or OpenZeppelin Timelock)

    // Always use msg.sender (NOT tx.origin) for authorization.
    // tx.origin can be spoofed if a user interacts with a malicious
    // intermediate contract.
}
19

ERC20 代币标准

ERC20 接口

ERC-20 是同质化代币(USDC、DAI、UNI)的标准。该接口定义了 6 个必需函数(totalSupply、balanceOf、transfer、allowance、approve、transferFrom)和 2 个事件(Transfer、Approval)。可选元数据(name、symbol、decimals)被普遍实现。新代币始终使用 OpenZeppelin 的 ERC20 基础合约——它经过审计、处理边缘情况,并被钱包/DEX 识别。

solidity
interface IERC20 {
    // Events
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // Metadata (optional but standard)
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);

    // Required functions
    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);
}

// ERC-20 is THE standard for fungible tokens (USDC, DAI, UNI, ...).
// Implemented by thousands of tokens; supported by every wallet/Dex.
// Implementing the interface correctly ensures interoperability.
// Use OpenZeppelin's ERC20 base contract — battle-tested and audited.

Transfer 函数

transfer(to, amount) 将代币从调用者移动到 'to'。标准检查:非零收件人地址、足够余额。在发出 Transfer 事件之前更新余额(CEI 模式)。铸币建模为从 address(0) 转账;销币是转账到 address(0)。decimals 字段(通常为 18)意味着金额以原子单位(wei 等价物)表示;UI 除以 10^decimals 进行显示。

solidity
contract SimpleToken is IERC20 {
    string public name = "Simple Token";
    string public symbol = "SIM";
    uint8 public constant decimals = 18;
    uint256 public totalSupply;

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(uint256 _initialSupply) {
        totalSupply = _initialSupply;
        balanceOf[msg.sender] = _initialSupply;
        emit Transfer(address(0), msg.sender, _initialSupply);
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "insufficient");

        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    // Note: 0.8+ auto-checks underflow, so we can simplify:
    //   balanceOf[msg.sender] -= amount;
    // If insufficient, it reverts automatically.
    // But explicit require gives clearer error messages.
}

Approve 与 Allowance

approve(spender, amount) 设置 'spender' 可以代表你转账多少(通过 transferFrom)。常见于 DEX 和质押。警告:approve 竞争条件——将 allowance 从 A 更改为 B 让攻击者如果前端运行则可以花费 A 和 B 两者。通过先设置为 0,然后设置为新值来缓解,或使用 increaseAllowance/decreaseAllowance(OpenZeppelin)。每次更改都必须发出 Approval 事件。

solidity
contract ApprovalExample is IERC20 {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    // approve: allow 'spender' to spend up to 'amount' on your behalf
    function approve(address spender, uint256 amount) external returns (bool) {
        require(spender != address(0), "zero address");
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    // Common use cases:
    //   - DEX trading: approve the DEX router to spend your tokens
    //   - Staking: approve the staking contract
    //   - Subscriptions: approve a recurring spender

    // WARNING: the approve race condition.
    // If you change allowance from 100 to 50, an attacker who saw 100
    // could spend 100 BEFORE your tx, then spend 50 AFTER = 150 total.
    // Mitigation (EIP-20): set to 0 first, then to new value.
    //   approve(spender, 0); approve(spender, 50);
    // Or use increaseAllowance / decreaseAllowance (OpenZeppelin).

    function increaseAllowance(address spender, uint256 added)
        public returns (bool)
    {
        uint256 newAllowance = allowance[msg.sender][spender] + added;
        allowance[msg.sender][spender] = newAllowance;
        emit Approval(msg.sender, spender, newAllowance);
        return true;
    }
}

TransferFrom

transferFrom(from, to, amount) 让批准的 spender 将代币从 'from' 移动到 'to'。spender 的 allowance 必须 >= amount;作为效果(CEI)的一部分减少 allowance。被 DEX 路由器、质押合约和任何基于拉取的工作流使用。代币特性:USDT 不减少 allowance;一些代币在零地址转账时回滚。集成前始终用特定代币测试。

solidity
contract TransferFromExample is IERC20 {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    // transferFrom: spender moves tokens from 'from' to 'to'
    // Requires allowance[from][spender] >= amount
    function transferFrom(address from, address to, uint256 amount)
        external returns (bool)
    {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "insufficient balance");
        require(allowance[from][msg.sender] >= amount, "insufficient allowance");

        // Update allowance FIRST (CEI)
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }

    // Used by:
    //   - DEX routers (user approves router, router calls transferFrom)
    //   - Staking contracts
    //   - Any 'pull' workflow where a contract takes tokens from a user

    // Note on infinite allowance:
    // Some tokens (USDT) do NOT decrement allowance on transferFrom,
    // and others set allowance to max uint256 by default. Read the
    // specific token's docs before integrating.
}

ERC20 事件

ERC-20 定义了两个事件:Transfer(在每次代币移动时发出,包括从 address(0) 铸币和销币到 address(0))和 Approval(在每次 allowance 更改时发出)。这些事件是 dApp 和索引器(The Graph、Dune、Etherscan)如何在不直接查询状态的情况下跟踪代币活动的方式。始终发出它们——省略事件会破坏与依赖日志的钱包和交易所的互操作性。

solidity
contract ERC20Events {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    uint256 public totalSupply;

    function _mint(address to, uint256 amount) internal {
        require(to != address(0), "zero address");
        totalSupply += amount;
        balanceOf[to] += amount;
        // Minting = transfer from address(0)
        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) internal {
        require(balanceOf[from] >= amount, "insufficient");
        balanceOf[from] -= amount;
        totalSupply -= amount;
        // Burning = transfer to address(0)
        emit Transfer(from, address(0), amount);
    }

    function _approve(address owner, address spender, uint256 amount) internal {
        emit Approval(owner, spender, amount);
    }

    // Events let dApps and indexers (The Graph, Dune) track:
    //   - Every transfer (incl. mint/burn)
    //   - Every approval change
    //   - Token holdings over time
    // Without events, integrating with wallets/DEXs is much harder.
}
20

部署与测试

Remix IDE

Remix 是开始使用 Solidity 的最简单方式——一个无需安装的浏览器 IDE。它包括编译器、用于本地测试的 JavaScript VM、部署到测试网、交易调试器和静态分析。非常适合学习和原型设计。对于生产,转移到 Hardhat 或 Foundry 进行脚本编写、自动化测试和 CI/CD 集成。

solidity
// Remix is a browser-based IDE for Solidity.
// URL: https://remix.ethereum.org

// Features:
//   - File explorer, editor, compiler, deployer in one tab
//   - Built-in Solidity compiler (no install needed)
//   - Deploy to JavaScript VM, local node, or testnet
//   - Interact with deployed contracts via GUI
//   - Debug transactions step-by-step
//   - Static analysis tools (Remix Analyzer)

// Workflow:
// 1. Create a .sol file in the File Explorer
// 2. Compile in the "Solidity Compiler" tab (pick version)
// 3. Deploy in the "Deploy & Run Transactions" tab
// 4. Interact via the deployed contract panel

// Good for: learning, prototyping, quick tests, debugging.
// Not ideal for: production workflows, automated testing, scripting.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Hello {
    string public msg = "Hello from Remix";
}

Hardhat 设置

Hardhat 是一个基于 JavaScript/TypeScript 的开发环境。通过 npm 安装,在 hardhat.config.ts 中配置网络和编译器。它提供编译、测试(Mocha/Chai)、本地节点(hardhat node)、部署脚本和控制台。Hardhat Toolbox 插件包包括 ethers.js、chai、typechain 等。使用 .env 存储机密(私钥、RPC URL)。

solidity
// Hardhat: JavaScript/TypeScript development environment
// Install: npm install --save-dev hardhat
// Init:    npx hardhat init

// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.19",
    settings: { optimizer: { enabled: true, runs: 200 } },
  },
  networks: {
    hardhat: {},                    // built-in local network
    sepolia: {
      url: `${process.env.ALCHEMY_URL}`,
      accounts: [process.env.PRIVATE_KEY ?? ""],
    },
  },
};
export default config;

// Project layout:
//   contracts/    Solidity source files
//   scripts/      deployment scripts (JS/TS)
//   test/         test files (Chai/Mocha)
//   ignition/     Hardhat Ignition modules (declarative deploy)

// Common commands:
//   npx hardhat compile
//   npx hardhat test
//   npx hardhat run scripts/deploy.ts --network sepolia
//   npx hardhat node                // start local node
//   npx hardhat console             // interactive REPL

Hardhat 测试

Hardhat 测试使用 Mocha + Chai + ethers.js。getSigners() 返回带 ETH 的测试账户。connect(addr) 从特定账户发送交易。断言:相等(equal)、回滚(revertedWith)、事件(emit + withArgs)。TypeChain 从你的 ABI 自动生成 TypeScript 类型,为你提供自动补全和编译时检查。用 'npx hardhat test' 运行测试。

solidity
// test/Token.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { Token } from "../typechain-types";

describe("Token", () => {
  let token: Token;
  let owner: any, addr1: any;

  beforeEach(async () => {
    [owner, addr1] = await ethers.getSigners();
    const Factory = await ethers.getContractFactory("Token");
    token = await Factory.deploy(1000);
    await token.waitForDeployment();
  });

  it("assigns initial supply to owner", async () => {
    expect(await token.balanceOf(owner.address)).to.equal(1000);
  });

  it("transfers tokens between accounts", async () => {
    await token.transfer(addr1.address, 100);
    expect(await token.balanceOf(addr1.address)).to.equal(100);
  });

  it("reverts on insufficient balance", async () => {
    await expect(
      token.connect(addr1).transfer(owner.address, 1)
    ).to.be.revertedWith("insufficient");
  });

  it("emits Transfer event", async () => {
    await expect(token.transfer(addr1.address, 100))
      .to.emit(token, "Transfer")
      .withArgs(owner.address, addr1.address, 100);
  });
});

// Run: npx hardhat test
// TypeChain generates typed contract bindings in typechain-types/.

Foundry 设置

Foundry(forge)是一个快速的基于 Rust 的工具包,测试用 Solidity 本身编写——无需 JavaScript/TypeScript。通过 foundryup 安装。关键工具:forge(构建、测试、脚本)、cast(用于 RPC 调用的 CLI)、anvil(本地节点)、chisel(Solidity REPL)。它比 Hardhat 快,包括内置模糊测试。依赖项是 lib/ 下的 git 子模块。配置位于 foundry.toml 中。

solidity
// Foundry: Rust-based, fast Solidity dev toolkit
// Install:  curl -L https://foundry.paradigm.xyz | bash
//           foundryup
// Init:     forge init my-project

// Project layout:
//   src/        Solidity sources (Default = src/Contract.sol)
//   test/       Solidity tests (*.t.sol)
//   script/     Deployment scripts (*.s.sol)
//   lib/        Dependencies (git submodules, e.g. forge-std, openzeppelin)

// Commands:
//   forge build                 // compile
//   forge test                  // run tests
//   forge test -vvv             // verbose (shows console logs)
//   forge coverage              // code coverage
//   forge script script/Deploy.s.sol --rpc-url $RPC --broadcast

// Foundry config (foundry.toml):
// [profile.default]
// src = "src"
// out = "out"
// libs = ["lib"]
// solc = "0.8.19"
// optimizer = true
// optimizer_runs = 200

// Why Foundry?
// - Tests are written IN Solidity (no JS/TS context switch)
// - Extremely fast (parallelized, cached)
// - Built-in fuzzing and invariant testing
// - Cast/anvil/chisel CLI tools included

Foundry 测试(Solidity)

Foundry 测试是扩展 forge-std/Test 的 Solidity 合约。使用 assertEq、assertTrue 等进行断言。Cheatcodes(vm.*)操纵 EVM:vm.prank 伪造 msg.sender,vm.warp 设置时间,vm.deal 铸造 ETH,vm.expectRevert 断言失败。模糊测试(testFuzz_*)在约束内自动生成随机输入(vm.assume)。不变量测试(testInvariant_*)运行随机序列以查找破坏的不变量。

solidity
// test/Token.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import "../src/Token.sol";

contract TokenTest is Test {
    Token public token;
    address owner = address(this);
    address user = address(0x1);

    function setUp() public {
        token = new Token(1000);
    }

    function testInitialSupply() public view {
        assertEq(token.balanceOf(owner), 1000);
    }

    function testTransfer() public {
        token.transfer(user, 100);
        assertEq(token.balanceOf(user), 100);
    }

    function testRevertOnInsufficient() public {
        vm.expectRevert("insufficient");
        token.transfer(user, 99999);
    }

    // Fuzz test: 'x' is randomized by the framework
    function testFuzzTransfer(uint256 x) public {
        vm.assume(x <= 1000);
        token.transfer(user, x);
        assertEq(token.balanceOf(user), x);
    }

    // Cheatcodes (vm.*):
    //   vm.prank(addr)         // next call from addr
    //   vm.startPrank(addr)    // all subsequent calls from addr
    //   vm.warp(timestamp)     // set block.timestamp
    //   vm.roll(blockNum)      // set block.number
    //   vm.deal(addr, 1 ether) // give ETH to addr
    //   vm.expectRevert(...)   // assert next call reverts
    //   vm.expectEmit(...)     // assert next call emits event
}

// Run: forge test -vvv

这篇内容对您有帮助吗?