前言
做区块链相关的创业已经有4年了,都是做ETL方面的,对于真正的合约以及Trading,其实没有真正的使用,技术就是这样,用了就会发现有意思,从最早的没有信仰,到现在的信心满满,虽然BTC的价格还是3年前BTC的价格,但是市场已经今非昔比了,从DEFI的火热,到各种POOL的出现,区块链的运营方式和目标人群都巨大的变化了。在大经济形势周期的转变下,我也从数据,转战到了DEFI,因为DEFI才是代表了未来真正的放下,大环境下,随着疫情,越来越多的政府和国家地区更深入的隔绝,对于崇尚自由的人来说,我们更应该获得真正意义上的金融自由。虽然现在Blockchain还是被美帝把持着。
闲话不多说,我们开始真正的来玩转合约,合约从Ether起,也和Ether的创始团队息息相关,从V神,还有WOOD博士开始,Ether的出现也带来了Solidity和Vyper,智能合约语言,目前来看Vyper更安全,而Solidity使用更广泛,本文主要讲Solidity的开发和使用。
Solidity文档: https://learnblockchain.cn/docs/solidity/introduction-to-smart-contracts.html
正文
Solidity语法
Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在 以太坊虚拟机(EVM) 上运行。Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性。在部署合约时,应该尽量使用最新版本,因为新版本会有一些重大的新特性以及bug修复。
1 | // SPDX-License-Identifier: GPL-3.0 |
pragmas(编译指令)是告知编译器如何处理源代码的指令, Solidity中合约的含义就是一组代码(它的 函数 )和数据(它的 状态 ),它们位于以太坊区块链的一个特定地址上。该合约能完成的事情并不多(由于以太坊构建的基础架构的原因):它能允许任何人在合约中存储一个单独的数字,并且这个数字可以被世界上任何人访问,且没有可行的办法阻止你发布这个数字。当然,任何人都可以再次调用 set
,传入不同的值,覆盖你的数字,但是这个数字仍会被存储在区块链的历史记录中。随后,我们会看到怎样施加访问限制,以确保只有你才能改变这个数字。 所有的标识符(合约名称,函数名称和变量名称)都只能使用ASCII字符集。UTF-8编码的数据可以用字符串变量的形式存储。
1 | // SPDX-License-Identifier: GPL-3.0 |
address public minter;
这一行声明了一个可以被公开访问的 address
类型的状态变量。 address
类型是一个160位的值,且不允许任何算数操作。mapping (address => uint) public balances;` 也创建一个公共状态变量,但它是一个更复杂的数据类型。 该类型将address映射为无符号整数。 Mappings 可以看作是一个 哈希表 它会执行虚拟初始化,以使所有可能存在的键都映射到一个字节表示为全零的值。 但是,这种类比并不太恰当,因为它既不能获得映射的所有键的列表,也不能获得所有值的列表。
event Sent(address from, address to, uint amount);
这行声明了一个所谓的“事件(event)”,它会在 send
函数的最后一行被发出。
对于程序员来说,区块链这个概念并不难理解,这是因为大多数难懂的东西 (挖矿, 哈希 ,椭圆曲线密码学 ,点对点网络(P2P) 等) 都只是用于提供特定的功能和承诺。
区块,交易,事务
区块链是全球共享的事务性数据库,这意味着每个人都可加入网络来阅读数据库中的记录。如果你想改变数据库中的某些东西,你必须创建一个被所有其他人所接受的事务。此外,交易总是由发送人(创建者)签名。
在比特币中,要解决的一个主要难题,被称为“双花攻击 (double-spend attack)”:如果网络存在两笔交易,都想花光同一个账户的钱时(即所谓的冲突)会发生什么情况?交易互相冲突?
以太仿虚拟机EVM
以太坊虚拟机 EVM 是智能合约的运行环境。它不仅是沙盒封装的,而且是完全隔离的,也就是说在 EVM 中运行代码是无法访问网络、文件系统和其他进程的。甚至智能合约之间的访问也是受限的。以太坊中有两类账户(它们共用同一个地址空间): 外部账户 由公钥-私钥对(也就是人)控制; 合约账户 由和账户一起存储的代码控制.每个账户都有一个键值对形式的持久化存储。其中 key 和 value 的长度都是256位,我们称之为 存储 。此外,每个账户有一个以太币余额( balance )(单位是“Wei”, 1 ether
是 10**18 wei
),余额会因为发送包含以太币的交易而改变。交易可以看作是从一个帐户发送到另一个帐户的消息(这里的账户,可能是相同的或特殊的零帐户,请参阅下文)。它能包含一个二进制数据(合约负载)和以太币。如果目标账户含有代码,此代码会被执行,并以 payload 作为入参。如果目标账户是零账户(账户地址为 0
),此交易将创建一个 新合约 。 如前文所述,合约的地址不是零地址,而是通过合约创建者的地址和从该地址发出过的交易数量计算得到的(所谓的“nonce”)。 这个用来创建合约的交易的 payload 会被转换为 EVM 字节码并执行。执行的输出将作为合约代码被永久存储。这意味着,为创建一个合约,你不需要发送实际的合约代码,而是发送能够产生合约代码的代码。
一经创建,每笔交易都收取一定数量的 gas ,目的是限制执行交易所需要的工作量和为交易支付手续费。EVM 执行交易时,gas 将按特定规则逐渐耗尽。gas price 是交易发送者设置的一个值,发送者账户需要预付的手续费= gas_price * gas
。如果交易执行后还有剩余, gas 会原路返还。无论执行到什么位置,一旦 gas 被耗尽(比如降为负值),将会触发一个 out-of-gas 异常。当前调用帧(call frame)所做的所有状态修改都将被回滚。译者注:调用帧(call frame),指的是下文讲到的EVM的运行栈(stack)中当前操作所需要的若干元素。
每个账户有一块持久化内存区称为 存储 。 存储是将256位字映射到256位字的键值存储区。 在合约中枚举存储是不可能的,且读存储的相对开销很高,修改存储的开销甚至更高。合约只能读写存储区内属于自己的部分。
第二个内存区称为 内存 ,合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。 内存是线性的,可按字节级寻址,但读的长度被限制为256位,而写的长度可以是8位或256位。当访问(无论是读还是写)之前从未访问过的内存字(word)时(无论是偏移到该字内的任何位置),内存将按字进行扩展(每个字是256位)。扩容也将消耗一定的gas。 随着内存使用量的增长,其费用也会增高(以平方级别)。
EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为 栈(stack) 的区域执行。 栈最大有1024个元素,每个元素长度是一个字(256位)。对栈的访问只限于其顶端,限制方式为:允许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面16个元素中的一个。所有其他操作都只能取最顶的两个(或一个,或更多,取决于具体的操作)元素,运算后,把结果压入栈顶。当然可以把栈上的元素放到存储或内存中。但是无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素。
EVM的指令集量应尽量少,以最大限度地避免可能导致共识问题的错误实现。所有的指令都是针对”256位的字(word)”这个基本的数据类型来进行操作。具备常用的算术、位、逻辑和比较操作。也可以做到有条件和无条件跳转。此外,合约可以访问当前区块的相关属性,比如它的编号和时间戳。
有一种特殊类型的消息调用,被称为 委托调用(delegatecall) 。它和一般的消息调用的区别在于,目标地址的代码将在发起调用的合约的上下文中执行,并且 msg.sender
和 msg.value
不变。 这意味着一个合约可以在运行时从另外一个地址动态加载代码。存储、当前地址和余额都指向发起调用的合约,只有代码是从被调用地址获取的。 这使得 Solidity 可以实现”库“能力:可复用的代码库可以放在一个合约的存储上,如用来实现复杂的数据结构的库。
有一种特殊的可索引的数据结构,其存储的数据可以一路映射直到区块层级。这个特性被称为 日志(logs) ,Solidity用它来实现 事件(events) 。合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。因为部分日志数据被存储在 布隆过滤器(Bloom filter) 中,我们可以高效并且加密安全地搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。
合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作 selfdestruct
。合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码从状态中被移除。移除一个合约听上去不错,但其实有潜在的危险,如果有人发送以太币到移除的合约,这些以太币将永远提丢失。
安装Solidity
1 | ### 最常用的还是通过nodejs来安装 |
Solidity源文件结构
源文件中可以包含任意多个 合约定义 、导入源文件指令 、 版本标识 指令、 结构体 , 枚举 和 函数 定义.
1 | pragma solidity ^0.5.2; |
这样,源文件将既不允许低于 0.5.2 版本的编译器编译, 也不允许高于(包含) 0.6.0
版本的编译器编译(第二个条件因使用 ^
被添加)。 这种做法的考虑是,编译器在 0.6.0 版本之前不会有重大变更,所以可确保源代码始终按预期被编译。 上面例子中不固定编译器的具体版本号,因此编译器的补丁版也可以使用。
Pragma 是 pragmatic information 的简称,微软 Visual C++ 文档 中译为标识。 Solidity 中沿用 C ,C++ 等中的编译指令概念,用于告知编译器 如何 编译。 ——译者注
第2个标注是用来标注实验性阶段的功能,它可以用来启用一些新的编译器功能或语法特性。 当前支持下面的一些实验性标注: ABIEncoderV2
新的 ABI 编码器可以用来编码和解码嵌套的数组和结构体,当然这部分代码还在优化之中,他没有像之前 ABI 编码器 那样经过严格的测试,我们可以使用下面的语法来启用它
1 | pragma experimental ABIEncoderV2; |
1 | import "filename"; |
ES6 即 ECMAScript 6.0,ES6是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布。 ——译者注
通常使用相对引用 import "./filename.sol";
并且避免使用 ..
,后面这种方式可以使用全局路径并设置映射,下面会有解释。
可以使用单行注释(//
)和多行注释(/*...*/
)
在 Solidity 语言中,合约类似于其他面向对象编程语言中的类。
每个合约中可以包含 状态变量、 函数、事件 Event、 结构体、 和 枚举类型 的声明,且合约可以从其他合约继承。
1 | pragma solidity >=0.4.0 <0.8.0; |
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | pragma solidity >=0.4.22 <0.8.0; |
函数 修改器modifier 可以用来以声明的方式修改函数语义(参阅合约章节中 函数修改器)。
1 | pragma solidity >=0.4.21 <0.8.0; |
事件是能方便地调用以太坊虚拟机日志功能的接口。
1 | pragma solidity >=0.4.0 <0.8.0; |
1 | pragma solidity >=0.4.0 <0.8.0; |
值类型
bool
:可能的取值为字面常量值 true
和 false
。
运算符:
!
(逻辑非)&&
(逻辑与, “and” )||
(逻辑或, “or” )==
(等于)!=
(不等于)运算符
||
和&&
都遵循同样的短路( short-circuiting )规则。就是说在表达式f(x) || g(y)
中, 如果f(x)
的值为true
,那么g(y)
就不会被执行,即使会出现一些副作用。
int
/ uint
:分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8
到 uint256
(无符号,从 8 位到 256 位)以及 int8
到 int256
,以 8
位为步长递增。 uint
和 int
分别是 uint256
和 int256
的别名。
运算符:
- 比较运算符:
<=
,<
,==
,!=
,>=
,>
(返回布尔值) - 位运算符:
&
,|
,^
(异或),~
(位取反) - 移位运算符:
<<
(左移位) ,>>
(右移位) - 算数运算符:
+
,-
, 一元运算-
, 一元运算+
,*
,/
,%
(取余或叫模运算) ,**
(幂)
对于整形 X
,可以使用 type(X).min
和 type(X).max
去获取这个类型的最小值与最大值。
加法,减法和乘法具有通常的语义,值用两进制补码表示,意思是比如:uint256(0) - uint256(1)== 2 ** 256 - 1
。 我们在设计和编写智能合约时必须考虑到溢出问题。
表达式 -x
相当于 (T(0) - x)
这里 T
是指 x
的类型。 这意味着如果 x
的类型的类型是无符号整数类型 -x
不会是负数。 另外,如果 x
为负数, -x
也可以为正数。 由于两进制补码表示还需要小心:
除法运算结果的类型始终是其中一个操作数的类型,整数除法总是产生整数。 在Solidity中,分数会取零。 这意味着 int256(-5) / int256(2) == int256(-2)
注意在智能合约中,在 字面常量 上进行除法会保留精度(保留小数位)。
除以0 会发生错误(assert 类型错误)。
模运算 a%n
是在操作数 a
的除以 n
之后产生余数 r
,其中 q = int(a / n)
和 r = a - (n * q)
。 这意味着模运算结果与左操作数相同的符号相同(或零)。 对于 负数的a : a % n == -(a % n)
, 几个例子:
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取模会发生错误(assert 类型错误)。
注意 `00在EVM中定义为
1` 。**
fixed
/ ufixed
:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxN
和 fixedMxN
中,M
表示该类型占用的位数,N
表示可用的小数位数。 M
必须能整除 8,即 8 到 256 位。 N
则可以是从 0 到 80 之间的任意数。 ufixed
和 fixed
分别是 ufixed128x19
和 fixed128x19
的别名。
地址类型有两种形式,他们大致相同:
address
:保存一个20字节的值(以太坊地址的大小)。ddress payable
:可支付地址,与address
相同,不过有成员函数transfer
和send
。
这种区别背后的思想是 address payable
可以接受以太币的地址,而一个普通的 address
则不能。
可以使用 balance
属性来查询一个地址的余额, 也可以使用 transfer
函数向一个可支付地址(payable address)发送 以太币Ether (以 wei 为单位):
1 | address x = 0x123; |
如果当前合约的余额不够多,则 transfer
函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer
函数同样会失败而进行回退。
如果 x
是一个合约地址,它的代码(更具体来说是, 如果有receive函数, 执行 receive 接收以太函数, 或者存在fallback函数,执行 Fallback 回退函数 函数)会跟 transfer
函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币Ether 交易会被打回,当前的合约也会在终止的同时抛出异常。
1 | 警告⚠️: |
1 | bytes memory payload = abi.encodeWithSignature("register(string)", "MyName"); |
所有这些函数都是低级函数,应谨慎使用。 具体来说,任何未知的合约都可能是恶意的,我们在调用一个合约的同时就将控制权交给了它,而合约又可以回调合约,所以要准备好在调用返回时改变相应的状态变量(可参考 可重入 ), 与其他合约交互的常规方法是在合约对象上调用函数(x.f())。
所有三个函数 call
,delegatecall
和 staticcall
都是非常低级的函数,应该只把它们当作 最后一招 来使用,因为它们破坏了 Solidity 的类型安全性。
1 | uint128 a = 1; |
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)] |
内存memory 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
存储storage 状态变量保存的位置,只要合约存在就一直存储.
调用数据calldata 用来保存函数参数的特殊数据位置,是一个只读位置。
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | pragma solidity >=0.6.99 <0.8.0; |
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | // SPDX-License-Identifier: GPL-3.0 |
映射本身是无法遍历的,即无法枚举所有的键。不过,可以在它们之上实现一个数据结构来进行迭代。 例如,以下代码实现了 IterableMapping
库,然后 User
合约可以添加数据, sum
函数迭代求和所有值。
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | uint8 y; |
单位和全局变量
以太币Ether 单位之间的换算就是在数字后边加上 wei
、gwei
或 ether
来实现的,如果后面没有单位,缺省为 wei。例如 2 ether == 2000 finney
的逻辑判断值为 true
。
1 | assert(1 wei == 1); |
这些后缀不能直接用在变量后边。如果想用时间单位(例如 days)来将输入变量换算为时间,你可以用如下方式来完成:
1 | 1 == 1 seconds |
1 | 这些编码函数可以用来构造函数调用数据,而不用实际进行调用。此外,keccak256(abi.encodePacked(a, b)) 是一种计算结构化数据的哈希值(尽管我们也应该关注到:使用不同的函数参数类型也有可能会引起“哈希冲突” )的方式,不推荐使用的 keccak256(a, b) 。 |
错误处理
可以参阅专门的章节 assert and require 参阅有关错误处理以及何时使用哪个函数的更多详细信息。
assert(bool condition)
如果不满足条件,则会导致无效的操作码,则撤销状态更改 - 用于检查内部错误。
require(bool condition)
如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误。
require(bool condition, string memory message)
如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误,可以同时提供一个错误消息。
revert()
终止运行并撤销状态更改。
revert(string memory reason)
终止运行并撤销状态更改,可以同时提供一个解释性的字符串。
1 | pragma solidity >=0.5.0 <0.8.0; |
1 | pragma solidity >=0.5.0 <0.8.0; |
在内部, Solidity 对一个 require
式的异常执行回退操作(指令 0xfd
)并执行一个无效操作(指令 0xfe
)来引发 assert
式异常。 在这两种情况下,都会导致 EVM 回退对状态所做的所有更改。回退的原因是不能继续安全地执行,因为没有实现预期的效果。
在这两种情况下,调用者都可以使用 try
/catch
来应对此类失败(在assert
类型的异常中,仅在剩余足够gas的情况下才行 ),但是调用者中的更改将始终被还原。
请注意, assert
式异常消耗了所有可用的调用 gas ,而从 Metropolis 版本起 require
式的异常不会消耗任何 gas。 这里还是尽量使用require.
合约
Solidity 合约类似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。 调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。
可以通过以太坊交易“从外部”或从 Solidity 合约内部创建合约。
一些集成开发环境,例如 Remix, 通过使用一些UI用户界面使创建合约的过程更加顺畅。 在以太坊上通过编程创建合约最好使用 JavaScript API web3.js。 现在,我们已经有了一个叫做 web3.eth.Contract 的方法能够更容易的创建合约。
创建合约时, 合约的 构造函数 (一个用关键字 constructor
声明的函数)会执行一次。 构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。
构造函数执行完毕后,合约的最终代码将部署到区块链上。此代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。 部署的代码没有 包括构造函数代码或构造函数调用的内部函数。
在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js
则不必关心这个问题。
如果一个合约想要创建另一个合约,那么创建者必须知晓被创建合约的源代码(和二进制代码)。 这意味着不可能循环创建依赖项。
1 | pragma solidity >=0.4.22 <0.8.0; |
由于 Solidity 有两种函数调用(内部调用不会产生实际的 EVM 调用或称为“消息调用”,而外部调用则会产生一个 EVM 调用), 函数和状态变量有四种可见性类型。 函数可以指定为 external
,public
,internal
或者 private
。 对于状态变量,不能设置为 external
,默认是 internal
。
1 | external |
外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f
不能从内部调用(即 f
不起作用,但 this.f()
可以)。 当收到大量数据的时候,外部函数有时候会更有效率,因为数据不会从calldata复制到内存.
1 | public |
public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数(见下面)。
1 | internal |
这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this
调用。
1 | private |
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
合约中的所有内容对外部观察者都是可见的。设置一些 private
类型只能阻止其他合约访问和修改这些信息, 但是对于区块链外的整个世界它仍然是可见的。
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | // SPDX-License-Identifier: GPL-3.0 |
如果状态变量声明为 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)。 此功能尚不完全可用。
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | pragma solidity >=0.4.16 <0.8.0; |
1 | pragma solidity >=0.5.0 <0.8.0; |
Getter 方法自动被标记为 view
。constant
之前是 view
的别名,不过在0.5.0之后移除了。函数可以声明为 pure
,在这种情况下,承诺不读取也不修改状态。
一个合约最多有一个 receive
函数, 声明函数为: receive() external payable { ... }
不需要 function
关键字,也没有参数和返回值并且必须是 external
可见性和 payable
修饰. 在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive
函数. 例如 通过 .send()
or .transfer()
如果 receive
函数不存在, 但是有payable 的 fallback 回退函数 那么在进行纯以太转账时,fallback 函数会调用.
如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).
更糟的是,fallback函数可能只有 2300 gas 可以使用(如,当使用 send
或 transfer
时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :
写入存储
创建合约
调用消耗大量 gas 的外部函数
发送以太币
一个没有定义 fallback 函数或 receive 函数的合约,直接接收以太币(没有函数调用,即使用 send
或 transfer
)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。 所以如果你想让你的合约接收以太币,必须实现receive函数(使用 payable fallback 函数不再推荐,因为它会让借口混淆)。 这个之前调试老版本的合约中出现过异常的情况。
1 | pragma solidity >=0.6.2 <0.8.0; |
1 | var options = { |
关于web3 subscribe的描述
1 | pragma solidity >=0.4.10 <0.8.0; |
日志的底层接口
1 | // SPDX-License-Identifier: GPL-3.0 |
父合约标记为 virtual
函数可以在继承合约里重写(overridden)以更改他们的行为。重写的函数需要使用关键字 override
修饰。
重写函数只能将覆盖函数的可见性从 external
更改为 public
。
可变性可以按照以下顺序更改为更严格的一种: nonpayable
可以被 view
和 pure
覆盖。 view
可以被 pure
覆盖。 payable
是一个例外,不能更改为任何其他可变性。
1 | // SPDX-License-Identifier: GPL-3.0 |
接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制:
- 无法继承其他合约,不过可以继承其他接口。
- 所有的函数都需要是 external
- 无法定义构造函数。
- 无法定义状态变量。
将来可能会解除这里的某些限制。
接口基本上基本上仅限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换应该不会丢失任何信息。
接口由它们自己的关键字表示:
1 | pragma solidity >=0.6.2 <0.8.0; |
就像继承其他合约一样,合约可以继承接口。接口中的函数都会隐式的标记为 virtual
,意味着他们会被重写。 但是不表示重写(overriding)函数可以再次重写,仅仅当重写的函数标记为 virtual
才可以再次重写。
1 | pragma solidity >=0.6.0 <0.7.0; |
这个我们没咋个看到过
合约元数据
Solidity编译器自动生成JSON文件,即合约的元数据,其中包含了当前合约的相关信息。 它可以用于查询编译器版本,所使用的源代码,|ABI| 和 |natspec| 文档,以便更安全地与合约进行交互并验证其源代码。
编译器会将元数据文件的 Swarm 哈希值附加到每个合约的字节码末尾(详情请参阅下文), 以便你可以以认证的方式获取该文件,而不必求助于中心化的数据提供者。
当然,你必须将元数据文件发布到 Swarm (或其他服务),以便其他人可以访问它。 该文件可以通过使用 solc --metadata
来生成,并被命名为 ContractName_meta.json
。 它将包含源代码的在 Swarm 上的引用,因此你必须上传所有源文件和元数据文件。
元数据文件具有以下格式。 下面的例子将以人类可读的方式呈现。 正确格式化的元数据应正确使用引号,将空白减少到最小,并对所有对象的键值进行排序以得到唯一的格式。 代码注释当然也是不允许的,这里仅用于解释目的。
1 | { |
由于生成的合约的字节码包含元数据的哈希值,因此对元数据的任何更改都会导致字节码的更改。 此外,由于元数据包含所有使用的源代码的哈希值,所以任何源代码中的, 哪怕是一个空格的变化都将导致不同的元数据,并随后产生不同的字节代码。需注意,上面的 ABI 没有固定的顺序,随编译器的版本而不同。尽管从 Solidity 0.5.12 开始,数组保持了一定的顺序。
源代码验证方法
为了验证编译,可以通过元数据文件中的链接从 Swarm 中获取源代码。 获取到的源码,会根据元数据中指定的设置,被正确版本的编译器(应该为“官方”编译器之一)所处理。 处理得到的字节码会与创建交易的数据或者 CREATE
操作码使用的数据进行比较。 这会自动验证元数据,因为它的哈希值是字节码的一部分。 而额外的数据,则是与基于接口进行编码并展示给用户的构造输入数据相符的。
ABI
在 以太坊Ethereum 生态系统中, 应用二进制接口Application Binary Interface(ABI) 是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。 数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。
除了 元组tuple 以外,Solidity 支持以上所有类型的名称。ABI 元组tuple 是利用 Solidity 的 structs
编码得到的。
1 | pragma solidity ^0.4.16; |
这样,对于我们的例子 Foo
,如果我们想用 69
和 true
做参数调用 baz
,我们总共需要传送 68 字节,可以分解为:
0xcdcd77c0
:方法ID。这源自ASCII格式的baz(uint32,bool)
签名的 Keccak 哈希的前 4 字节。0x0000000000000000000000000000000000000000000000000000000000000045
:第一个参数,一个被用 0 值字节补充到 32 字节的 uint32 值69
。0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数,一个被用 0 值字节补充到 32 字节的 boolean 值true
。
合起来就是:
1 | 0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001 |
它返回一个 bool
。比如它返回 false
,那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000
,一个bool值。
事件,是 以太坊Ethereum 的日志/事件监视协议的一个抽象。日志项提供了合约的地址、一系列的主题(最高 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能(与接口定义一起),事件沿用了既存的 ABI 函数。
给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有 3 个,被用来与事件签名的 Keccak 哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。
一个事件描述是一个有极其相似字段的 JSON 对象:
type
:总是"event"
;name
:事件名称;1
inputs
:对象数组,每个数组对象会包含:
name
:参数名称;type
:参数的权威类型(相见下文);components
:供 元组tuple 类型使用(详见下文);indexed
:如果此字段是日志的一个主题,则为true
;否则为false
。
anonymous
:如果事件被声明为anonymous
,则为true
。
1 | // SPDX-License-Identifier: GPL-3.0 |
1 | [{ |
结语
Solidity的合约开始真正的意义上的普及还需要一些时间,但是对于真正的使用,还是相当的有意思。
接下来的使用中,在一步一步更新这个文档,感觉solidity蛮有意思的。