基础概念
数据类型
-
值类型(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