基础概念

数据类型

  • 值类型(Value Types):bool 布尔类型、uint 整数类型、address 地址类型等

  • 引用类型(Reference Types): 参考 引用类型

  • 映射类型(Mapping Types): mapping(k => v)

基本语法

  • pragma:版本声明

  • contract:合约声明

  • address:地址类型

  • uint:无符号整数类型

  • bool:布尔类型

  • mapping:映射类型

  • struct:结构体类型

  • event:事件类型

  • function:函数类型

  • modifier:修饰器类型

可见性

public external internal private
修饰函数 Yes Yes Yes Yes
修饰变量 Yes No Yes Yes
当前合约内可访问 Yes No Yes Yes
派生合约可访问 Yes No Yes No
外部访问 Yes Yes No No

变量

  • constant 定义常量, 定义的时候必须赋值, 变量的值会直接嵌入到字节码中,不会占用合约的存储空间

  • immutable 定义不可修改变量, 在构造函数中进行赋值,构造函数是在部署的时候执行,因此这是运行时赋值。immutable 变量的值存储在合约的存储中,但由于其在部署后不可改变,EVM 可以进行一些优化,使其行为类似于常量

函数

函数定义: `function + 函数名(参数列表) + 可⻅性 + 状态可变性(可多个)+ 返回值


function transfer(address to, uint256 value) public payable returns (bool success) {

// TODO

}

状态可变性:

  • view:用 view 修饰的函数,称为视图函数,它只能读取状态,而不能修改状态。

  • pure:用 pure 修饰的函数,称为纯函数,它既不能读取也不能修改状态。

  • payable:用 payable 修饰的函数表示可以接受以太币,如果未指定,该函数将自动拒绝所有发送给它的以太币

回调函数

  • receive: 有转账了,通知告诉合约一下

  • fallback: 找不到对应的方法时被调用,最后的保障

receive 和 payable 只适用于接收以太坊

继承、接口

  • is : 继承某合约

  • abstract: 表示该合约不可被部署

  • super:调用父合约函数

  • virtual: 表示函数可以被重写

  • override: 表示重写了父合约函数, 函数被重写后,父合约的函数就会被遮蔽, 不过可以使用 super 调用父合约函数

Solidity 支持多重继承, 直接在is后面接多个父合约即可,例如:


contract Sub is Base1, Base2 {

// 需要注意,如果多个父合约之间也有继承关系,那么 is 后面的顺序应该是越往上层级的父合约写在前面

}

在多重继承下,如果有多个父合约有相同定义的函数,在函数重写时,override 关键字后必须指定所有的父合约名


pragma solidity >=0.8.0;

  

contract Base1 {

function foo() virtual public {}

}

  

contract Base2 {

function foo() virtual public {}

}

  

contract Inherited is Base1, Base2 {

// 继承自隔两个父合约定义的foo(), 必须显式的指定override

function foo() public override(Base1, Base2) {}

}

函数修饰器

用于在函数执行前检查某种前置条件, 支持传递参数,支持多个修改器一起使用

修改器也是可被继承的,同时还可被继承合约重写(Override)


modifier checkAmount(uint256 amount) {

require (amount > 0, "amount must be greater than 0");

_;

}

函数修改器一般是带有一个特殊符号 _; 修改器所修饰的函数的函数体会被插入到_;的位置, 注意调用顺序

错误处理

参考: 错误处理

在以太坊上,每个交易都是原子操作,类似于在数据库里事务(transcation)一样,要么保证状态的修改要么全部成功,要么全部失败, 如果不做任何处理, 当 EVM 执行代码发生错误时, 就会回退整个交易,因此建议使用 requir revert asset 来检查各种可能的错误,并给出相应的错误提示


区分合约及外部地址

合约地址和外部地址在 EVM 层本质是一样的,都是有:nonce(交易序号)

balance(余额)storageRoot(状态)codeHash(代码), 区别在于外部 EOA 账户并没有 storageRoot(状态)codeHash(代码)

EVM提供了一个操作码EXTCODESIZE,用来获取地址相关联的代码大小(长度),如果是外部账号地址,则没有代码返回, 因此我们可以使用以下方法判断合约地址及外部账号地址:


function isContract(address addr) internal view returns (bool) {

uint256 size;

assembly { size := extcodesize(addr) }

return size > 0;

}

ABI 与底层调用

类似于 Java 中的 RPC 接口定义,其他合约可以根据 ABI 找到对应的函数方法,调用其他合约

使用地址的底层调用功能,是在运行时动态地决定调用目标合约和函数, 因此在编译时,可以不知道具体要调用的函数或方法,类似于 Java 中的反射

有 3 个底层的成员函数

  • targetAddr.call(bytes memory abiEncodeData) returns (bool, bytes memory)

  • targetAddr.delegatecall(bytes memory abiEncodeData) returns (bool, bytes memory)

  • targetAddr.staticcall(bytes memory abiEncodeData) returns (bool, bytes memory)

call 是常规调用,delegatecall 为委托调用,staticcall 是静态调用(不修改合约状态, 相当于调用 view 方法), call 与 delegatecall