Solidity文档
文档
1.基础
pragma 编译指令
pragma solidity >=0.4.16 <0.9.0;
告诉编译器如何处理源代码指令
Solidity杂项
solidity合约的含义就是一组代码(函数)和数据(状态),他们位于特定地址上。
合约中访问一个状态变量不需要像this.
的前缀
入门智能合约
简单的智能合约
子货币合约(Subcurrency)
https://learnblockchain.cn/docs/solidity/introduction-to-smart-contracts.html#subcurrency
币可以无限产生,但只有创建者可以;任何人都可以给其他人转币;
address public minter;
这一行声明了一个可以被公开访问的 address 类型的状态变量。address 类型是一个160位的值,且不允许任何算数操作。
mapping (address => uint) public balances;
创建一个公共状态变量
这个类型将address映射为无符号整数
mappings可以看成一个哈希表,它会执行虚拟初始化,以使所有可能存在的键都映射到一个字节表示为全零的值。但是它不能获得映射的所有键的列表,也不能获取所有值的表。 因此,要么记住你添加到mapping中的数据(使用列表或更高级的数据类型会更好),要么在不需要键列表或值列表的上下文中使用它。
编译器会自动为所有public状态变量创建getter函数。使用getter函数可以查到账户余额
event Sent(address from, address to, uint amount);
这行声明了一个所谓的“事件(event)”,它会在 send 函数的最后一行被发出。如下。
点击查看代码
function send(address receiver, uint amount) public {
...
emit Sent(msg.sender, receiver, amount);
}
点击查看代码
Coin.Sent().watch({}, '', function(error, result) {
if (!error) {
console.log("Coin transfer: " + result.args.amount + " coins were sent from " + result.args.from + " to " + result.args.to + ".");
console.log("Balances now:\n" +"Sender: " + Coin.balances.call(result.args.from) +"Receiver: " + Coin.balances.call(result.args.to));
}
})
合约中真正被其他用户或合约调用的功能是mint和send。
点击查看代码
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
require(amount < 1e60);
balances[receiver] += amount;
}
function send(address receiver, uint amount) public {
require(amount <= balances[msg.sender], "Insufficient balance.");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
如果使用合约发币,区块链浏览器上是看不到任何信息的,因为实际上发币和更改余额的信息仅仅存在合约的数据存储器中,但通过事件event可以创建一个区块链浏览器来最终交易和余额。
区块链基础
交易
区块
为了应对双花,比特币可以自动选择一条交易序列,打包到区块中。未被选择的将会拒绝,不包含在区块中。块和块按时间形成了一个线性序列,正是区块链。区块以一定时间间隔添加到链上,对于以太坊,这个时间间隔是17s。
EVM
EVM是智能合约的运行环境,是封装的、完全隔离的,在EVM中运行的代码无法访问网络、文件系统和其他进程,智能合约间的访问也会受限。
账户
EOA和合约账户,他们对于EVM没有区别
EOA的地址是公钥决定的,合约账户的地址是创建合约时确定的,是通过合约创建者的地址和从地址发出过的交易数量(nonce)计算得到的。
每个账户都有一个键值对形式的持久化存储(key和value长度都是256),每个账户都有一个以太坊余额(balance,单位是Wei,10^18Wei=1ether)
交易
交易有四种形式
目标账户含有代码,则代码会被执行,以payload传参
目标账户是零账户,交易将创建合约。携带的代码(即payload)将被转换为EVM字节码并执行,输出将作为合约代码被永久存储。这意味着,创建一个合约,不需要发送实际的合约代码,而是要发送能够产生合约代码的代码。
在合约创建的过程中,代码还是空的,直到构造函数执行结束,都不能调用合约中自己的函数。
Gas
gas_price是发送者设置的(每笔交易中,gaslimit和gasprice需要指定。为了保证交易顺利,gaslimit往往会设置的大些。gasprice默认1gas=0.05e12Wei =20Gwei)
预付手续费(gas_limit/startgas)=gas_price * gas
gas一旦耗尽,会触发out-of-gas的异常,当前调用帧(call frame 指EVM运行栈(stack)中当前操作所需要的元素)
存储、内存、栈(字:word)
每个账户有一块持久化内存区域:存储,是一种256位字-->256位字的键值存储区。
合约枚举所有存储是不可能的,读存储和修改存储的开销都很高,合约只能读写存储区中属于自己的部分。
还有一个存储区称为内存。
合约会试图为每次消息调用获取一块内存实例。
内存是线性的,可按字节寻址,读的长度为256,写的长度为8/256。访问未访问过的内存将会按字进行扩容(256位/字),消耗gas。
EVM不基于以上这种寄存器,而是基于栈,所有的计算都在一个叫做栈(stack)的区域执行。栈最大有1024个元素,每个元素长度是一个字。
访问栈仅限于顶端:允许拷贝最顶端的16个元素中的一个到栈顶,或交换栈顶元素和下面16个元素之一。运算后结果压入栈顶。无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素。
指令集
EVM指令集应尽可能少,所有指令都是针对“256位的字(word)”,这个基本的数据类型进行操作。
消息调用message call
合约可以通过消息调用的方式调用其他合约或发送以太到其他非合约账户。
每个交易都由一个顶层消息调用组成,这个消息调用又可以创建更多的消息调用。
当合约内部消息调用时发生outofgas异常,将由一个压入栈顶到错误值指明。
Solidity中,发起调用的合约默认出发一个手工异常,以便异常可以从栈里冒泡出来。
消息调用合约时,被调用的合约会获得一块清理过的内存,并可以访问调用的payload(由一种叫做calldata的独立区域提供的数据)。调用执行后,返回数据被存放在调用预先分配好的内存中,调用深度为1024.
面对复杂操作,尽量使用循环而非递归。
委托调用/代码调用和库delegatecall
这是一种特殊的消息调用。和一般的messagecall的区别是,目标地址的代码将在上下文中执行,切msg.sender & msg.value保持不变。
这意味着合约可以在运行时从另一个地址动态的加载代码。 存储、当前地址和余额都指向发起调用的合约,只有代码是从被调用地址上获取的。这赋予了Solidity实现库的能力:将可复用的代码库放在一个合约的存储起来。
日志logs
日志是一种特殊的可索引的数据结构,其存储的数据可以一路映射直到区块层级。
Solidity用logs来实现事件(events)。
合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效访问。引文部分日志数据被存储在布隆过滤器(bloom filter)中。我们也可以高效并加密安全的搜索日志,轻客户端也可以找到日志。
合约创建
通过一个特殊的指令来创建其他合约(不是简单的调用零地址)。
创建合约的调用create calls和普通消息的区别在于,payload会被执行,执行的结果被存储为合约代码,创建者在栈上得到新合约的地址。
失效和自毁selfdestruct
合约代码从区块链移除的唯一操作是执行自毁。
合约账户上剩余的以太会发送给指定目标,然后其存储和代码从状态中被移除。(如果有人发送以太到移除的合约,以太将永久失去)
合约代码可以不显式的调用selfdestruct,而是通过delegatecall或callcode的方式自毁。
如果要使合约失效,则应通过更改内部状态来禁用合约,这样可以在使用函数无法执行从而进行 revert,从而达到返还以太的目的。
旧合约的删减可能会,也可能不会被以太坊的各种客户端程序实现。另外,归档节点可选择无限期保留合约存储和代码。
EOA不能从状态中移除。
安装Solidity编译器
https://learnblockchain.cn/docs/solidity/installing-solidity.html
通过例子学习Solidity
投票合约
https://learnblockchain.cn/docs/solidity/solidity-by-example.html#id1
秘密竞价合约
https://learnblockchain.cn/docs/solidity/solidity-by-example.html#index-1
安全的远程购买合约
https://learnblockchain.cn/docs/solidity/solidity-by-example.html#index-2
微支付通道合约
https://learnblockchain.cn/docs/solidity/solidity-by-example.html#id7
使用库合约
https://learnblockchain.cn/docs/solidity/solidity-by-example.html#index-3
Solidity
Solidity源文件
SPDX(The Software Package Data Exchange)
SPDX规范是一项国际开发标准,所有的源文件都要使用一个SPDX注释来表示其许可证。
https://spdx.dev/ids/#how
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: GPL-3.0-or-later
如果不想添加许可证,用特殊值:UNLICENSED
Pragma
- 版本Pragma
pragma solidity ^0.5.2;
- ABI Coder Pragma
pragma experimental ABIEncoderV2;
- 实验性功能标注(新的编译器功能或语法特性)
import
Comments
//
/* */
合约结构
状态变量(uint ...)
uint storedXlbData; // 状态变量
函数(function)
点击查看代码
function Mybid() public payable { // 定义函数
// ...
}
变量类型
solidity中要指定变量的类型。
solidity中没有undifined
和null
值的概念,但新变量有一个跟类型相关的默认值
要处理任何意外的值,应该使用错误处理来恢复整个交易,或者返回一个带有第二个 bool 值的元组表示成功。
值类型
bool类型
bool
- 运算符 || 和 && 都遵循同样的短路( short-circuiting )规则:
就是说在表达式 f(x) || g(y) 中, 如果 f(x) 的值为 true ,那么 g(y) 就不会被执行,即使会出现一些副作用。
整型
int:有符号整型变量 uint:无符号整型变量
int(uint)默认代表int256(uint256),其余也可以定义从int8~int256(步长为8)。
type(X).min
可以获取整型X的最小值
✨uint32类型的取值范围是0~2**32-1
,int32类型的取值范围是-2**32~2**32-1
✨0.8.0 开始,算术运算有两个计算模式:一个是 “wrapping”(截断)模式或称 “unchecked”(不检查)模式,一个是”checked” (检查)模式(默认checked)。算术运算在 “checked” 模式下,即都会进行溢出检查,如果结果落在取值范围之外,调用会通过 失败异常 回退。 你也可以通过 unchecked { ... }
切换到 “unchecked”模式
- 运算符:
比较运算符(返回bool)、位运算符(^
异或,~
位取反)、移位运算符、算数运算符(**
幂) - 比较运算:
- 位运算:
位运算在数字的二进制补码表示上执行。 这意味着: ~int256(0)== int256(-1)。 - 移位:
- 加减乘:
如果有int x = type(int).min;
, 那 -x 将不在正数取值的范围内。 这意味着这个检测unchecked { assert(-x == x); }
是可以通过的,如果是 checked 模式,则会触发异常。 - 除:
除法结果始终是操作数类型。
int256(-5) / int256(2) == int256(-2)
字面常量上进行除法会保留精度
除以0会触发panic异常,但可以通过不检查模式禁用
type(int).min / (-1)
是仅有的整除会发生向上溢出的情况。 在检查模式下,这会触发一个失败异常,在截断模式下,表达式的值将是type(int).min
。 - 模运算(取余):
int256(5) % int256(2) == int256(1) int256(5) % int256(-2) == int256(1) int256(-5) % int256(2) == int256(-1) int256(-5) % int256(-2) == int256(-1)
对0取模会发生错误 Panic 错误,不检查模式不能解决 。 - 幂运算:
仅适用于无符号
定长浮点型
fixed / ufixed
:表示各种大小的有符号和无符号的定长浮点型。
在关键字 ufixedMxN 和 fixedMxN
中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。
ufixed 和 fixed 分别是 ufixed128x19 和 fixed128x19 的别名。
✨浮点型(float or double)和定长浮点型
浮点型整数部分和小数部分需要的位数可以灵活变化,但定长浮点数长度受到严格的规定。
地址型Address
address & address payable
address 保存一个20字节的值(以太坊地址的大小)。
address payable有成员函数transfer
和send
address payable 可以接受以太币的地址,而一个普通的 address 则不能。
✨类型转换:
- 允许address payable 到 address 的隐式转换,而从 address 到 address payable 必须显示的转换, 通过
payable(<address>)
进行转换 - address 允许和 uint160、 整型字面常量、bytes20 及合约类型相互转换。
✨地址类型成员变量: - balance & transfer:查询 & 转账(transfer失败会终止)
- send:transfer的低级版,失败不会终止,会返回false
- call、delegatecall、staticcall:
为了与不符合ABI的合约交互,或者要更直接地控制编码,提供了函数call,delegatecall 和 staticcall
。 它们都带有一个bytes memory
参数和返回执行成功状态(bool)和数据(bytes memory)。
call、delegatecall、staticcall都是低级函数,只用作最后一招。
合约类型
每一个 contract 定义都有他自己的类型。
可以隐式地将合约转换为从他们继承的合约。 合约可以显式转换为 address
类型。
只有当合约具有 接收receive函数 或 payable 回退函数
时,才能显式和 address payable
类型相互转换 转换仍然使用 address(x)
执行, 如果合约类型没有接收或payable 回退功能,则可以使用 payable(address(x))
转换为 address payable
。
合约和 address 的数据表示是相同的
合约不支持任何运算符
合约类型大的成员是合约外部函数和public的状态变量
对于合约 C
可以使用type(C)
获取合约的类型信息,
定长字节数组
bytes1,bytes2......,bytes32
- 成员变量:.length返回长度
可以将byte[]
当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时,每个元素会浪费 31 字节。 更好地做法是使用 bytes。 - 比较运算、位运算、移位运算、索引访问
变长字节数组
- bytes: 变长字节数组,参见 数组。它并不是值类型。
- string: 变长 UTF-8 编码字符串类型,参见 数组。并不是值类型。
字面常量:int myAge = 24; 这里面myAge是整型变量,24是字面常量
地址字面常量
比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF
这样的通过了地址校验和测试的十六进制字面常量会作为 address
类型。 而没有通过校验测试, 长度在 39 到 41 个数字之间的十六进制字面常量,会产生一个错误,您可以在零前面添加(对于整数类型)或在零后面添加(对于bytesNN类型)以消除错误。
有理数和整数字面常量
Solidity 对每个有理数都有对应的数值字面常量类型。 整数字面常量和有理数字面常量都属于数值字面常量类型。 除此之外,所有的数值字面常量表达式(即只包含数值字面常量和运算符的表达式)都属于数值字面常量类型。 因此数值字面常量表达式 1 + 2 和 2 + 1 的结果跟有理数三的数值字面常量类型相同。
数值字面常量表达式只要在非字面常量表达式中使用就会转换成非字面常量类型(数值字面常量支持任意精度,但出现在变量表达式中会发生转变,精度跟随变量)。
在下面的例子中,尽管我们知道 b 的值是一个整数,但 2.5 + a 这部分表达式并不进行类型检查,因此编译不能通过。
uint128 a = 1; uint128 b = 2.5 + a + 0.5;
字符串字面常量和类型
字符串字面常量是指由双引号或单引号引起来的字符串("foo" 或者 'bar')。 它们也可以分为多个连续的部分("foo" "bar" 等效于“f””fobar”),这在处理长字符串时很有用。
“foo”相当于三个字节而不是四个。
和整数字面常量一样,字符串字面常量也可以发生改变。
字符串字面常量可以隐式转化为bytes1,……,bytes32,
如果合适的话,还可以转换成 bytes
以及 string
。例如:bytes32 samevar = "stringliteral"
字符串字面常量在赋值给 bytes32 时被解释为原始的字节形式。
字符串字面常量只能包含可打印的ASCII字符,这意味着他是介于0x1F和0x7E之间的字符。
字符串字面常量支持下面的转义字符:\<newline> (转义实际换行) \\ (反斜杠) \' (单引号) \" (双引号) \b (退格) \f (换页) \n (换行符) \r (回车) \t (标签 tab) \v (垂直标签) \xNN (十六进制转义,见下文) \uNNNN (unicode 转义,见下文)
Unicode字面常量
string memory a = unicode"Hello 😃";
十六进制字面常量
hex"00112233" hex"44556677"
等同于 hex"0011223344556677"
枚举类型
枚举是在Solidity中创建用户定义类型的一种方法。 它们是显示所有整型相互转换,但不允许隐式转换。 从整型显式转换枚举,会在运行时检查整数时候在枚举范围内,否则会导致异常( Panic异常 )。 枚举需要至少一个成员,默认值是第一个成员,枚举不能多于 256 个成员。
函数类型
内部(internal) 函数类型
外部(external) 函数类型
内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。 调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。
外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]
- 类型转换(pure、constant、view、payable):
函数类型 A 可以隐式转换为函数类型 B 当且仅当: 它们的参数类型相同,返回类型相同,它们的内部/外部属性是相同的,并且 A 的状态可变性并不比 B 的状态可变性更具限制性,比如:pure 函数可以转换为 view 和 non-payable 函数、view 函数可以转换为 non-payable 函数、payable 函数可以转换为 non-payable 函数。 - 关于payable和non-payable:
如果一个函数是 payable ,这意味着它 也接受零以太的支付,因此它也是 non-payable 。
另一方面,non-payable 函数将拒绝发送给它的 以太币Ether , 所以 non-payable 函数不能转换为payable 函数。 - 如果当函数类型的变量还没有初始化时就调用它的话会引发一个 Panic 异常。 如果在一个函数被 delete 之后调用它也会发生相同的情况。
- 当前合约的 public 函数既可以被当作内部函数也可以被当作外部函数使用。如果想将一个函数当作内部函数使用,就用
f
调用,如果想将其当作外部函数,使用this.f
。
成员方法:
public(或 external)函数都有下面的成员:
.address
返回函数的合约地址。
.selector
返回 ABI 函数选择器
引用类型
目前,引用类型包括结构,数组和映射。
如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:
- 内存(memory):
数据仅在其生命周期内(函数调用期间有效),不能用于外部调用 - 存储(storage):
状态变量保存的位置,只要合约存在就一直存储 - 调用数据(calldata):
用来保存函数参数的特殊数据位置,是一个只读位置,可用于任何函数
数据位置
所有的引用类型,都要额外注释一个数据位置:memory、storage、calldata。
其中调用数据(calldata)是不可修改的、非持久的函数参数存储区域
调用数据calldata 是外部函数的参数所必需指定的位置,但也可以用于其他变量。
如果可以的话,请尽量使用 calldata 作为数据位置,因为它将避免复制,并确保不能修改数据。 函数的返回值中也可以使用 calldata 数据位置的数组和结构,但是无法给其分配空间。
数据位置和赋值行为
数据位置不仅影响着数据如何保存还影响着赋值行为
- 在storage和memory之间俩俩赋值(或从calldata赋值),会创建一份独立的备份。
- 从memory到memory的赋值只创建引用,这意味着改变内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。
- 从storage到本地存储变量的赋值也只分配一个引用
- 其他的向storage的赋值,总是进行拷贝。
数组
数组可以指定长度(T[k]),也可以动态调整大小(T[])
✨举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组(二维数组),应声明为 uint[][5]
(注意这里跟其它语言比,数组长度的声明位置是反的)。作为对比,如在Java中,声明一个包含5个元素、每个元素都是数组的方式为 int[5][]
。P.S. 虽然声明时数组长度的声明位置是反的,但访问时却是正的:如果有一个变量为 uint[][5] memory x,
要访问第三个动态数组的第二个元素,使用 x[2][1]
,要访问第三个动态数组使用 x[2]
。
✨在Solidity中,X[3]
总是一个包含三个 X 类型元素的数组,即使 X 本身就是一个数组,这和其他语言也有所不同,比如 C 语言。
数组元素可以是任何类型,包括映射或结构体。对类型的限制是映射只能存储在 存储storage 中,并且公开访问函数的参数需要是 ABI 类型。
状态变量标记 public 的数组,Solidity创建一个 getter函数 。 小标数字索引就是 getter函数 的参数。
访问超出数组长度的元素会导致异常(assert 类型异常 )。 可以使用 .push() 方法在末尾追加一个新元素,其中 .push() 追加一个零初始化的元素并返回对它的引用。
✨bytes&strings:是特殊的数组,bytes 类似于 byte[],但它在 调用数据calldata 和 内存memory 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 string 与 bytes 相同,但不允许用长度或索引来访问。
Solidity没有字符串操作函数,但是可以使用第三方字符串库,我们可以比较两个字符串通过计算他们的 keccak256-hash ,可使用 keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))
或者使用 abi.encodePacked(s1, s2)
来拼接字符串。
ABI,application binary interface,如名字所示,是程序被编译为binary code后二进制程序的接口,是二进制格式的程序之间通信方式,是low level的,与机器硬件紧密相关的。与此对应的是我们熟悉的API,是high level的,硬件无关的。当API更新时,程序员需要更新代码以符合新的API比如更新参数个数。而ABI更新则需要编译器重新编译代码。 ABI是一个json格式的文件,可以很好的编码和解码。 区块链项目使用ABI,因为在区块链里保存的合约都是二进制格式的,虚拟机执行这些二进制程序,要有一种合适的方式传递调用所需要的合约名,函数名,函数参数,通过对ABI里json格式解析就可以得出调用哪个合约哪个function和用哪些参数。 ABI json格式的定义和解析,在不同操作系统有不同定义(linux用的ELF,windows用的PE),在区块链中也是如此,以太坊和EOS生成的ABI格式就不同,里面包含的tag名称和数量也不同。所以跟操作系统类似,不同区块链的生成的合约不兼容。
✨创建内存数组:
关键词new可以在memory中创建运行时的动态长度数组,和storage中的数组不同,memory中的数组不能通过.push
修改数组大小
✨数组字面常量/内联数组:
必须有一个所有元素都可以隐式转换到普通的类型,这个类型就是数组的基本类型。 数组字面常量总是静态固定大小的 内存memory 数组。
定长内存数组不能赋值给变长的内存数组;如果想要初始化动态长度的数组,就必须要显示的给各个元素赋值。
✨数组成员:
length
push():动态storage数组以及bytes(string类型不可以)都有一个push()的成员函数,可以添加新的元素到数组末尾,有返回值。 x.push().t = 2 或 x.push() = b.
push(x):动态storage数组以及bytes(string类型不可以)都有一个push()的成员函数,可以添加新的元素到数组末尾,没有返回值。
pop:动态storage数组以及bytes(string类型不可以)都有一个push()的成员函数,可以删除数组末尾的元素,隐式调用了delete
push()消耗固定的gas,而pop消耗的gas则依赖元素的大小。
push和pop是更改数组长度的唯一方法
数组切片
仅可用于calldata数组
x[start:end]
, start 和 end 是 uint256 类型(或结果为 uint256 的表达式)。x[start:end]
的第一个元素是x[start]
最后一个元素是 x[end - 1]
。
结构体
合约外声明的结构体可被多个合约共享(没啥用)
合约内声明的结构体仅在此合约和衍生合约中可见
点击查看代码
struct Campaign {
address beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}
映射可以视作 哈希表 ,它们在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值:一个类型的 默认值。然而下面是映射与哈希表不同的地方: 在映射中,实际上并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。 ,因此如果没有其他信息键的信息是无法被删除
映射只能是storage的数据位置,因此只允许作为状态变量或作为函数内的storage引用或作为库函数的参数。
可以将映射声明为public,Solidity会为其创建一个getter函数,KeyType是getter的必须参数,getter会返回ValueType
可迭代映射
映射本身无法遍历,无法枚举所有的键,但可以在他们之上实现一个数据结构进行迭代
涉及LValues的运算符
如果 a 是一个 LValue(即一个变量或者其它可以被赋值的东西),以下运算符都可以使用简写:
a += e 等同于 a = a + e。 其它运算符 -=, *=, /=, %=, |=, &= 以及 ^= 都是如此定义的。 a++ 和 a-- 分别等同于 a += 1 和 a -= 1,但表达式本身的值等于 a 在计算之前的值。 与之相反,--a 和 ++a 虽然最终 a 的结果与之前的表达式相同,但表达式的返回值是计算之后的值。
delete
delete a 的结果是将 a 类型初始值赋值给 a。
即对于整型变量来说,相当于 a = 0。
delete 也适用于数组,对于动态数组来说,是将重置为数组长度为0的数组。
而对于静态数组来说,是将数组中的所有元素重置为初始值。
对数组而言,delete a[x] 仅删除数组索引 x 处的元素,其他的元素和长度不变,这以为着数组中留出了一个空位。如果打算删除项,映射可能是更好的选择。
如果对象 a 是结构体,则将结构体中的所有属性(成员)重置。
换句话说:在 delete a 之后 a 的值与在没有赋值的情况下声明 a 的情况相同
基本类型之间的转换
隐式转换
uint8 y;
uint16 z;
uint32 x = y + z;
显示转换
int8 y = -3;
uint x = uint(y);
这段代码的最后,x 的值将是 0xfffff..fd (64 个 16 进制字符),因为这是 -3 的 256 位补码形式。
字面常量和基本类型之间的转换
整型与字面常量的转换
uint8 a = 12; // 可行
uint32 b = 1234; // 可行
uint16 c = 0x123456; // 失败, 会截断为 0x3456
定长字节数组与字面常量转换
bytes2 a = 54321; // 不可行,十进制不能隐式转换为定长字节数组。
bytes2 b = 0x12; // 不可行
bytes2 c = 0x123; // 不可行,十六进制可以,但仅当十六进制数字大小完全符合定长字节数组长度
bytes2 d = 0x1234; // 可行
bytes2 e = 0x0012; // 可行
bytes4 f = 0; // 可行,0值可以转换为任意定长字节数组类型
bytes4 g = 0x0; // 可行
地址类型
通过校验和测试的正确大小的十六进制字面常量会作为 address 类型。
没有其他字面常量可以隐式转换为 address 类型。
从 bytes20 或其他整型显示转换为 address 类型时,都会作为 address payable 类型。
一个地址 address a 可以通过payable(a)
转换为 address payable 类型.
单位和全局变量
Ether单位
assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);
时间单位
秒是缺省时间单位,在时间单位之间,数字后面带有 seconds、 minutes、 hours、 days 和 weeks 的可以进行换算
因为闰秒是无法预测的,所以需要借助外部的预言机(oracle,是一种链外数据服务,译者注)来对一个确定的日期代码库进行时间矫正。
特殊变量和函数
此处即Solidity语言层面的原生API
区块和交易属性
blockhash(uint blockNumber) returns (bytes32)
:指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块
block.chainid (uint)
:当前链 id
block.coinbase ( address )
: 挖出当前区块的矿工地址
block.difficulty ( uint )
: 当前区块难度
block.gaslimit ( uint )
: 当前区块 gas 限额
block.number ( uint )
: 当前区块号
block.timestamp ( uint)
: 自 unix epoch 起始当前区块以秒计的时间戳
gasleft() returns (uint256)
:剩余的 gas
msg.data ( bytes )
: 完整的 calldata
msg.sender ( address )
: 消息发送者(当前调用)
msg.sig ( bytes4 )
: calldata 的前 4 字节(也就是函数标识符)
msg.value ( uint )
: 随消息发送的 wei 的数量
tx.gasprice (uint)
: 交易的 gas 价格
tx.origin (address payable)
: 交易发起者(完全的调用链)
🌟每一个对外部函数的调用,所有msg成员的值都会变化,包括对库函数的调用
🌟不要依赖 block.timestamp 和 blockhash 产生随机数,除非你知道自己在做什么。
🌟当前区块的时间戳必须严格大于最后一个区块的时间戳
🌟基于可扩展因素,区块哈希不是对所有区块都有效。你仅仅可以访问最近 256 个区块的哈希,其余的哈希均为零。
ABI编码和解码函数
abi.decode(bytes memory encodedData, (...)) returns (...)
: 对给定的数据进行ABI解码,而数据的类型在括号中第二个参数给出 。 例如: (uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
abi.encode(...) returns (bytes)
: ABI - 对给定参数进行编码
abi.encodePacked(...) returns (bytes)
:对给定参数执行 紧打包编码 ,注意,可以不明确打包编码。
abi.encodeWithSelector(bytes4 selector, ...) returns (bytes)
: ABI - 对给定第二个开始的参数进行编码,并以给定的函数选择器作为起始的 4 字节数据一起返回
abi.encodeWithSignature(string signature, ...) returns (bytes)
:等价于 abi.encodeWithSelector(bytes4(keccak256(signature), ...)
🌟这些编码函数可以用来构造函数调用数据,而不用实际进行调用。
🌟keccak256(abi.encodePacked(a, b))
是一种计算结构化数据的哈希值的方式,不推荐使用的 keccak256(a, b)
。
🌟关于计算结构化数据的哈希 :https://learnblockchain.cn/2019/04/24/token-EIP712/ ;更多参考ABI和紧打包编码
错误处理
assert(bool condition)
:如果不满足条件,则会导致Panic 错误,则撤销状态更改 - 用于检查内部错误。
require(bool condition)
:如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误。
require(bool condition, string memory message)
:如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误,可以同时提供一个错误消息。
revert()
:终止运行并撤销状态更改。
revert(string memory reason)
:终止运行并撤销状态更改,可以同时提供一个解释性的字符串。
数学和密码学函数
addmod(uint x, uint y, uint k) returns (uint)
:计算 (x + y) % k,加法会在任意精度下执行,并且加法的结果即使超过 2 ** 256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。
mulmod(uint x, uint y, uint k) returns (uint)
:计算 (x * y) % k,乘法会在任意精度下执行,并且乘法的结果即使超过 2 ** 256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。
keccak256((bytes memory) returns (bytes32)
:计算 Keccak-256 哈希。
sha256(bytes memory) returns (bytes32)
:计算参数的 SHA-256 哈希。
ripemd160(bytes memory) returns (bytes20)
:计算参数的 RIPEMD-160 哈希。
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
:利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。
🌟函数参数对应于 ECDSA签名的值:
r = 签名的前 32 字节
s = 签名的第2个32 字节
v = 签名的最后一个字节
ecrecover 返回一个 address, 而不是 address payable 。他们之前的转换参考 address payable ,如果需要转移资金到恢复的地址。
🌟在一个私链上,你很有可能碰到由于 sha256、ripemd160 或者 ecrecover 引起的 Out-of-Gas。这个原因就是他们被当做所谓的预编译合约而执行,并且在第一次收到消息后这些合约才真正存在(尽管合约代码是硬代码)。发送到不存在的合约的消息非常昂贵,所以实际的执行会导致 Out-of-Gas 错误。在你的合约中实际使用它们之前,给每个合约发送一点儿以太币,比如 1 Wei。这在官方网络或测试网络上不是问题。
地址成员
<address>.balance (uint256)
以 Wei 为单位的 地址类型 Address 的余额。
<address>.code (bytes memory)
在 地址类型 Address 上的代码(可以为空)
<address>.codehash (bytes32)
:ref:address
的codehash
<address payable>.transfer(uint256 amount)
向 地址类型 Address 发送数量为 amount 的 Wei,失败时抛出异常,使用固定(不可调节)的 2300 gas 的矿工费。
<address payable>.send(uint256 amount) returns (bool)
向 地址类型 Address 发送数量为 amount 的 Wei,失败时返回 false,发送 2300 gas 的矿工费用,不可调节。
<address>.call(bytes memory) returns (bool, bytes memory)
用给定的有效载荷(payload)发出低级 CALL 调用,返回成功状态及返回数据,发送所有可用 gas,也可以调节 gas。
<address>.delegatecall(bytes memory) returns (bool, bytes memory)
用给定的有效载荷 发出低级 DELEGATECALL 调用 ,返回成功状态并返回数据,发送所有可用 gas,也可以调节 gas。
<address>.staticcall(bytes memory) returns (bool, bytes memory)
用给定的有效载荷 发出低级 STATICCALL 调用 ,返回成功状态并返回数据,发送所有可用 gas,也可以调节 gas
🌟🌟在执行另一个合约函数时,应该尽可能避免使用 .call() ,因为它绕过了类型检查,函数存在检查和参数打包。
🌟🌟使用 send 有很多危险:如果调用栈深度已经达到 1024(这总是可以由调用者所强制指定),转账会失败;并且如果接收者用光了 gas,转账同样会失败。为了保证以太币转账安全,总是检查 send 的返回值,利用 transfer 或者下面更好的方式: 用这种接收者取回钱的模式。
合约相关
this
(当前的合约类型)
当前合约,可以显示转换为 地址类型 Address。
selfdestruct(address payable recipient)
销毁合约,并把余额发送到指定 地址类型 Address。
请注意, selfdestruct 具有从EVM继承的一些特性:
- 接收合约的 receive 函数 不会执行。
- 合约仅在交易结束时才真正被销毁,并且 revert 可能会“撤消”销毁。
此外,当前合约内的所有函数都可以被直接调用,包括当前函数。
类型信息
表达式 type(X) 可用于检索参数 X 的类型信息。 目前,此功能还比较有限( X 仅能是合约和整型),但是未来应该会扩展。
用于合约类型 C 支持以下属性:
type(C).name:
获得合约名
type(C).creationCode:
获得包含创建合同字节码的内存字节数组。它可以在内联汇编中构建自定义创建例程,尤其是使用 create2 操作码。 不能在合同本身或派生的合同访问此属性。 因为会引起循环引用。
type(C).runtimeCode
获得合同的运行时字节码的内存字节数组。这是通常由 C 的构造函数部署的代码。 如果 C 有一个使用内联汇编的构造函数,那么可能与实际部署的字节码不同。 还要注意库在部署时修改其运行时字节码以防范定期调用(guard against regular calls)。 与 .creationCode 有相同的限制,不能在合同本身或派生的合同访问此属性。 因为会引起循环引用。
除上面的属性, 下面的属性在接口类型I
下可使用:
type(I).interfaceId:
返回接口I
的 bytes4 类型的接口 ID,接口 ID 参考: EIP-165 定义的, 接口 ID 被定义为 XOR (异或) 接口内所有的函数的函数选择器(除继承的函数。
🌟对于整型 T 有下面的属性可访问:
type(T).min
T 的最小值。
type(T).max
T 的最大值。
表达式和控制结构
函数调用
Solidity有if、else、while、do、for、break、continue、return、?:
Solidity没有switch、goto
Solidity还支持 try/catch
的异常处理
控制结构
内部函数调用
合约中创建的函数可以直接调用,可以递归调用
🌟在EVM中,这被视为简单的跳转
🌟应该避免过多的调用,每次内部函数的调用会使用一个堆栈槽,最多只有1024个堆栈槽使用
外部函数调用
c.g(2);
(其中 c 是合约实例)是一种外部函数调用,会通过一个消息调用来执行,而非直接的跳转。(这不会创建自己的交易,而是作为整个交易的一部分的信息调用)
🌟当调用其他合约的函数时,随函数调用发送的 Wei 和 gas 的数量可以分别由特定选项 {value: 10, gas: 10000}
。但不建议明确指定gas,因为操作码的gas消耗将来可能会发生变化
🌟不可以在构造函数中通过this来调用函数,因为此时真正的合约实例还未被创建。
🌟对于一个外部调用,所有的函数参数都要被复制到内存
🌟EVM可以调用不存在的合约,Solidity中会使用 extcodesize
操作码来检查需要调用的合约是否存在。
🌟任何与其他合约的交互都会产生潜在危险,尤其是在不能预先知道合约代码的情况下。 交互时当前合约会将控制权移交给被调用合约,而被调用合约可能做任何事。即使被调用合约从一个已知父合约继承,继承的合约也只需要有一个正确的接口就可以了。 被调用合约的实现可以完全任意的实现,因此会带来危险。 此外,请小心这个交互调用在返回之前再回调我们的合约,这意味着被调用合约可以通过它自己的函数改变调用合约的状态变量。 一个建议的函数写法是,例如,在合约中状态变量进行各种变化后再调用外部函数,这样,你的合约就不会轻易被滥用的重入攻击 (reentrancy) 所影响
具名调用和匿名函数参数
函数调用参数也可以按照任意顺序由名称给出,如果它们被包含在 { } 中, 如以下示例中所示。参数列表必须按名称与函数声明中的参数列表相符,但可以按任意顺序排列。
点击查看代码
...
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
省略函数参数名称
//
通过new创建合约
使用 new 可以在合约中创建一个新合约
加盐的合约创建 create2
创建合约时,将根据创建合约的地址和每次创建合约交易时的计数器来计算合约的地址。但如果指定了一个可选的salt(bytes32),合约创建将使用另一种机制来生成新合约地址:通过给定的salt、创建合约的字节码和构造函数来计算创建合约的地址。(不实用nonce,可提供更大的灵活性)
表达式计算顺序
表达式中计算顺序不是特定的。
赋值
解构赋值和返回多值
Solidity 内部允许元组 (tuple) 类型,也就是一个在编译时元素数量固定的对象列表,列表中的元素可以是不同类型的对象。这些元组可以用来同时返回多个数值,也可以用它们来同时给多个新声明的变量或者既存的变量(或通常的 LValues):
点击查看代码
...
contract C {
uint index;
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}
function g() public {
//基于返回的元组来声明变量并赋值
(uint x, bool b, uint y) = f();
//交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。
(x, y) = (y, x);
//元组的末尾元素可以省略(这也适用于变量声明)。
(index,,) = f(); // 设置 index 为 7
}
}
数组和结构体的复杂性
赋值语义对于像数组和结构体(包括 bytes 和 string) 这样的非值类型来说会有些复杂。
在下面的示例中, 对 g(x) 的调用对 x 没有影响, 因为它在内存中创建了存储值独立副本。但是, h(x) 成功修改 x , 因为只传递引用而不传递副本。
点击查看代码
contract C {
uint[20] x;
function f() public {
g(x);
h(x);
}
function g(uint[20] memory y) internal pure {
y[2] = 3;
}
function h(uint[20] storage y) internal {
y[3] = 4;
}
}
点击查看代码
contract C {
function f(uint a, uint b) pure public returns (uint) {
// 溢出会返回“截断”的结果
unchecked { return a - b; }
}
function g(uint a, uint b) pure public returns (uint) {
// 溢出会抛出异常
return a - b;
}
}
🌟assert 函数只能用于测试内部错误,检查不变量,正常的函数代码永远不会产生Panic, 甚至是基于一个无效的外部输入时。 如果发生了,那就说明出现了一个需要你修复的 bug。如果使用得当,语言分析工具可以识别出那些会导致 Panic 的 assert 条件和函数调用。
下列情况将会产生一个Panic异常: 提供的错误码编号,用来指示Panic的类型。
0x01: 如果你调用 assert 的参数(表达式)结果为 false 。
0x11: 在unchecked { … }
外,如果算术运算结果向上或向下溢出。
0x12: 如果你用零当除数做除法或模运算(例如 5 / 0 或 23 % 0 )。
0x21: 如果你将一个太大的数或负数值转换为一个枚举类型。
0x22: 如果你访问一个没有正确编码的存储byte数组.
0x31: 如果在空数组上 .pop() 。
0x32: 如果你访问 bytesN 数组(或切片)的索引太大或为负数。(例如: x[i] 而 i >= x.length 或 i < 0).
0x41: 如果你分配了太多的内内存或创建了太大的数组。
0x51: 如果你调用了零初始化内部函数类型变量。
🌟在下面的情况下,来自外部调用的错误数据(如果提供的话)被转发,这意味可能 Error 或 Panic 都有可能触发。
0x01: 如果 .transfer() 失败。
0x11: 如果你通过消息调用调用某个函数,但该函数没有正确结束(例如, 它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),不包括使用低级别 call , send , delegatecall , callcode 或 staticcall 的函数调用。低级操作不会抛出异常,而通过返回 false 来指示失败。
0x12: 如果你使用 new 关键字创建合约,但合约创建 没有正确结束 。
🌟可以给 require 提供一个消息字符串,而 assert 不行。 在下例中,你可以看到如何轻松使用require
检查输入条件以及如何使用 assert 检查内部错误.
点击查看代码
contract Sharer {
function sendHalf(address addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
//由于转移函数在失败时抛出异常并且不能在这里回调,因此我们应该没有办法仍然有一半的钱。
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
🌟例子展示了错误字符串如何使用 revert (等价于 require ) :
contract VendingMachine {
function buy(uint amount) payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 下边是等价的方法来做同样的检查:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// 执行购买操作
}
}
如果直接提供错误原因字符串,则这两个语法是等效的,根据开发人员的偏好选择。
🌟require 是一个像其他函数一样可被执行的函数。 意味着,所有的参数在函数被执行之前就都会被计算(执行)。 尤其,在 require(condition, f()) 里,函数 f 会被执行,即便 condition 为 True .
try/catch
外部调用的失败,可以通过 try/catch 语句来捕获,如下:
pragma solidity ^0.6.0;
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// 如果错误超过 10 次,永久关闭这个机制
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /reason/) {
// This is executed in case
// revert was called inside getData
// and a reason string was provided.
errorCount++;
return (0, false);
} catch (bytes memory /lowLevelData/) {
// This is executed in case revert() was used。
errorCount++;
return (0, false);
}
}
}
合约
Solidity 合约类似于面向对象语言中的类。
调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。
创建合约
创建合约时, 合约的 构造函数 (一个用关键字constructor
声明的函数)会执行一次。 构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。构造函数执行完毕后,合约的最终代码将部署到区块链上。此代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。 部署的代码 没有 包括构造函数代码或构造函数调用的内部函数。
🌟在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js 则不必关心这个问题。
🌟如果一个合约想要创建另一个合约,那么创建者必须知晓被创建合约的源代码(和二进制代码)。 这意味着不可能循环创建依赖项。
可见性和getter
状态变量可见性
状态变量有三种可见性
- public:
对于public状态变量会自动生成一个getter函数,以便其他合约读取。在一个合约中使用时,外部方式访问(this.x)会调用getter函数,而内部方式访问(x)会直接从storage中获取值。
setter函数不会产生,其他合约不能直接修改其值 - internal
internal状态变量只能在它们所定义的合约和派生合同
中访问 - private
私有状态变量类似内部变量,但他们在派生合约中不可见
internal和private只能防止其他合约读取或修改信息,但他们仍然可以在链外查看到。
- 可见性标识符的定义位置:
uint public data;
对于状态变量来说是类型后面
function f(uint a) private pure returns (uint b) { return a + 1; }
对于函数来说是参数列表和返回关键字中间
函数可见性
Solidity中:外部函数调用会产生一个EVM调用,内部函数调用则不会
函数有四种可见性:
- external
外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但this.f()
可以)。 - public
public 函数是合约接口的一部分,可以在内部或通过消息调用。 - internal
内部可见性函数访问可以在当前合约或派生的合约访问,不可以外部访问。
由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。 - private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
点击查看代码
contract C {
uint private data;
function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}
// 下面代码编译错误
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // 错误:成员 `f` 不可见
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // 错误:成员 `compute` 不可见
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // 访问内部成员(从继承合约访问父合约成员)
}
}
getter函数
编译器自动为所有的public状态变量创建getter函数。
写得很好:https://learnblockchain.cn/docs/solidity/contracts.html#getter-functions
modifier
modifier可以轻松改变函数的行为。modifier是合约的可继承属性,并可以被派生合约覆盖(前提是被标记为virtual)。此处详见 modifier重载
如果你想访问定义在合约 C 的 modifier m , 可以使用 C.m 去引用它,而不需要使用虚拟表查找。
只能使用在当前合约或在基类合约中定义的 modifier , modifier 也可以定义在库里面,但是他们被限定在库函数使用。
如果同一个函数有多个 modifier,它们之间以空格隔开, modifier 会依次检查执行。
modifier不能隐式地访问或改变它们所修饰的函数的参数和返回值。 这些值只能在调用时明确地以参数传递。
modifier 或函数体中显式的 return 语句仅仅跳出当前的 modifier 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 modifier 中的定义的 _ 之后继续执行。
用 return; 从修改器中显式返回并不影响函数返回值。 然而,修改器可以选择完全不执行函数体,在这种情况下,返回的变量被设置为:ref:默认值default-value,就像该函数是空函数体一样。
_ 符号可以在修改器中出现多次,每处都会替换为函数体。
修改器modifier 的参数可以是任意表达式,在此上下文中,所有在函数中可见的符号,在 修改器modifier 中均可见。 在 修改器modifier 中引入的符号在函数中不可见(可能被重载改变)。
Constant和Immutable状态变量
状态变量声明为 constant (常量)或者 immutable (不可变量)
,在这两种情况下,合约一旦部署之后,变量将不在修改。
对于 constant 常量, 他的值在编译器确定,而对于 immutable, 它的值在部署时确定。
0.7.2之后也可以在文件级别定义 constant 变量
编译器不会为这些变量预留存储位,它们的每次出现都会被替换为相应的常量表达式(它可能被优化器计算为实际的某个值)。
与常规状态变量相比,常量和不可变量的gas成本要低得多。 对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。 这样可以进行本地优化。
不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留32个字节,即使它们适合较少的字节也是如此。 因此,常量有时可能比不可变量更便宜。
不是所有类型的状态变量都支持用 constant 或 immutable 来修饰,当前仅支持 字符串 (仅常量) 和 值类型 .
Constant (常量)
如果状态变量声明为 constant (常量)。在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。 任何通过访问 storage,区块链数据(例如 block.timestamp, address(this).balance 或者 block.number)或执行数据( msg.value 或 gasleft() ) 或对外部合约的调用来给它们赋值都是不允许的。
允许可能对内存分配产生副作用(side-effect)的表达式,但那些可能对其他内存对象产生副作用的表达式则不允许。
内建(built-in)函数 keccak256 , sha256 , ripemd160 , ecrecover , addmod 和 mulmod 是允许的(即使他们确实会调用外部合约, keccak256 除外)。
允许内存分配器的副作用的原因是它可以构造复杂的对象,例如: 查找表(lookup-table)。 此功能尚不完全可用。
immutable (不可变量)
声明为不可变量(immutable)的变量的限制要比声明为常量(constant) 的变量的限制少:可以在合约的构造函数中或声明时为不可变的变量分配任意值。 不可变量只能赋值一次,并且在赋值之后才可以读取。
编译器生成的合约创建代码将在返回合约之前修改合约的运行时代码,方法是将对不可变量的所有引用替换为分配给它们的值。 如果要将编译器生成的运行时代码与实际存储在区块链中的代码进行比较,则这一点很重要。
不可变量可以在声明时赋值,不过只有在合约的构造函数执行时才被视为视为初始化。 这意味着,你不能用一个依赖于不可变量的值在行内初始化另一个不可变量。 不过,你可以在合约的构造函数中这样做。
这是为了防止对状态变量初始化和构造函数顺序的不同解释,特别是继承时,出现问题
函数
可以在合约内部和外部定义函数
合约之外的函数(也称为“自由函数”)始终具有隐式的 internal 可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。
函数参数及返回值
与 Javascript 一样,函数可能需要参数作为输入; 而与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输出
- 函数的输入参数
函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。
函数参数可以作为本地变量,也可以在等号左边被赋值。
外部函数 不可以接受多维数组作为参数(如果原文件加入pragma abicoder v2;
可以启用ABI v2版编码功能,这此功能可用。)
内部函数 则不需要启用ABI v2 就接受多维数组作为参数。 - 返回变量
返回多个值:function arithmetic(uint a, uint b) public pure returns (uint sum, uint product)
返回变量名可省略:
和下面的一个意思
function arithmetic(uint a, uint b)
public
pure
returns (uint sum, uint product)
{
sum = a + b;
product = a * b;
}
和上面的一个意思
function arithmetic(uint a, uint b)
public
pure
returns (uint sum, uint product)
{
return (a + b, a * b);
}
非内部函数有些类型没法返回,比如限制的类型有:多维动态数组、结构体等。
如果添加pragma abicoder v2;
启用 ABI V2 编码器,则是可以的返回更多类型,不过 mapping 仍然是受限的。
- 返回多个值
当函数需要使用多个值,可以用语句 return (v0, v1, ..., vn) 。 参数的数量需要和声明时候一致。
状态可变性 pure/constant/view/payable
-
view函数(constant此前是view的别名,已移除)
可以将函数声明为 view 类型,这种情况下要保证不修改状态。
🌟下面的语句被认为是修改状态:
1.修改状态变量。
2.产生事件。
3.创建其它合约。
4.使用 selfdestruct。
5.通过调用发送以太币。
6.调用任何没有标记为 view 或者 pure 的函数。
7.使用低级调用。
8.使用包含特定操作码的内联汇编。
🌟操作码 STATICCALL 将用于视图函数, 这些函数强制在 EVM 执行过程中保持不修改状态。
🌟库view函数不会在运行时检查进而阻止状态修改。这不会对安全性产生负面影响,因为库代码通常在编译时知道,并且静态检查起会执行编译时的检查。
🌟getter方法自动被标记为view -
pure纯函数
函数可以声明为pure,这种情况下,承诺不读取也不修改状态变量
🌟以下被认为是读取状态:
读取状态变量。
访问 address(this).balance 或者 address.balance。
访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。
调用任何未标记为 pure 的函数。
使用包含某些操作码的内联汇编。
读取 immutable 变量(我们应该可以在编译时确定一个 pure 函数,它仅处理输入参数和 msg.data ,对当前区块链状态没有任何了解。 这也意味着读取 immutable 变量也不是一个 pure 操作。)
🌟pure函数可以在发生错误时使用 revert() 和require() 去还原潜在状态更改
还原状态更改不被视为 “状态修改”, 因为它只还原以前在没有view
或pure
限制的代码中所做的状态更改, 并且代码可以选择捕获revert
并不传递还原。这种行为也符合STATICCALL
操作码。
🌟不可能在 EVM 级别阻止函数读取状态, 只能阻止它们写入状态 (即只能在 EVM 级别强制执行 view , 而 pure 不能强制)。
特别的函数
-
receive接受函数
一个合约最多有一个 receive 函数, 声明函数为:receive() external payable { ... }
。
🌟不需要function
关键字,也没有参数和返回值。并且必须是external 可见性和 payable 修饰
. 它可以是virtual
的,可以被重载,也可以有modifier
。
🌟在对合约没有任何附加数据调用(通常是对合约转账)时会执行 receive 函数.( .send() or .transfer() )
如果 receive 函数不存在,但是有payable的 fallback函数,那么在进行纯以太转账时,fallback 函数会调用.
如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).
🌟receive 函数可能只有 2300 gas 可以使用(如,当使用 send 或 transfer 时), 除了基础的日志输出之外,进行其他操作的余地很小。
下面的操作消耗会操作 2300 gas : 写入存储/创建合约/调用消耗大量 gas 的外部函数/发送以太币
🌟一个没有定义fallback 函数或 receive 函数
的合约,直接接收以太币(没有函数调用,即使用 send 或 transfer)会抛出一个异常, 并返还以太币。 -
fallback回退函数
合约可以最多有一个回退函数。函数声明为: fallback () external [payable] 或 fallback (bytes calldata input) external [payable] returns (bytes memory output)
没有 function 关键字。 必须是 external 可见性,它可以是 virtual 的,可以被重载也可以有 修改器modifier 。
如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配fallback会被调用. 或者在没有 receive 函数 时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。
fallback 函数始终会接收数据,但为了同时接收以太时,必须标记为 payable 。
如果使用了带参数的版本, input 将包含发送到合约的完整数据(等于 msg.data ),并且通过 output 返回数据。 返回数据不是 ABI 编码过的数据,相反,它返回不经过修改的数据。
payable 的fallback函数也可以在纯以太转账的时候执行, 如果没有 receive 以太函数 推荐总是定义一个receive函数,而不是定义一个payable
的fallback函数
如果想要解码输入数据,那么前四个字节用作函数选择器,然后用 abi.decode 与数组切片语法一起使用来解码ABI编码的数据:
(c, d) = abi.decode(_input[4:], (uint256, uint256));
请注意,这仅应作为最后的手段,而应使用对应的函数。
函数重载
合约中可以有多个不同参数的同名函数,称为重载(overloading),这也适用于继承函数。
重载函数也存在于外部接口。如果两个外部可见函数仅区别于Solidity内的类型而不是它们的外部类型则会导致错误。
点击查看代码
contract A {
function f(B value) public pure returns (B out) {
out = value;
}
function f(address value) public pure returns (address out) {
out = value;
}
}
重载解析和参数匹配
通过当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。
(如果所有参数都可以隐式转换为预期类型,则选择函数作为重载候选项,如果一个候选都没有,解析失败)
返回参数不作为重载解析的依据。
contract A {
点击查看代码
contract A {
function f(uint8 val) public pure returns (uint8 out) {
out = val;
}
function f(uint256 val) public pure returns (uint256 out) {
out = val;
}
🌟合约有可能出现以下情况:在不同地方定义的相同名称的错误。而这些错误对调用者来说是无法区分的。 对于外部来说,如 ABI 仅关联了错误的名字,而没有定义它的合约或文件。
error应该只用于指示错误,而不是作为控制流的一种手段>>原因是默认情况下内部调用或的的错误数据可能是通过外部调用链冒泡过来的。这意味着,一个内部调用可以伪造错误数据,使它看起来像是被调用的合约。
继承
Solidity中支持多重继承包括多态
所有的函数调用都是虚拟的,这意味着最终派生的函数会被调用,除非明确给出合约名称或者使用super关键字。
当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约(或称为父合约)的代码被编译到创建的合约中。这意味着对基类合约函数的所有内部调用也只是使用内部函数调用(super.f(..)将使用JUMP跳转而不是消息调用)。
函数重写(override)
父合约标记为 virual 可以在继承合约里 override 以更改他们的行为。重写的函数需要使用关键字override修饰
override的可见性:重写函数只能将覆盖函数的可见性从external改为public。
override的可变性:重写函数的可变性可以更改为更严格的一种:nonpayable可以被view和pure覆盖。view可以被pure覆盖。payable则不能更改为其他可变性。
对于多重继承,如果有多个父合约有相同函数,override关键词后必须制定所有的父合约名。
如果重写的函数来自一个公共的父合约,override可以不显示指定。
如果函数没有标记为virtual,派生合约将不能重写。
private函数不能标记为virtual。
除接口之外(接口会自动标记为virtual),没有实现的函数必须标记为virual。
重写接口函数不要求override关键字,除非函数在多个父合约定义。
modifier重写
modifier也可以重写,工作方式与函数重写类似。需要被重写的modifier也需要virtual修饰,override则同样修饰重载
如果是多重继承,所有直接父合约必须显示指定override
构造函数
构造函数是使用constructor关键字声明的一个可选函数,可以在其中运行合约初始化代码。
在执行构造函数代码之前, 如果状态变量可以初始化为指定值; 如果不初始化, 则为 默认值 。
如果没有构造函数,合约将采用默认构造函数,等效于 constructor(){} 。
在构造函数运行后,合约的最终代码会部署到区块链,代码的部署需要的gas与代码长度线性相关。
这里的代码包括所有函数部分是公有接口以及可以通过函数调用访问的所有函数,但不包括构造函数代码或仅仅从构造函数调用的内部函数。
构造函数可以使用内部(internal)参数(如只想存储的指针),此时合约必须标记为抽象合约。
基类构造函数的参数
基类合约的构造函数中如果有参数,派生合约需要指定所有的参数。如果派生合约没有给出所有基类合约指定参数,则这个合约将是抽象合约。
- 直接在继承列表中指定参数
- 通过派生的构造函数中用modifier
多重继承与线性化
语言中多重继承的常见问题:
- 钻石问题(基类a,b、c继承a,d多重继承b、c。a中有个方法f(),b、c中都重写了,d多重继承后f()是b中的还是c中的)
Solidity借鉴了Python的方式,使用C3线性化强制一个由基类构成的DAG(有向无环图)保持一个特定的顺序。
即:当一个在不同的合约中多次定义函数被调用时, , 给定的基类以从右到左 (Python 中从左到右) 按深度优先的方式进行搜索,在第一次匹配的时候停止。 如果基类合约已经搜索过, 则跳过该合约。
可以通过一个简单的规则来记忆: 以从“最接近的基类”(most base-like)到“最远的继承”(most derived)的顺序来指定所有的基类。
contract X {}
contract A is X {}
// 编译出错
contract C is A, X {}
代码编译出错的原因是 C 要求 X 重写 A (因为定义的顺序是 A, X ), 但是 A 本身要求重写 X,无法解决这种冲突。
继承有相同名字的不同类型成员
function和modifier 同名
function和events同名
events和modifier同名
(例外:状态变量的getter函数可以覆盖external函数)
抽象合约 abstract
如果未实现合约中的至少一个函数,则需要将合约标记为 abstract。 即使实现了所有功能,合同也可能被标记为abstract。
abstract contract Feline {
function utterance() public pure returns (bytes32); //此函数没有具体的实现,应该用abstract修饰合约
}
contract Cat is Feline {
function utterance() public pure returns (bytes32) { return "miaow"; } //合约继承自抽象合约,重写所有未实现的函数后变为非抽象合约。如果只重写了部分函数,则仍是抽象合约。
}
易混:
- 没有实现的函数示例(函数声明):
function foo(address) external returns (address); - 函数类型的示例(变量声明,其中变量的类型为“函数”):
function(address) external returns (address) foo;
接口
接口类似于抽象合约,但不实现任何函数。还有更多的限制
- 无法继承其他合约,但可以继承其他接口
- 接口中所有的函数都是external,即便在合约里是public
- 无法定义构造函数
- 无法定义状态变量
- 无法声明modifier
合约可以继承接口,接口中的函数会被隐式标记为virtual(这意味着重写接口函数不要override)。但不表示重写函数可以再次重写,除非被标记为virtual。
接口可以继承其他接口。
interface Token { //接口的关键字
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}
定义在接口或其他类合约( contract-like)结构体里的类型,可以在其他的合约里用这样的方式访问: Token.TokenType
或 Token.Coin.
库
库与合约类似,库的目的是只需要在特定的地址部署一次,其代码就可以通过EVM的 DELEGATECALL
特性进行重用。
这意味着如果库函数被调用,它的代码在调用合约的上下文执行,即this指向调用合约。
库函数仅能通过直接调用来使用。
库函数不可能被销毁。
库函数的调用与调用显示的基类合约类似( L.f() )
当使用内部调用约定来调用库的内部函数,意味着所有的internal类型和内存类型都是通过引用而不是复制来传递。
EVM为了实现这些,合约所调用的内部库函数的代码和内部调用的所有函数在变一阶段都被包含道调用合约中,然后使用了一个JUMP
代替了DELEGATECALL
与合约相比,库的限制
- 没有状态变量
- 不能继承或被继承
- 不能接受以太
- 不可以被销毁
库的函数签名与选择器
//
库的调用保护
如果库的代码是通过 CALL
来执行,而不是 DELEGATECALL
或者CALLCODE
那么执行的结果会被回退, 除非是对 view 或者 pure 函数的调用。
EVM 没有为合约提供检测是否使用 CALL
的直接方式,但是合约可以使用 ADDRESS
操作码找出正在运行的“位置”。 生成的代码通过比较这个地址和构造时的地址来确定调用模式。
更具体地说,库的运行时代码总是从一个 push
指令开始,它在编译时是 20 字节的零。当运行部署代码时,这个常数 被内存中的当前地址替换,修改后的代码存储在合约中。在运行时,部署时地址就成为了第一个被 push
到堆栈上的常数, 对于任何 non-view
和non-pure
函数,调度器代码都将对比当前地址与这个常数是否一致。
这意味着库在链上存储的实际代码与编译器输出的 deployedBytecode 的编码是不同。
Using for
在当前合约中,指令 using A for B;
用于附加库函数(从库A
)到任何类型(B)作为成员函数。这些函数将接收到调用他们的对象作为他们的第一个参数。
Using For 可在文件或合约内部及合约级都是有效的。
第一部分 A 可以是以下之一:
- 一些库或文件级的函数列表(using {f, g, h, L.t} for uint;), 仅是那些函数被附加到类型。
- 库名称 (using L for uint;) ,库里所有的函数(包括 public 和 internal 函数) 被附加到类型上。
第二部分 B 必须是一个显式类型(不用指定数据位置)
🌟在合约内,你可以使用 using L for *;, 表示库 L 中的函数被附加在所有类型上。
内联汇编
Solidity中内联汇编的语言为Yul
Yul为Solidity提供了对EVM更精细的控制。
内联汇编是一种低级别的访问EVM的方式,这种方式绕过了几个重要的安全功能和检查,只有有信心使用的时候才应该应用到任务中。
assembly{......}
<--Yul以这种方式写在{}内
assembly in Solidity(https://learnblockchain.cn/article/675)
1⃣️汇编语言可以帮助我们完成Solidity无法完成的事(得益于汇编可以和EVM直接交互),产生更少的gas消耗,获得更强的功能。
2⃣️EVM是一个stack machine,只允许两个操作:pop和push,且后入先出(LIFO)。
stack machine是一种处理器,所有数据数保存在栈上。且仍具有PC(程序计数器)和SP(堆栈指针),的存储器和寄存器。
操作码释义:https://www.jianshu.com/p/4017bf2e89fa
3⃣️Solidity中的两种汇编:独立汇编和内联汇编(主要)
4⃣️基本汇编语法:
点击查看代码
assembly{
// yul语言,或称为汇编、汇编代码、EVM汇编
}
不同的汇编代码块之间不能交互
5⃣️简单汇编示例
点击查看代码
function addition(uint x,uint y) public pure returns (uint){
assembly{
let result := add(x,y) //使用操作吗add计算x+y,赋给新变量result
mstore(0x0,result) //使用mstore操作码将restult变量的值存入内存,指定地址0x0
return(0x0,32) //从内存地址0x0返回32字节
}
}
6⃣️定义&赋值
Yul中let
定义关键字变量,使用:=
赋值
如果没有:=
赋值,变量自动初始化为0
可以使用复杂的表达式为变量赋值
xxxxxxxxxxxxxxxxxxx
EVM中,let执行了如下任务:
- 创建一个新的堆栈槽位
- 为变量保留槽位
- 代码块结束时销毁槽位 //因此在汇编代码中定义的变量,外部无法访问
7⃣️注释
8⃣️字面量:字符串字面量最多可以包含32个字符(“very long string more than 32 bytes”<--有35个字符)
9⃣️块和作用域:块使用大括号表示,变量尽在做定义的块内有效
🔟汇编中访问变量
1⃣️1⃣️汇编中的循环:
for:
Solidity:
点击查看代码
function for_loop_solidity(uint n, uint value) public pure returns(uint) {
for ( uint i = 0; i < n; i++ ) {
value = 2 * value;
}
return value;
}
点击查看代码
function for_loop_assembly(uint n, uint value) public pure returns (uint) {
assembly {
for { let i := 0 } lt(i, n) { i := add(i, 1) } {
value := mul(2, value)
}
mstore(0x0, value)
return(0x0, 32)
}
}
1⃣️2⃣️汇编中的判断
if:
点击查看代码
assembly {
if slt(x, 0) { x := sub(0, x) } // OK
}
switch(拥有默认分支default):
点击查看代码
assembly {
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
分支列表不需要大括号,但是分支的代码块需要大括号;
所有的分支条件值必须:1)具有相同的类型 2)具有不同的值
如果分支条件已经涵盖所有可能的值,那么不允许再出现default条件
✨Solidity支持if...else、while、do、for
,但不支持switch
✨当比较的值和一个case匹配时,控制流停止,不会延续到下一个case
因此EVM汇编中的swich可以作为:
非常基本版本的 if/else
if语句的扩展
1⃣️2⃣️汇编中的函数
可以在内联汇编中定义底层函数,包含自己的逻辑,调用这些自定义的函数和使用内置的操作码类似
分配指定长度length的内存,并返回内存指针pos//代码
assembly {
function allocate(length) -> pos { //函数allocate输入length,返回pos
pos := mload(0x40) //从栈顶提取参数
mstore(0x40, add(pos, length)) //将结果压入栈
}
let free_memory_pointer := allocate(64)
}
点击查看代码
assembly {
function my_assembly_function(param1, param2) {
// assembly code here
}
}
点击查看代码
assembly {
function my_assembly_function(param1, param2) -> my_result {
// param2 - (4 * param1)
my_result := sub(param2, mul(4, param1))
}
let some_value = my_assembly_function(4, 9) // 4 - (9 * 4) = 32
}
1⃣️3⃣️操作码OPcodes和汇编
分类:
- 算数和比较
- 位操作
- 密码学计算(keccak256)
- 环境操作码(与区块链相关的全局信息)
- 存储、内存、栈操作
- 交易和合约调用操作
- 停机操作
- 日志操作
1⃣️4⃣️高级汇编概念:
✨多个赋值:
如果函数返回了多个值,可以将他们赋值给元祖(tuple)
点击查看代码
assembly {
function f() -> a, b {}
let c, d := f()
}
🌟栈平衡:
在每个assembly{......}
的末尾,必须平衡堆栈,否则编译器将产生警告。
1⃣️5⃣️Solidity中的函数式风格编程
//函数版本,更易理解哪个操作数(operand)作用于哪个操作码(opcode)
mstore(0x80, add(mload(0x80), 3))
//非函数版本,更易看到值最终位于堆栈的位置
3 0x80 mload add 0x80 mstore
//堆栈布局:操作码的第一个参数始终在堆栈的顶部结束
empty PUSH 3 PUSH 0x80 MLOAD ADD PUSH 0x80 MSTORE
|_0x80| > |5| |_0x80|
|| > |3| > |3| > |3| > |8| > |8| > ||
1⃣️6⃣️缺点
1⃣️7⃣️汇编中都是uint
EVM汇编中的算术运算忽略了某些类型可以小于256位
速查表
运算优先级、全局变量、函数可见性、修饰符、保留关键字
https://learnblockchain.cn/docs/solidity/cheatsheet.html
🌟函数可见性
function myFunction()
return true;
}
public: visible externally and internally (creates a getter function for storage/state variables)
private: only visible in the current contract
external: only visible externally (only for functions) - i.e. can only be message-called (via this.func)
internal: only visible internally
✨修饰符
pure for functions: Disallows modification or access of state.
view for functions: Disallows modification of state.
payable for functions: Allows them to receive Ether together with a call.
constant for state variables: Disallows assignment (except initialisation), does not occupy storage slot.
immutable for state variables: Allows exactly one assignment at construction time and is constant afterwards. Is stored in code.
anonymous for events: Does not store event signature as topic.
indexed for event parameters: Stores the parameter as topic.
virtual for functions and modifiers: Allows the function’s or modifier’s behaviour to be changed in derived contracts.
override: States that this function, modifier or public state variable changes the behaviour of a function or modifier in a base contract.
深入Solidity内部
状态变量在存储中的布局
合约的状态变量会以一种紧凑的方式存储在区块链中,有时候多个值会使用同一个存储槽。
除了动态大小的数组和mapping,数据的存储方式是从位置0开始连续放置在storage中的。对于每个变量,根据其类型确认字节大小。
如果变量的存储大小小于32个字节,则会被打报到一个storage slot(存储插槽)中:
- storage slot的第一项会以地位对齐的方式存储
- 值类型仅使用存储他们所需的字节
- 如果storage slot中剩余空间不足以存储一个值类型,那么他会被存入下一个storage slot。
- struct(结构体)和数组数据总会开启一个新插槽
- struct和数组之后的数据也会开启一个新插槽
- 声明变量时如果按照uint128、uint128、uint256的顺序声明变量会占用两个storage slot(如果按照uint128、uint256、uint128的顺序声明变量会占用三个storage slot)
✨对于使用了继承的合约,来自不同的合约的状态变量会共享一个storage slot(满足一些规则后)
✨在使用小于32字节的元素时,消耗的gas可能大于使用高于32字节的元素,这是因为EVM每次操作32个字节,如果元素小于32个字节,EVM会执行额外的操作
🌟storage中的状态变量的布局被认为是Solidity外部接口的一部分,因此Storage变量值真可以传递给库函数。
mapping和动态数组
由于mapping和动态数组不可预知大小,不能在状态变量之间存储。
他们包含元素的实际存储位置,是通过keccak-256计算确定的。
✨假设mapping或动态数组根据上述存储规则最终确定了某个位置p:
1⃣️对于动态数组而言,此插槽会存储数组中元素的数量(字节数组和字符串除外)
2⃣️对于mapping,该插槽为空,但仍是需要的,为了保证两个彼此相邻的mapping在不同的位置上
数组的元素会从keccak256开始,布局与静态大小的数组相同,一个元素接着一个元素。如果元素的字节不超过16,还有可能共享slot。
json输出
合约的存储布局可以通过编译器获取。
输出的json对象包括两个字段:storage和types(具体内容:https://learnblockchain.cn/docs/solidity/internals/layout_in_storage.html#json)。
下面的例子显示了一个合约和它的存储布局,包含值类型和引用类型、被编码打包的类型和嵌套类型。
pragma solidity >=0.4.0 <0.9.0;
contract A {
struct S {
uint128 a;
uint128 b;
uint[2] staticArray;
uint[] dynArray;
}
uint x;
uint y;
S s;
address addr;
mapping (uint => mapping (address => bool)) map;
uint[] array;
string s1;
bytes b1;
}
{
"storage": [
{
"astId": 15,
"contract": "fileA:A",
"label": "x",
"offset": 0,
"slot": "0",
"type": "t_uint256"
},
{
"astId": 17,
"contract": "fileA:A",
"label": "y",
"offset": 0,
"slot": "1",
"type": "t_uint256"
},
{
"astId": 20,
"contract": "fileA:A",
"label": "s",
"offset": 0,
"slot": "2",
"type": "t_struct(S)13_storage"
},
{
"astId": 22,
"contract": "fileA:A",
"label": "addr",
"offset": 0,
"slot": "6",
"type": "t_address"
},
{
"astId": 28,
"contract": "fileA:A",
"label": "map",
"offset": 0,
"slot": "7",
"type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))"
},
{
"astId": 31,
"contract": "fileA:A",
"label": "array",
"offset": 0,
"slot": "8",
"type": "t_array(t_uint256)dyn_storage"
},
{
"astId": 33,
"contract": "fileA:A",
"label": "s1",
"offset": 0,
"slot": "9",
"type": "t_string_storage"
},
{
"astId": 35,
"contract": "fileA:A",
"label": "b1",
"offset": 0,
"slot": "10",
"type": "t_bytes_storage"
}
],
"types": {
"t_address": {
"encoding": "inplace",
"label": "address",
"numberOfBytes": "20"
},
"t_array(t_uint256)2_storage": {
"base": "t_uint256",
"encoding": "inplace",
"label": "uint256[2]",
"numberOfBytes": "64"
},
"t_array(t_uint256)dyn_storage": {
"base": "t_uint256",
"encoding": "dynamic_array",
"label": "uint256[]",
"numberOfBytes": "32"
},
"t_bool": {
"encoding": "inplace",
"label": "bool",
"numberOfBytes": "1"
},
"t_bytes_storage": {
"encoding": "bytes",
"label": "bytes",
"numberOfBytes": "32"
},
"t_mapping(t_address,t_bool)": {
"encoding": "mapping",
"key": "t_address",
"label": "mapping(address => bool)",
"numberOfBytes": "32",
"value": "t_bool"
},
"t_mapping(t_uint256,t_mapping(t_address,t_bool))": {
"encoding": "mapping",
"key": "t_uint256",
"label": "mapping(uint256 => mapping(address => bool))",
"numberOfBytes": "32",
"value": "t_mapping(t_address,t_bool)"
},
"t_string_storage": {
"encoding": "bytes",
"label": "string",
"numberOfBytes": "32"
},
"t_struct(S)13_storage": {
"encoding": "inplace",
"label": "struct A.S",
"members": [
{
"astId": 3,
"contract": "fileA:A",
"label": "a",
"offset": 0,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 5,
"contract": "fileA:A",
"label": "b",
"offset": 16,
"slot": "0",
"type": "t_uint128"
},
{
"astId": 9,
"contract": "fileA:A",
"label": "staticArray",
"offset": 0,
"slot": "1",
"type": "t_array(t_uint256)2_storage"
},
{
"astId": 12,
"contract": "fileA:A",
"label": "dynArray",
"offset": 0,
"slot": "3",
"type": "t_array(t_uint256)dyn_storage"
}
],
"numberOfBytes": "128"
},
"t_uint128": {
"encoding": "inplace",
"label": "uint128",
"numberOfBytes": "16"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
}
}
}
变量在内存布局
Solidity保留了四个32字节的插槽,字节范围(包括端点)特定用途如下:
0x00 - 0x3f (64 字节): 用于哈希方法的暂存空间(临时空间)
0x40 - 0x5f (32 字节): 当前分配的内存大小(也作为空闲内存指针)
0x60 - 0x7f (32 字节): 零位插槽
暂存空间可以在语句之间使用 (例如在内联汇编中)。 零位插槽用作动态内存数组的初始值,并且永远不应写入(空闲内存指针最初指向 0x80).
Solidity 总是将新对象放在空闲内存指针上,并且内存永远不会被释放(将来可能会改变)。
Solidity 中的内存数组中的元素始终占据32字节的倍数(对于 bytes1[] 总是这样,但不适用与 bytes 和 string )。
多维内存数组是指向内存数组的指针,动态数组的长度存储在数组的第一个插槽中,然后是数组元素。
与存储中布局的不同
如上所述,在内存中的布局与在 存储中 有一些不同。下面是一些例子:
数组的不同
下面的数组在存储中占用32字节(1个槽),但在内存中占用128字节(4个元素,每个32字节)。
uint8[4] a;
结构体的不同
下面的结构体在存储中占用 96 (3个槽,每个32字节) ,但在内存中占用 128 个字节(4 个元素每个 32 字节)。
struct S {
uint a;
uint b;
uint8 c;
uint8 d;
}
Call data布局
ABI规范要求参数填充为32对倍数个字节。
合约构造函数的参数直接附加在合约代码的末尾,也采用ABI编码。 构造函数将通过硬编码偏移量而不是通过使用 codesize 操作码来访问它们,因为在将数据追加到代码时,它就会会改变。
清理变量
当一个值短于256位时,在某些情况下,剩余位必须被清理。
例如,在将一个值写入存储器之前,需要清除剩余的位,因为存储器的内容可以用于计算哈希值或作为消息调用的数据发送。
✨但是通过内联汇编的访问数据没有这种操作,如果通过内联汇编访问短于256位的Solidity变量, 编译器不回保证该值正被清理。
Source mapping
优化器(Optimizer)
合约的元数据
Solidity编译器自动生成的json文件,即合约的元数据,包含着当前合约的相关信息。
包含了当前合约的相关信息:可以用于查询编译器版本、所使用的源代码、ABI和以太坊注释规范格式文档。
✨编译器会讲元数据文件的Swarm哈希值附加到合约的字节码末尾,以便可以以认证的方式获取文件,而不必求助于中心化的数据提供者。
✨必须将元数据文件发送到Swarm等,以便其他人可以访问他。该文件使用solc -- metadata
来生成,并被命名为ContractName_meta.json
某合约元数据
{
// 必选:元数据格式的版本
"version": "1",
// 必选:源代码的编程语言,一般会选择规范的“子版本”
"language": "Solidity",
// 必选:编译器的细节,内容视语言而定。
"compiler": {
// 对 Solidity 来说是必须的:编译器的版本
"version": "0.4.6+commit.2dabbdf0.Emscripten.clang",
// 可选: 生成此输出的编译器二进制文件的哈希值
"keccak256": "0x123..."
},
// 必选:编译的源文件/源单位,键值为文件名
sources:
{
"myFile.sol": {
// 必选:源文件的 keccak256 哈希值
"keccak256": "0x123...",
// 必选(除非定义了“content”,详见下文):
// 已排序的源文件的URL,URL的协议可以是任意的,但建议使用 Swarm 的URL
"urls": [ "bzzr://56ab..." ]
// Optional: 在源文件中定义的 SPDX license 标识
"license": "MIT"
},
"mortal": {
// 必选:源文件的 keccak256 哈希值
"keccak256": "0x234...",
// 必选(除非定义了“urls”): 源文件的字面内容
"content": "contract mortal is owned { function kill() { if (msg.sender == owner) selfdestruct(owner); } }"
}
},
// 必选:编译器的设置
"settings":
{
// 对 Solidity 来说是必须的: 已排序的重定向列表
"remappings": [ ":g/dir" ],
// 可选: 优化器的设置( enabled 默认设为 false )
"optimizer": {
"enabled": true,
"runs": 500,
"details": {
// peephole defaults to "true"
"peephole": true,
// inliner defaults to "true"
"inliner": true,
// jumpdestRemover defaults to "true"
"jumpdestRemover": true,
"orderLiterals": false,
"deduplicate": false,
"cse": false,
"constantOptimizer": false,
"yul": true,
// Optional: Only present if "yul" is "true"
"yulDetails": {
"stackAllocation": false,
"optimizerSteps": "dhfoDgvulfnTUtnIf..."
}
}
}
},
"metadata": {
// Reflects the setting used in the input json, defaults to false
"useLiteralContent": true,
// Reflects the setting used in the input json, defaults to "ipfs"
"bytecodeHash": "ipfs"
}
// Required for Solidity: File and name of the contract or library this
// metadata is created for.
"compilationTarget": {
"myFile.sol": "MyContract"
},
// Required for Solidity: Addresses for libraries used
"libraries": {
"MyLib": "0x123123..."
}
},
// 必选:合约的生成信息
"output":
{
// 必选:合约的 ABI 定义
"abi": [ /*...*/ ],
// 必选:合约的 NatSpec 用户文档
"userdoc": [ /*...*/ ],
// 必选:合约的 NatSpec 开发者文档
"devdoc": [ /*...*/ ],
}
}
应用二进制接口(ABI)说明
基本设计
在以太坊生态中,ABI是从区块链外部与合约进行交互以及合约与合约进行交互的一种标准方式。数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。
函数选择器(function selector)
一个函数调用数据的前四字节,指定了要调用的函数。
✨函数的返回类型不是这个签名的一部分。
参数编码
从第五个字节开始是被编码的参数。
类型编码
1⃣️基础类型
- uint
: M 位的无符号整数, 0 < M <= 256、 M % 8 == 0。例如: uint32, uint8, uint256。 - int
:以 2 的补码作为符号的 M 位整数, 0 < M <= 256、 M % 8 == 0。 - address:除了字面上的意思和语言类型的区别以外,等价于 uint160。在计算和 函数选择器Function Selector 中,通常使用 address。
- uint、 int: uint256、 int256 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 uint256 和 int256。
- bool:等价于 uint8,取值限定为 0 或 1 。在计算和 函数选择器Function Selector 中,通常使用 bool。
- fixed
x : M 位的有符号的固定小数位的十进制数字 8 <= M <= 256、 M % 8 == 0、且 0 < N <= 80。其值 v 即是 v / (10 ** N)。(也就是说,这种类型是由 M 位的二进制数据所保存的,有 N 位小数的十进制数值。译者注。) - ufixed
x :无符号的 fixed x 。 - fixed、 ufixed: fixed128x18、 ufixed128x18 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 fixed128x18 和 ufixed128x18。
- bytes
: M 字节的二进制类型, 0 < M <= 32。 - function:一个地址(20 字节)之后紧跟一个 函数选择器Function Selector (4 字节)。编码之后等价于 bytes24。
2⃣️定长数组类型
[M]:有 M 个元素的定长数组, M >= 0,数组元素为给定类型。
3⃣️非定长类型
- bytes:动态大小的字节序列。
- string:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。
[]:元素为给定类型的变长数组。
可以将若干类型放到一对括号中,用逗号分隔开,以此来构成一个 元组tuple:- (T1,T2,...,Tn):由 T1,…, Tn, n >= 0 构成的 元组tuple。
用 元组tuple 构成 元组tuple、用 元组tuple 构成数组等等也是可能的。另外也可以构成“零元组(zero-tuples)”,就是 n = 0 的情况。
Solidity到ABI类型映射
下表在左栏显示了不支持 ABI 的 Solidity 类型,以及在右栏显示可以代表它们的 ABI 类型。
Solidity ABI
address payable address
contract address
enum uint8
user defined value types its underlying value type
struct tuple
编码的设计准则
编码有如下属性:
1⃣读取的次数取决于参数数组结构中的最大深度。也就是说,要取得a[i][k][l][r],就需要读取四次。
2⃣变量或数组元素的数据不与其他数据交错,他可以是重定位过的,即他只使用相对的地址。
编码的形式化说明
我们需要区分静态和动态类型:静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。
1⃣️
1⃣以下为动态:
bytes
strings
任意类型的变长数组
动态类型的定长数组
动态类型的元祖
2⃣其他类型被称为静态
2⃣️len(a)代表一个二进制字符串a的字节长度,以uint256的方式呈现。
3⃣️enc是实际的编码,对于任意ABI值X,我们根据X的实际类型,递归的定义enc(X)
Function selector和参数编码
大体而言,一个以 a_1, ..., a_n 为参数的对 f 函数的调用,会被编码为
function_selector(f) enc((a_1, ..., a_n))
f 的返回值 v_1, ..., v_k 会被编码为
enc((v_1, ..., v_k))
也就是说,返回值会被组合为一个 元组tuple 进行编码。
动态类型的使用
https://learnblockchain.cn/docs/solidity/abi-spec.html#id9
事件
https://learnblockchain.cn/docs/solidity/abi-spec.html#id10
JSON
合约接口的JSON格式是用来描述函数,事件或错误描述的一个数组。一个函数的描述是一个有如下字段的JSON对象:
type: "function"、 "constructor" 或 "fallback" (未命名的 “缺省” 函数)
name:函数名称;
inputs:对象数组,每个数组对象会包含:
name:参数名称;
type:参数的权威类型(详见下文)
components:供 元组tuple 类型使用(详见下文)
outputs:一个类似于 inputs 的对象数组,如果函数无返回值时可以被省略;
payable:如果函数接受 以太币Ether ,为 true;缺省为 false;
stateMutability:为下列值之一: pure (指定为不读取区块链状态), view (指定为不修改区块链状态), nonpayable (默认值:不接收 Ether)和 payable (与上文 payable 一样)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix