solidity学习笔记01

概念

Solidity 是一种静态类型的,面向合约的高级语言,用于在 Ethereum 平台上实现智能合约。

在 Solidity 中,状态变量 指合约中所有存储在区块链上的变量,定义在合约之内,函数之外。局部变量则是定义在函数之内。

Solidity 函数调用有两种方式:

  1. 内部调用(jump):像普通函数调用,参数用内存指针传递。
  2. 外部调用(EVM message call):通过 calldata 传递数据。

external 函数只支持外部调用,编译器知道参数只来自 calldata,所以可以直接读取 calldata 中的数据,不需要拷贝 → 节省 gas。

public 函数必须支持两种调用方式,所以编译器会强制把参数(尤其是数组、string、bytes)拷贝到 memory(即使是从外部调用也拷贝),这会产生 gas 开销。

solidity 有一些俗成的约定,会使用下划线 _ 区分状态变量和局部变量或者函数是否应该被外部调用,比如构造函数内部:

1
2
3
4
5
6
7
contract MyContract {
    uint256 private _value;

    constructor(uint256 value) {
        _value = value; // 将构造函数参数赋值给状态变量
    }
}

数据类型

值类型

  • bool:true | false,默认 false

  • int/uint:有符号、无符号整型,后面可以加数字,步长是 8,从 int8、uint8 到 int256、uint256,省略数字则默认 256,值 0。可以用 type(X).min 和 type(X).max 来访问该类型代表的最小值和最大值。

  • fixed/ufixed:有符号、无符号的定长浮点型。Solidity 还没有完全支持定长浮点型,可以声明但不能赋值。在关键字 ufixedMxN 和 fixedMxN 中, M 表示该类型占用的位数, N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixed 和 fixed 分别是 ufixed128x18 和 fixed128x18 的别名。

  • address:地址类型,仅能存储 20 字节的地址。

  • address payable:地址类型,有额外的 .transfer() 和 .send() 方法,可接收 ETH,如:

    1
    2
    3
    4
    5
    6
    7
    8
    
    address addr = 0xAbC...;
    address payable payableAddr = payable(addr); // 转换为 payable
    
    //属性:
    addr.balance 该地址的ETH余额,单位是wei
    addr.code 合约字节码(bytes memory),若为 EOA 则为空
    addr.code.length 判断是否为合约(>0 表示是合约)
    addr.codehash 合约代码的 keccak256 哈希(bytes32
    
  • bytes1-32:定长字节数组,从 bytes1 到 bytes32,支持索引,有长度。每个单独的元素将被初始化为与其类型相应的默认值。

    如果要把一个 32 字节的变量转成 address,则需要 address(uint160(bytes20(b))),因为 address 是 20 字节,因此要先截取前 20 字节,并通过数值类型显示转换,20 字节就是 160 位数字。

  • enum:枚举类型,按定义顺序从 0 开始自动分配整数值。

    1
    2
    3
    4
    
    enum Status { Pending, Approved, Rejected }
    // Pending → 0
    // Approved → 1
    // Rejected → 2
    

引用类型

引用类型使用时都需要加上 storage、memory 或 calldata。

storage:持久化存储在区块链上(合约状态变量)

memory:临时存储在内存,只在函数执行期间存在,函数结束即销毁。适合临时数据,gas 消耗较低。(函数内部使用)

calldata:只读的调用数据,通常用于外部函数参数,从交易数据中读取,不分配新内存,节省 gas。但构造函数参数不能使用 calldata

  • Array:数组,分为定长数组和动态数组,比如 int[5]和 int[ ]

  • struct:结构体

    1
    2
    3
    4
    
    struct Person {
        string name;
        uint age;
    }
    
  • mapping:映射,也就是哈希 map,仅能声明为 storge,只能作为状态变量,不能作为局部变量或函数出入参。key 只能是值类型,value 无限制,key 不存在时默认返回值是 0。mapping 没有长度,无法遍历,也不能作为函数参数传递。

  • string:动态长度的字符串,底层是 bytes,不支持索引,但是可以通过 bytes(string)转成 bytes 类型。默认值是一个字符串。

  • bytes:变长字节数组,默认值是空数组。

  • 合约类型,每个合约定义后,其名称本身就是一个类型。

    1
    2
    3
    4
    5
    6
    
    //如果 Child 继承自 Parent,那么 Child 实例可隐式转为 Parent 类型:
    Parent p = new Child();
    
    //任何合约实例都可以显式转为 address:
    MyToken token = new MyToken();
    address addr = address(token);
    

继承

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 父合约
contract Parent {
    uint public parentVar = 10;
    function parentFunc() public pure returns (string memory) {
        return "Hello from Parent";
    }
}

// 子合约继承父合约
contract Child is Parent {
    function childFunc() public pure returns (string memory) {
        // 直接访问父合约的变量和函数
        uint value = parentVar;
        return parentFunc();
    }
}

Solidity 支持多重继承。当多个父合约有同名函数时,子合约必须使用 override 关键字明确指定要重写的函数来源。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
contract Base1 {
    function greet() public pure virtual returns (string memory) {
        return "Hello from Base1";
    }
}

contract Base2 {
    function greet() public pure virtual returns (string memory) {
        return "Hello from Base2";
    }
}

contract Derived is Base1, Base2 {
    function greet() public pure override(Base1, Base2) returns (string memory) {
        return "Hello from Derived";
    }
}

函数

构造函数

在合约部署时只执行一次,用于初始化状态变量(如设置所有者、初始参数等)。

特点:

  • 只能有一个构造函数

  • 不能有返回值

  • 不能被外部调用

  • 如果没有显示定义则有一个空的默认构造函数

从 Solidity 0.7.0 起,不再使用 function ContractName() 的方式定义构造函数,必须使用 constructor 关键字,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pragma solidity ^0.8.0;

contract MyToken {
    address public owner;
    string public name;

    // 构造函数
    constructor(string memory _name) {
        owner = msg.sender;   // 部署者设为所有者
        name = _name;         // 初始化代币名称
    }
}

普通函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function transfer(address to, uint amount) public {

  //require,一般用于外部输入的条件检查,如果不满足condition条件则:
  //1. 立刻终止当前函数执行
  //2. 回滚所有状态更改
  //3. 退还剩余gas,已消耗的不退
  //4. 最终返回一条错误信息
  require(condition, "可选的错误消息");

  //如果assert失败说明代码存在bug
  //1. 回滚不退gas
  //2. 抛出操作码
  assert(condition)

  //立即回滚 + 退还未用 gas + 返回错误消息
  if(...){
    revert("msg");
  }
}

函数可见性修饰符

  • public:public 修饰的函数或状态变量可以从合约内外被访问。对于状态变量,编译器会自动生成一个同名的外部 getter 函数。

  • private:private 修饰的函数或状态变量仅能在其合约内部访问。即使是继承它的子合约也无法访问。

  • internal:internal 修饰的函数或状态变量可以在当前合约内部以及所有继承自它的子合约内部被访问。

  • external:external 修饰的函数只能从合约外部被调用,不能在合约内部直接调用。它通常用于定义合约的对外接口,在处理大型数据结构(如数组、字符串)时比 public 更节省 Gas。

状态修饰符

状态修饰符向 EVM 声明函数如何与区块链状态交互,直接影响交易执行和 Gas 消耗

  • pure:纯计算函数,既不读也不写区块链的状态,只能进行数学运算或处理传入参数,无法访问 block.timestamp、msg.sender 或任何存储变量。执行时使用 STATICCALL,Gas 成本最低。

    1
    2
    3
    
    function add(uint a, uint b) public pure returns (uint) {
        return a + b; // 仅计算,不接触状态
    }
    
  • view:只读函数,可以读取状态变量但禁止修改状态变量。为了实现不修改,EVM 提供了 STATICCALL 操作码,在 STATICCALL 执行期间,任何尝试修改状态的操作都会导致 revert,这是运行时(runtime)级别的保护。

  • payable:可接收 ETH 函数,允许函数在调用时接收以太币

pure/view 的误用:声明为 pure 或 view 的函数如果实际上读取或修改了状态,编译不会报错,但调用时会回滚。务必确保声明与实际行为一致。 payable 的必要性:任何需要接收 ETH 的函数都必须标记 payable,否则发送的 ETH 会被退回,交易失败。

如何获取合约的对象

  1. 用 new 在当前交易中部署一个全新的合约实例到区块链上。这会执行构造函数并返回新合约的地址。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 基础合约
contract Token {
    string public name;

    constructor(string memory _name) {
        name = _name;
    }
}

// 工厂合约:使用new创建新实例
contract TokenFactory {
    Token public newToken;

    function createToken(string memory _name) public {
        // 使用new创建全新Token合约实例
        newToken = new Token(_name);
        // newToken现在指向新部署的合约地址
    }
}
  1. 用 ContractName(address)类型转换来获取已有合约对象引用。这不会创建新合约,而是与已部署的合约交互。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 已有Token合约部署在区块链上
contract Token {
    function transfer(address to, uint amount) public;
}

// 用户合约:引用现有Token实例
contract UserContract {
    Token public existingToken;

    function setTokenAddress(address tokenAddress) public {
        // 通过地址引用已部署的Token合约
        existingToken = Token(tokenAddress);
        // existingToken现在指向区块链上已有的合约
    }

    function sendTokens(address recipient, uint amount) public {
        // 调用引用合约的方法
        existingToken.transfer(recipient, amount);
    }
}

receive 函数

receive 是特殊函数关键字,一个合约最多可以有一个 receive 函数。交易仅发送 ETH,且 calldata 为空(msg.data.length == 0)时触发。

1
2
3
4
//没有 function 关键字
receive() external payable {
    // 必须标记为 payable
}

receive 优先级高于 fallback,举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//场景 1:用户直接向合约转账 ETH(无附加数据)
to: 0xContract...
value: 1 ether
data: 0x (空)
/**
→ 触发 receive()(如果存在)
→ 若无 receive(),则尝试触发 payable fallback()
→ 若都没有 → 交易 revert
 */


//场景 2:用户调用一个不存在的函数
to: 0xContract...
value: 0
data: 0x12345678 (无效函数选择器)
/**
→ 触发 fallback()(无论是否 payable)
→ 如果 fallback 不是 payable,但交易带 ETH → revert
 */

//场景 3:用户调用合约的某个函数(如 transfer(...))
//→ 正常执行该函数,不会触发 receive/fallback

fallback 函数

fallback 是特殊函数关键字,一个合约最多可以有一个 fallback 函数。交易 calldata 非空,但不匹配任何函数时触发。

1
2
3
4
5
//没有 function 关键字
fallback() external payable {
    // 可接收 ETH
    // payable 可选
}

modifier(修饰器)

用于复用校验逻辑(如权限控制)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//这个合约片段实现了一个 “仅限所有者提款” 的功能

//用modifier声明了onlyOwner函数
modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    // _; 是一个占位符,表示“在这里插入被修饰的函数体”。
    _;
}

//当调用 withdraw()函数时,会先触发onlyOwner()校验,校验通过后才会执行payable(msg.sender)...
function withdraw() public onlyOwner {
    payable(msg.sender).transfer(address(this).balance);
}

函数选择器

函数选择器是一个 4 字节的标识符,由函数签名(函数名 + 参数类型)经 Keccak256 哈希后取前 4 字节生成。例如:

1
2
3
4
5
6
function transfer(address to, uint256 amount) external;
/**
函数签名字符串:"transfer(address,uint256)"
Keccak256 哈希:0xa9059cbb2...
函数选择器:0xa9059cbb(前 4 字节)
 */

当外部账户或合约调用某个函数时,必须在交易的 calldata 开头放入这个 4 字节选择器,EVM 才知道要调用哪个函数。

一些全局变量

  • msg:一个内置的全局结构体(struct),由以太坊虚拟机(EVM)在每次外部调用时自动提供,无需声明或初始化,在任何函数中都可以直接访问,它包含了当前函数调用的上下文信息。

  • block:提供当前区块的信息

ABI 接口

Application Binary Interface,应用二进制接口。相当于接口说明书,java 的 swagger 文档,json 格式,包含了:

  • 这个合约有哪些可调用的函数?
  • 每个函数的名字、参数类型、返回值类型是什么?
  • 还有哪些事件(Events)、错误(Errors)可以被触发或抛出?

为什么需要 ABI?

EVM 只认识字节码,而字节码是一串人类看不懂的二进制字符串,因此 ABI 可以通过编码把人类可读的函数调用(如 transfer(address to, uint amount))转成 EVM 能懂的二进制数据,可以解码把合约返回的二进制数据转回人类可读的结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[
	{
		"inputs": [
			{ "internalType": "address", "name": "to", "type": "address" },
			{ "internalType": "uint256", "name": "amount", "type": "uint256" }
		],
		"name": "transfer",
		"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"internalType": "address",
				"name": "from",
				"type": "address"
			},
			{
				"indexed": true,
				"internalType": "address",
				"name": "to",
				"type": "address"
			},
			{
				"indexed": false,
				"internalType": "uint256",
				"name": "value",
				"type": "uint256"
			}
		],
		"name": "Transfer",
		"type": "event"
	}
]
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计