Skip to content

Solidity

Solidity 简介

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

Solidity 基础语法

Solidity 是一种静态类型语言,语法类似于 JavaScript 和 C++。以下是 Solidity 的基础语法要点:

合约结构

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

contract MyContract {          // 合约定义
    // 状态变量
    uint public myNumber;
    
    // 构造函数
    constructor(uint initialNumber) {
        myNumber = initialNumber;
    }
    
    // 函数
    function setNumber(uint newNumber) public {
        myNumber = newNumber;
    }
    
    function getNumber() public view returns (uint) {
        return myNumber;
    }
}

Solidity 数据类型

1. 基础值类型

布尔值 (bool)

js
bool isActive = true;  // 仅存储 true/false
  • 逻辑运算!a, a && b, a || b
  • 比较运算==, !=

整数类型

js
int32 negative = -1;   // 有符号整数
uint256 large = 2**256 - 1;  // 无符号整数
  • 范围
    • intX: -2^(X-1) 到 2^(X-1)-1
    • uintX: 0 到 2^X-1
  • 最佳实践:优先使用 uint256/int256(EVM 原生优化)

地址类型

js
address user = 0x1234567890123456789012345678901234567890;
address payable recipient = payable(user); // 可支付地址
  • 关键操作
    js
    // 调用方给recipient转账
    recipient.transfer(1 ether);  // 转账(自动处理失败)
    bool success = recipient.send(1 ether);  // 转账(需检查返回值)

字节类型

js
bytes1 singleByte = 0x01;  // 固定大小
bytes dynamicBytes;        // 动态大小
  • 应用场景
    • bytes32:存储哈希值
    • bytes:处理原始二进制数据

2. 引用类型

数组

js
uint[3] fixedArr;       // 固定长度
uint[] dynamicArr;       // 可变长度
string[] stringArr;
  • 操作
    solidity
    dynamicArr.push(10);   // 添加元素
    dynamicArr.pop();      // 移除末尾元素

结构体

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

映射

js
mapping(address => uint) balances;
mapping(uint => string) idToName;
  • 特性
    • 所有可能的键默认映射到零值
    • 不支持遍历(需额外维护键列表)

3. 特殊类型

枚举

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

函数类型

js
function(uint) external callbackFunc;

4. 数据存储位置

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

5. 类型转换技巧

安全转换

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

地址处理

js
// 地址与整型互转
address addr = address(uint160(12345));
uint160 asInt = uint160(addr);

// 支付地址转换
address payable payAddr = payable(regularAddr);

6. 实用全局函数

js
// 类型极值
type(uint8).max;  // 255

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

// 字符串拼接
string.concat("Hello", " ", "World");

函数

1. 函数基础

Solidity 函数是智能合约中可执行的代码块,用于封装特定的功能逻辑。

1.1 基本语法

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

1.2 示例

js
pragma solidity ^0.8.0;

contract SimpleFunction {
    // 简单函数
    function add(uint a, uint b) public pure returns(uint) {
        return a + b;
    }
    
    // 无参数函数
    function getMessage() public pure returns(string memory) {
        return "Hello, Solidity!";
    }
}

2. 函数可见性

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

  1. public - 可以从任何地方访问(包括外部合约和交易)
  2. private - 只能在当前合约内部访问
  3. internal - 只能在当前合约和继承合约中访问
  4. external - 只能从合约外部访问(内部this.xx调用)

示例

js
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访问
    }
}

3. 状态可变性

函数可以声明它们如何与区块链状态交互:

  1. pure - 不读取也不修改状态
  2. view - 只读取状态但不修改
  3. 不指定 - 可以读取和修改状态
  4. payable - 可以接收以太币

示例

js
contract MutabilityExample {
    uint public value;
    
    // pure函数
    function add(uint a, uint b) public pure returns(uint) {
        return a + b;
    }
    
    // view函数
    function getValue() public view returns(uint) {
        return value;
    }
    
    // 修改状态的函数
    function setValue(uint _value) public {
        value = _value;
    }
    
    // payable函数
    function deposit() public payable {
        value += msg.value;
    }
}

4. 函数参数和返回值

4.1 参数

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

js
function registerUser(string memory name, uint age, address wallet) public {
    // 函数体
}

4.2 返回值

函数可以返回多个值,通过 returns 关键字声明返回类型。

js
function calculate(uint a, uint b) public pure returns(uint sum, uint product) {
    sum = a + b;
    product = a * b;
    // 也可以直接 return (a+b, a*b);
}

5. 特殊函数

5.1 构造函数

在合约部署时只执行一次的特殊函数。

js
contract ConstructorExample {
    address public owner;
    uint public creationTime;
    
    constructor() {
        owner = msg.sender;
        creationTime = block.timestamp;
    }
}

5.2 回退函数

当调用合约中不存在的函数时执行,或者向合约发送以太币但没有指定接收函数时执行。

js
contract FallbackExample {
    event Log(string message);
    
    // 接收以太币的回退函数
    receive() external payable {
        emit Log("Received Ether");
    }
    
    // 通用回退函数
    fallback() external {
        emit Log("Fallback called");
    }
}

6. 函数修饰器

修饰器可以用来修改函数的行为,常用于权限检查、输入验证等。

js
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. 函数重载

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

js
contract OverloadingExample {
    function getSum(uint a, uint b) public pure returns(uint) {
        return a + b;
    }
    
    function getSum(uint a, uint b, uint c) public pure returns(uint) {
        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 内部调用

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

js
function internalCallExample() public pure returns(uint) {
    return add(1, 2);
}

function add(uint a, uint b) private pure returns(uint) {
    return a + b;
}

8.2 外部调用

调用其他合约的函数需要合约实例和函数签名。

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

contract OtherContract {
    uint public number = 42;
    
    function getNumber() public view returns(uint) {
        return number;
    }
}

10. 常见模式

10.1 检查-效果-交互模式

js
function withdraw(uint amount) public {
    // 检查
    require(balances[msg.sender] >= amount, "Insufficient balance");
    
    // 效果
    balances[msg.sender] -= amount;
    
    // 交互
    payable(msg.sender).transfer(amount);
}

10.2 紧急停止模式

js
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 更便宜)。
  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;
    }
}