Skip to content

Solidity

导航目录

Solidity 简介

  • Solidity 是一种面向智能合约的高级编程语言,专门用于在以太坊区块链上编写去中心化应用(DApp)。它的语法类似 JavaScript 和 C++,支持继承、库、接口以及复杂数据类型。开发者可以用它创建自动执行的数字协议(智能合约),实现 代币发行、DeFi、NFT 等核心区块链业务逻辑。Solidity 代码最终会编译为 EVM(以太坊虚拟机)字节码 运行,具有 安全性优先、Gas 成本敏感 等特性,是当前最主流的区块链开发语言之一。

Solidity 基础语法

Solidity 是一种静态类型语言,语法类似于 JavaScript 和 C++。下面通过一个最小示例来说明基础结构。

合约结构(最小可运行示例)

solidity
// SPDX-License-Identifier: MIT  // 指定许可证
pragma solidity ^0.8.0;         // 声明编译器版本

contract MyContract {           // 合约定义
    // 状态变量(存储在 storage)
    uint256 public myNumber;

    // 构造函数:部署时执行一次
    constructor(uint256 initialNumber) {
        myNumber = initialNumber;
    }

    // 修改状态的函数(会消耗 gas)
    function setNumber(uint256 newNumber) public {
        myNumber = newNumber;
    }

    // view 函数:只读取状态,不修改
    function getNumber() public view returns (uint256) {
        return myNumber;
    }
}

实战建议: 编写合约时,优先区分 读操作(view写操作(会改状态、消耗 gas),有助于控制链上成本。

Solidity 数据类型

1. 基础值类型(Value Types)

布尔值 bool

solidity
bool isActive = true;  // 仅存储 true/false
  • 逻辑运算!aa && ba || b
  • 比较运算==!=

整数类型 int / uint

solidity
int32 negative = -1;                 // 有符号整数
uint256 large = type(uint256).max;  // 无符号整数最大值
  • 范围
    • intX:(-2^{X-1}) 到 (2^{X-1} - 1)
    • uintX:0 到 (2^X - 1)
  • 最佳实践:多数情况下优先使用 uint256 / int256(EVM 原生字长,Gas 表现更好)。

地址类型 address

solidity
address user = 0x1234567890123456789012345678901234567890;
address payable recipient = payable(user); // 可接收 ETH 的地址
  • 常见操作(转账示例)
solidity
// 调用方向 recipient 转账
recipient.transfer(1 ether);              // 转账,失败会自动 revert
bool success = recipient.send(1 ether);   // 返回 bool,需手动检查
require(success, "Send failed");

实战建议: 新项目中更推荐使用:
recipient.call{value: 1 ether}(""); 搭配检查返回值,便于处理复杂情况。

字节类型 bytes / bytesN

solidity
bytes1 singleByte = 0x01;  // 固定大小
bytes32 hashBytes;         // 常用来存储哈希
bytes dynamicBytes;        // 动态大小
  • 应用场景
    • bytes32:存储哈希值(如 keccak256 结果)
    • bytes:处理原始二进制数据

2. 引用类型(Reference Types)

数组 array

solidity
uint256[3] fixedArr;     // 固定长度
uint256[] dynamicArr;    // 可变长度
string[] stringArr;      // 字符串数组
  • 常用操作
solidity
dynamicArr.push(10);   // 添加元素
dynamicArr.pop();      // 移除末尾元素
uint256 len = dynamicArr.length;

结构体 struct

solidity
struct NFT {
    address owner;
    uint256 id;
    string metadataURI;
}
  • 使用方式
solidity
NFT memory newNFT = NFT({
    owner: msg.sender,
    id: 1,
    metadataURI: "ipfs://..."
});

映射 mapping

solidity
mapping(address => uint256) balances;
mapping(uint256 => string) idToName;
  • 特性
    • 所有可能的键默认映射到零值(不需要显式初始化)
    • 不支持遍历(需要额外维护键列表)

3. 特殊类型

枚举 enum

solidity
enum TradeStatus { Created, Filled, Cancelled }
TradeStatus status = TradeStatus.Created;

函数类型

solidity
function(uint256) external callbackFunc;

4. 数据存储位置速查

位置存储周期修改成本典型用途
storage永久合约状态变量
memory临时函数内部临时变量
calldata临时只读最低外部函数参数(只读)

5. 类型转换技巧

安全转换

solidity
uint256 big = 100;
uint64 small = uint64(big);  // 需确保值在目标范围内

地址处理

solidity
// 整型转地址
address addr = address(uint160(12345));
uint160 asInt = uint160(addr);

// 普通地址转 payable 地址
address payable payAddr = payable(addr);

6. 实用全局函数

solidity
// 类型极值
uint8 maxValue = type(uint8).max; // 255

// 字节操作
bytes memory merged = bytes.concat(bytes1(0x01), bytes1(0x02));

// 字符串拼接(0.8.12+)
string memory greeting = string.concat("Hello", " ", "World");

函数

1. 函数基础

Solidity 中的函数是智能合约的基本执行单元,用于封装一段可复用的业务逻辑。

1.1 基本语法

solidity
function functionName(parameterList)
    visibilityModifier
    stateMutability
    returns (returnType)
{
    // 函数体
}

常见组成:

  • 可见性(visibility)public / private / internal / external
  • 状态可变性(state mutability)pure / view / payable / 无(可修改状态)

1.2 示例

solidity
pragma solidity ^0.8.0;

contract SimpleFunction {
    // 简单函数
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    // 无参数函数
    function getMessage() public pure returns (string memory) {
        return "Hello, Solidity!";
    }
}

2. 函数可见性(visibility)

Solidity 提供了四种函数可见性修饰符:

  1. public:可以从任何地方访问(包括外部账户、其他合约、本合约内部)
  2. private仅当前合约内部可以访问
  3. internal:当前合约及其子合约可以访问
  4. external:只能从合约外部访问(内部访问需使用 this.xxx()

示例

solidity
contract VisibilityExample {
    function publicFunc() public pure returns (string memory) {
        return "Public function";
    }

    function privateFunc() private pure returns (string memory) {
        return "Private function";
    }

    function internalFunc() internal pure returns (string memory) {
        return "Internal function";
    }

    function externalFunc() external pure returns (string memory) {
        return "External function";
    }

    function testAccess() public pure {
        publicFunc();        // 可以访问
        privateFunc();       // 可以访问
        internalFunc();      // 可以访问
        // externalFunc();   // 不能这样直接调用,会报错
        this.externalFunc(); // 需要通过 this 调用 external 函数
    }
}

最佳实践:

  • 对仅内部使用的工具函数使用 internal / private,避免暴露不必要接口。
  • 对需要对外调用的接口使用 externalpublic,并搭配 modifier 做权限控制。

3. 状态可变性(state mutability)

函数可以声明它们如何与合约状态交互:

  1. pure:不读取也不修改状态(只依赖入参、只返回计算结果)
  2. view:仅读取状态,不修改
  3. 未指定:可以读取且修改状态
  4. payable:函数可以接收 ETH

示例

solidity
contract MutabilityExample {
    uint256 public value;

    // pure 函数:不读不写状态
    function add(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    // view 函数:读取状态,不修改
    function getValue() public view returns (uint256) {
        return value;
    }

    // 修改状态的函数
    function setValue(uint256 _value) public {
        value = _value;
    }

    // payable 函数:可接收 ETH
    function deposit() public payable {
        value += msg.value;
    }
}

实战建议: 尽量将纯计算逻辑标记为 pureview,有助于工具分析、Gas 估算以及安全审计。

4. 函数参数和返回值

4.1 参数

函数可以接受多个参数,每个参数需要指定类型和名称。

solidity
function registerUser(
    string memory name,
    uint256 age,
    address wallet
) public {
    // 业务逻辑
}

4.2 返回值(支持多返回值)

solidity
function calculate(uint256 a, uint256 b)
    public
    pure
    returns (uint256 sum, uint256 product)
{
    sum = a + b;
    product = a * b;
    // 也可以:return (a + b, a * b);
}

关键点: 返回多个值时可以命名返回变量,在函数体内直接赋值即可。

5. 特殊函数

5.1 构造函数 constructor

构造函数在合约部署时只执行一次,常用于初始化状态。

solidity
contract ConstructorExample {
    address public owner;
    uint256 public creationTime;

    constructor() {
        owner = msg.sender;
        creationTime = block.timestamp;
    }
}

5.2 接收 ETH 与回退函数:receive / fallback

当合约接收 ETH 或被调用不存在的函数时,会触发特殊函数:

  • receive():仅在 msg.data 为空且函数存在 时触发
  • fallback():用于处理:
    • 调用不存在的函数
    • msg.data 的 ETH 转入(或没有 receive() 时的纯 ETH 转入)

调用路径可以简化为:

是否接收 ETH?

  • 是 → msg.data 为空?
    • 是 → 存在 receive() → 调用 receive(),否则调用 fallback()
    • 否 → 调用 fallback()
solidity
contract FallbackExample {
    event Log(string message);

    // 接收 ETH 的专用函数(msg.data 为空时触发)
    receive() external payable {
        emit Log("Received Ether");
    }

    // 通用回退函数(函数不存在或有数据时触发)
    fallback() external payable {
        emit Log("Fallback called");
    }
}

安全提示:fallback / receive 中避免复杂逻辑,尽量只记录事件或做简单检查,防止被恶意多次调用造成风险。

6. 函数修饰器(简介)

说明: 本节有单独的 modifier 章节,下面只给一个快速示例。

修饰器可以用来复用前置检查逻辑,如权限控制、暂停开关等。

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

    constructor() {
        owner = msg.sender;
    }

    // 自定义修饰器
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _; // 继续执行函数体
    }

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    function pause() public onlyOwner {
        paused = true;
    }

    function unpause() public onlyOwner {
        paused = false;
    }

    function sensitiveAction() public whenNotPaused {
        // 重要操作
    }
}

7. 函数重载(overloading)

Solidity 支持函数重载,即同一作用域内可以有多个同名函数,但参数数量或类型不同

solidity
contract OverloadingExample {
    function getSum(uint256 a, uint256 b) public pure returns (uint256) {
        return a + b;
    }

    function getSum(uint256 a, uint256 b, uint256 c) public pure returns (uint256) {
        return a + b + c;
    }

    function getSum(string memory a, string memory b) public pure returns (string memory) {
        return string(abi.encodePacked(a, b));
    }
}

注意: 重载决议基于参数列表,与返回值类型无关。

8. 函数调用

8.1 内部调用

直接使用函数名调用同一合约内的其他函数。

solidity
function internalCallExample() public pure returns (uint256) {
    return _add(1, 2);
}

function _add(uint256 a, uint256 b) private pure returns (uint256) {
    return a + b;
}

8.2 外部调用(合约间调用)

调用其他合约的函数需要合约实例和接口。

solidity
contract OtherContract {
    uint256 public number = 42;

    function getNumber() public view returns (uint256) {
        return number;
    }
}

contract Caller {
    function callOtherContract(address _contractAddr) public view returns (uint256) {
        OtherContract other = OtherContract(_contractAddr);
        return other.getNumber();
    }
}

9. 常见安全模式

9.1 检查-效果-交互模式(Checks-Effects-Interactions)

推荐的安全写法顺序
1)检查(Checks) → 2)更新状态(Effects) → 3)与外部交互(Interactions)

solidity
function withdraw(uint256 amount) public {
    // 1. 检查
    require(balances[msg.sender] >= amount, "Insufficient balance");

    // 2. 效果(更新内部状态)
    balances[msg.sender] -= amount;

    // 3. 交互(外部调用 / 转账)
    payable(msg.sender).transfer(amount);
}

重要: 始终优先更新内部状态,再与外部合约交互,以降低重入攻击风险。

9.2 紧急停止模式(Circuit Breaker)

solidity
bool public stopped = false;

modifier stopInEmergency {
    require(!stopped, "Contract is stopped");
    _;
}

function emergencyStop() public onlyOwner {
    stopped = true;
}

function safeAction() public stopInEmergency {
    // 安全操作
}

用途: 当合约出现异常情况(被攻击、逻辑错误等)时,快速暂停关键操作,保护资金安全。

数据存储位置 storage|memory|calldata

Solidity 有三种主要的数据存储位置,理解它们的区别对编写高效、安全的智能合约至关重要。

1. 存储位置概述

  • 引用类型才能被修饰(storage|memory|calldata)
位置持久性修改性Gas 成本使用场景
storage永久存储可修改合约状态变量
memory临时内存可修改函数内部临时变量
calldata临时只读不可修改函数参数(外部调用)

2. storage(存储)

特点

  • 永久存储在区块链上
  • 相当于合约的"硬盘存储"
  • 读写操作消耗大量 gas
  • 所有合约状态变量默认存储在 storage

示例

js
contract StorageExample {
    uint public count; // 自动存储在storage

    function updateCount(uint newCount) public {
        count = newCount; // 修改storage变量
    }
}

关键点

  • 每次修改都会永久记录在区块链上
  • 存储槽优化可以节省 gas(连续小变量会打包到一个存储槽)

3. memory(内存)

特点

  • 临时存储,仅在函数执行期间存在
  • 函数调用结束后被清除
  • 比 storage 操作便宜很多
  • 用于函数内的局部变量

示例

js
function calculate(uint a, uint b) public pure returns (uint) {
    uint result = a + b; // result存储在memory
    return result;
}

特殊用法:数组和结构体

js
function processArray(uint[] memory arr) public {
    // 必须显式指定memory
    uint[] memory temp = new uint[](arr.length);
    // 操作memory数组...
}

4. calldata(调用数据)

特点

  • 只读的临时存储
  • 存储函数参数的原始调用数据
  • 最省 gas 的外部调用参数存储方式
  • 仅适用于 external 函数的参数

示例

js
function externalFunc(uint[] calldata data) external {
    // 只能读取data,不能修改
    uint first = data[0];
    // data[0] = 1; // 会报错!
}

最佳实践

  • 对于 external 函数,优先使用 calldata 而非 memory
  • 节省 gas(避免从 calldata 到 memory 的拷贝)

5. 数据位置规则

5.1 默认规则

  • 函数参数:
    • external 函数:默认 calldata
    • 其他函数:默认 memory
  • 引用类型局部变量:必须显式指定 memory 或 storage
  • 返回值:默认 memory

5.2 赋值行为

  • storage → memory:创建独立副本
  • memory → memory:创建引用(修改一个会影响另一个)
  • storage → storage:创建引用
  • 其他组合通常需要显式转换

6. 高级用法

6.1 storage 指针

js
contract PointerExample {
    struct User {
        uint id;
        string name;
    }

    User[] public users;

    function updateUser(uint index, string memory newName) public {
        User storage user = users[index]; // storage指针
        user.name = newName; // 直接修改原数据
    }
}

6.2 memory 与 calldata 性能对比

js
// 更高效(calldata)
function processData(uint[] calldata data) external {
    // 直接读取调用数据
}

// 较低效(memory)
function processData(uint[] memory data) public {
    // 需要先将calldata拷贝到memory
}

7. 常见错误

错误 1:未指定数据位置

js
function wrongFunc(uint[] arr) public {
    // 错误!引用类型必须指定memory或storage
}

错误 2:误用 storage 引用

js
function dangerousRef() public {
    uint[] storage arr; // 未初始化storage指针
    arr.push(1); // 会导致严重问题!
}

错误 3:错误地修改 calldata

js
function modifyCalldata(uint[] calldata data) external {
    data[0] = 1; // 错误!calldata不可修改
}

constantimmutable

1. constant(常量)

  • 特点

    • 值必须在 编译时 确定(硬编码在合约中)。
    • 不能通过构造函数或任何函数修改。
    • 存储在合约的 字节码 中,不占用存储插槽(storage),因此 Gas 成本更低
    • 适用于永远不会改变的固定值(如数学常数、固定配置)。
  • 语法

    js
    uint256 public constant MY_CONSTANT = 123;
    address public constant DEFAULT_ADDRESS = 0x123...;

2. immutable(不可变量)

  • 特点

    • 值可以在 构造函数 中设置一次,之后不能修改。
    • 存储在合约的 代码 中(类似 constant),但允许在部署时动态赋值。
    • constant 更灵活,但仍比普通状态变量更节省 Gas(因为不占用 storage)。
    • 适用于部署时确定但之后不再更改的值(如合约的创建者地址、初始化参数)。
  • 语法

    js
    uint256 public immutable maxSupply;
    address public immutable owner;
    
    constructor(uint256 _maxSupply) {
        maxSupply = _maxSupply;  // 只能在构造函数赋值
        owner = msg.sender;
    }

modifier(修饰器)

在 Solidity 中,modifier(修饰器)是一种特殊的函数,用于 在函数执行前或执行后添加检查或修改逻辑,类似于其他编程语言中的 装饰器(Decorator)AOP(面向切面编程)

它通常用于:

  • 权限控制(如 onlyOwner
  • 输入验证(如 nonReentrant 防重入)
  • 状态检查(如 whenNotPaused

定义修饰器

js
modifier minAmount(uint256 amount) {
    require(msg.value >= amount, "Amount too low");
    _;
}

function buyToken() public payable minAmount(1 ether) {
    // 必须至少发送 1 ETH
    // ...
}

_; 表示 原始函数的执行位置,可以放在 modifier 的开头、中间或结尾。

event(事件)

在 Solidity 中,event(事件)是一种 低成本的日志记录机制,允许智能合约在区块链上 存储特定的数据,供外部应用程序(如前端 DApp)监听和响应。

事件的主要用途:

  1. 记录重要状态变化(如转账、交易、所有权变更)。
  2. 节省 Gas 成本(比直接存储 storage 更便宜,事件(Event )每个大概消耗 2,000 gas;相比之下,链上存储一个新变量至少需要 20,000 gas。
  3. 提供链下可查询的数据(前端可以通过 Web3.js/ethers.js 监听事件)。

1. 基本语法

js
event Transfer(address indexed from, address indexed to, uint256 amount);
  • event 关键字声明一个事件。
  • indexed 表示该参数可以被 高效过滤(最多 3 个 indexed 参数)。

触发事件

js
function transfer(address to, uint256 amount) external {
    balances[msg.sender] -= amount;
    balances[to] += amount;
    emit Transfer(msg.sender, to, amount); // 触发事件
}
  • emit 关键字用于触发事件。

2. 事件的特点

(1) 数据存储位置

  • 事件数据存储在 交易日志(Logs) 中,而不是合约存储(storage),因此 Gas 成本更低
  • 适用于 不需要链上计算,但需要链下查询 的数据。

(2) indexed 参数的作用

  • indexed 参数允许 高效过滤,例如:
    javascript
    // 前端监听特定地址的转账
    contract.on("Transfer", { from: "0x123..." }, (event) => {
      console.log(event.args.to, event.args.amount);
    });
  • 最多 3 个参数 可以标记为 indexed

(3) 不可修改

  • 事件一旦触发,无法修改或删除(区块链不可篡改特性)。

3. 常见应用场景

(1) 记录代币转账(ERC20 标准)

js
event Transfer(address indexed from, address indexed to, uint256 value);

function _transfer(address from, address to, uint256 amount) internal {
    balances[from] -= amount;
    balances[to] += amount;
    emit Transfer(from, to, amount); // 记录转账
}

(2) 记录用户操作(如存款、取款)

js
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);

function deposit() public payable {
    balances[msg.sender] += msg.value;
    emit Deposit(msg.sender, msg.value); // 记录存款
}

function withdraw(uint256 amount) public {
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
    emit Withdraw(msg.sender, amount); // 记录取款
}

(3) 治理投票记录

js
event VoteCast(address indexed voter, uint256 proposalId, bool support);

function vote(uint256 proposalId, bool support) public {
    votes[msg.sender][proposalId] = support;
    emit VoteCast(msg.sender, proposalId, support); // 记录投票
}

4. 前端如何监听事件?

使用 ethers.js

javascript
const provider = new ethers.providers.Web3Provider(window.ethereum);
const contract = new ethers.Contract(contractAddress, abi, provider);

// 监听 Transfer 事件
contract.on("Transfer", (from, to, amount, event) => {
  console.log(`${from} sent ${amount} tokens to ${to}`);
});

// 过滤特定地址的转账
contract.on("Transfer", { from: "0x123..." }, (event) => {
  console.log("Filtered transfer:", event.args.to, event.args.amount);
});

使用 Web3.js

javascript
const web3 = new Web3(window.ethereum);
const contract = new web3.eth.Contract(abi, contractAddress);

// 监听所有 Transfer 事件
contract.events.Transfer({}).on("data", (event) => {
  console.log(event.returnValues);
});

继承

  • Solidity 支持多重继承,包括多级继承和多重继承。合约可以通过 is 关键字继承其他合约。

基本语法

js
contract Parent {
    // 父合约代码
}

contract Child is Parent {
    // 子合约代码
}

继承类型

1. 单继承

一个合约继承另一个合约。

js
contract A {
    function foo() public pure returns (string memory) {
        return "A";
    }
}

contract B is A {
    // 继承A的所有功能
}

2. 多级继承

继承链有多个层级。

js
contract Grandparent {
    // 祖父合约
}

contract Parent is Grandparent {
    // 父合约
}

contract Child is Parent {
    // 子合约
}

3. 多重继承

一个合约继承多个合约。

js
contract A {
    function foo() public pure returns (string memory) {
        return "A";
    }
}

contract B {
    function bar() public pure returns (string memory) {
        return "B";
    }
}

contract C is A, B {
    // 继承A和B的功能
}

继承规则

1. 构造函数执行顺序

构造函数按照从最基础到最派生的顺序执行。

js
contract A {
    constructor() {
        // 最先执行
    }
}

contract B is A {
    constructor() A() {
        // 然后执行
    }
}

2. 函数重写

子合约可以重写父合约的函数,需要使用 override 关键字。

js
contract A {
    function foo() public virtual pure returns (string memory) {
        return "A";
    }
}

contract B is A {
    function foo() public pure override returns (string memory) {
        return "B";
    }
}

3. 多重继承中的同名函数

当从多个父合约继承同名函数时,必须明确重写该函数。

js
contract A {
    function foo() public virtual pure returns (string memory) {
        return "A";
    }
}

contract B {
    function foo() public virtual pure returns (string memory) {
        return "B";
    }
}

contract C is A, B {
    function foo() public pure override(A, B) returns (string memory) {
        return "C";
    }
}

抽象合约

抽象合约包含至少一个未实现的函数(抽象函数)。

js
abstract contract Abstract {
    function foo() public virtual returns (uint);
}

contract Concrete is Abstract {
    function foo() public pure override returns (uint) {
        return 1;
    }
}