基础语法
以太币Ether 单位
以太币Ether 单位之间的换算就是在数字后边加上 wei
、 finney
、 szabo
或 ether
来实现的,如果后面没有单位,缺省为 Wei。例如 2 ether == 2000 finney
的逻辑判断值为 true
。
时间单位
秒是缺省时间单位,在时间单位之间,数字后面带有 seconds
、 minutes
、 hours
、 days
、 weeks
和 years
的可以进行换算,基本换算关系如下:
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
1 years == 365 days
由于闰秒造成的每年不都是 365 天、每天不都是 24 小时 leap seconds,所以如果你要使用这些单位计算日期和时间,请注意这个问题。因为闰秒是无法预测的,所以需要借助外部的预言机(oracle,是一种链外数据服务,译者注)来对一个确定的日期代码库进行时间矫正。
注解:years
后缀已经不推荐使用了,因为从 0.5.0 版本开始将不再支持。
这些后缀不能直接用在变量后边。如果想用时间单位(例如 days)来将输入变量换算为时间,你可以用如下方式来完成:
function f(uint start, uint daysAfter) public {
if (now >= start + daysAfter * 1 days) {
// ...
}
}
特殊变量和函数
在全局命名空间中已经存在了(预设了)一些特殊的变量和函数,他们主要用来提供关于区块链的信息或一些通用的工具函数。
区块和交易属性
block.blockhash(uint blockNumber) returns (bytes32)
:指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块;而 blocks 从 0.4.22 版本开始已经不推荐使用,由blockhash(uint blockNumber)
代替block.coinbase
(address
): 挖出当前区块的矿工地址block.difficulty
(uint
): 当前区块难度block.gaslimit
(uint
): 当前区块 gas 限额block.number
(uint
): 当前区块号block.timestamp
(uint
): 自 unix epoch 起始当前区块以秒计的时间戳gasleft() returns (uint256)
:剩余的 gasmsg.data
(bytes
): 完整的 calldatamsg.gas
(uint
): 剩余 gas - 自 0.4.21 版本开始已经不推荐使用,由gesleft()
代替msg.sender
(address
): 消息发送者(当前调用)msg.sig
(bytes4
): calldata 的前 4 字节(也就是函数标识符)msg.value
(uint
): 随消息发送的 wei 的数量now
(uint
): 目前区块时间戳(block.timestamp
)tx.gasprice
(uint
): 交易的 gas 价格tx.origin
(address
): 交易发起者(完全的调用链)
ABI 编码函数
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)
。
错误处理
assert(bool condition)
:- 如果条件不满足,则使当前交易没有效果 — 用于检查内部错误。
require(bool condition)
:- 如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误。
require(bool condition, string message)
:- 如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误,可以同时提供一个错误消息。
revert()
:- 终止运行并撤销状态更改。
revert(string 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(...) returns (bytes32)
:- 计算 (tightly packed) arguments 的 Ethereum-SHA-3 (Keccak-256)哈希。
sha256(...) returns (bytes32)
:- 计算 (tightly packed) arguments 的 SHA-256 哈希。
sha3(...) returns (bytes32)
:- 等价于 keccak256。
ripemd160(...) returns (bytes20)
:- 计算 (tightly packed) arguments 的 RIPEMD-160 哈希。
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
:- 利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。 (example usage)
上文中的“tightly packed”是指不会对参数值进行 padding 处理(就是说所有参数值的字节码是连续存放的,译者注),这意味着下边这些调用都是等价的:
keccak256("ab", "c") keccak256("abc") keccak256(0x616263) keccak256(6382179) keccak256(97, 98, 99)
如果需要 padding,可以使用显式类型转换:keccak256("x00x12")
和 keccak256(uint16(0x12))
是一样的。
请注意,常量值会使用存储它们所需要的最少字节数进行打包。例如:keccak256(0) == keccak256(uint8(0))
,keccak256(0x12345678) == keccak256(uint32(0x12345678))
。
在一个私链上,你很有可能碰到由于 sha256
、ripemd160
或者 ecrecover
引起的 Out-of-Gas。原因是因为这些密码学函数在以太坊虚拟机(EVM)中以“预编译合约”形式存在的,且在第一次收到消息后才被真正存在(尽管合约代码是EVM中已存在的硬编码)。因此发送到不存在的合约的消息非常昂贵,所以实际的执行会导致 Out-of-Gas 错误。在你实际使用你的合约之前,给每个合约发送一点儿以太币,比如 1 Wei。这在官方网络或测试网络上不是问题。
合约相关
this
(current contract's type):- 当前合约,可以明确转换为 地址类型。
selfdestruct(address recipient)
:- 销毁合约,并把余额发送到指定 地址类型。
suicide(address recipient)
:- 与 selfdestruct 等价,但已不推荐使用。
此外,当前合约内的所有函数都可以被直接调用,包括当前函数。
表达式和控制结构
输入参数和输出参数
与 Javascript 一样,函数可能需要参数作为输入; 而与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输出。
输出参数
输出参数的声明方式在关键词 returns
之后,与输入参数的声明方式相同。 例如,如果我们需要返回两个结果:两个给定整数的和与积,我们应该写作
pragma solidity ^0.4.16;
contract Simple {
function arithmetics(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}
输出参数名可以被省略。输出值也可以使用 return
语句指定。 return
语句也可以返回多值,参阅:ref:multi-return。 返回的输出参数被初始化为 0;如果它们没有被显式赋值,它们就会一直为 0。
输入参数和输出参数可以在函数体中用作表达式。因此,它们也可用在等号左边被赋值。
批注:更多还可以参考:https://blog.csdn.net/weixin_34000916/article/details/92404851
控制结构
JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了 switch
和 goto
。 因此 Solidity 中有 if
,else
,while
,do
,for
,break
,continue
,return
,? :
这些与在 C 或者 JavaScript 中表达相同语义的关键词。
用于表示条件的括号 不可以 被省略,单语句体两边的花括号可以被省略。
注意,与 C 和 JavaScript 不同, Solidity 中非布尔类型数值不能转换为布尔类型,因此 if (1) { ... }
的写法在 Solidity 中 无效 。
返回多个值
当一个函数有多个输出参数时, return (v0, v1, ...,vn)
写法可以返回多个值。不过元素的个数必须与输出参数的个数相同。
函数调用
内部函数调用
当前合约中的函数可以直接(“从内部”)调用,也可以递归调用,就像下边这个荒谬的例子一样
pragma solidity ^0.4.16;
contract C {
function g(uint a) public pure returns (uint ret) { return f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}
这些函数调用在 EVM 中被解释为简单的跳转。这样做的效果就是当前内存不会被清除,也就是说,通过内部调用在函数之间传递内存引用是非常有效的。
外部函数调用
表达式 this.g(8);
和 c.g(2);
(其中 c
是合约实例)也是有效的函数调用,但是这种情况下,函数将会通过一个消息调用来被“外部调用”,而不是直接的跳转。 请注意,不可以在构造函数中通过 this 来调用函数,因为此时真实的合约实例还没有被创建。
如果想要调用其他合约的函数,需要外部调用。对于一个外部调用,所有的函数参数都需要被复制到内存。
当调用其他合约的函数时,随函数调用发送的 Wei 和 gas 的数量可以分别由特定选项 .value()
和 .gas()
指定:
pragma solidity ^0.4.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(address addr) public { feed = InfoFeed(addr); }
function callFeed() public { feed.info.value(10).gas(800)(); }
}
payable
修饰符要用于修饰 info
,否则,.value() 选项将不可用。
注意,表达式 InfoFeed(addr)
进行了一个的显式类型转换,说明”我们知道给定地址的合约类型是 InfoFeed
“并且这不会执行构造函数。 显式类型转换需要谨慎处理。绝对不要在一个你不清楚类型的合约上执行函数调用。
我们也可以直接使用 function setFeed(InfoFeed _feed) { feed = _feed; }
。 注意一个事实,feed.info.value(10).gas(800)
只(局部地)设置了与函数调用一起发送的 Wei 值和 gas 的数量,只有最后的圆括号执行了真正的调用。
如果被调函数所在合约不存在(也就是账户中不包含代码)或者被调用合约本身抛出异常或者 gas 用完等,函数调用会抛出异常。
警告:
任何与其他合约的交互都会强加潜在危险,尤其是在不能预先知道合约代码的情况下。 当前合约将控制权移交给被调用合约,而被调用合约可能做任何事。即使被调用合约从一个已知父合约继承,继承的合约也只需要有一个正确的接口就可以了。 被调用合约的实现可以完全任意,因此会带来危险。此外,请小心万一它再调用你系统中的其他合约,或者甚至在第一次调用返回之前返回到你的调用合约。 这意味着被调用合约可以通过它自己的函数改变调用合约的状态变量。。一个建议的函数写法是,例如,在你合约中状态变量进行各种变化后再调用外部函数,这样,你的合约就不会轻易被滥用的重入 (reentrancy) 所影响
具名调用和匿名函数参数
如果它们被包含在 {}
中,函数调用参数也可以按照任意顺序由名称给出, 如以下示例中所示。参数列表必须按名称与函数声明中的参数列表相符,但可以按任意顺序排列。
pragma solidity ^0.4.0;
contract C {
function f(uint key, uint value) public {
// ...
}
function g() public {
// 具名参数
f({value: 2, key: 3});
}
}
省略函数参数名称
未使用参数的名称(特别是返回参数)可以省略。这些参数仍然存在于堆栈中,但它们无法访问。
pragma solidity ^0.4.16;
contract C {
// 省略参数名称
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
通过 new
创建合约
使用关键字 new
可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。
pragma solidity ^0.4.0;
contract D {
uint x;
function D(uint a) public payable {
x = a;
}
}
contract C {
D d = new D(4); // 将作为合约 C 构造函数的一部分执行
function createD(uint arg) public {
D newD = new D(arg);
}
function createAndEndowD(uint arg, uint amount) public payable {
//随合约的创建发送 ether
D newD = (new D).value(amount)(arg);
}
}
如示例中所示,使用 .value()
选项创建 D
的实例时可以转发 Ether,但是不可能限制 gas 的数量。如果创建失败(可能因为栈溢出,或没有足够的余额或其他问题),会引发异常。
表达式计算顺序
表达式的计算顺序不是特定的(更准确地说,表达式树中某节点的字节点间的计算顺序不是特定的,但它们的结算肯定会在节点自己的结算之前)。该规则只能保证语句按顺序执行,布尔表达式的短路执行。更多相关信息,请参阅:操作符优先级。
赋值
解构赋值和返回多值
Solidity 内部允许元组 (tuple) 类型,也就是一个在编译时元素数量固定的对象列表,列表中的元素可以是不同类型的对象。这些元组可以用来同时返回多个数值,也可以用它们来同时给多个新声明的变量或者既存的变量(或通常的 LValues):
数组和结构体的复杂性
赋值语义对于像数组和结构体这样的非值类型来说会有些复杂。 为状态变量 赋值 经常会创建一个独立副本。另一方面,对局部变量的赋值只会为基本类型(即 32 字节以内的静态类型)创建独立的副本。如果结构体或数组(包括 bytes
和 string
)被从状态变量分配给局部变量,局部变量将保留对原始状态变量的引用。对局部变量的第二次赋值不会修改状态变量,只会改变引用。赋值给局部变量的成员(或元素)则 改变 状态变量。
作用域和声明
变量声明后将有默认初始值,其初始值字节表示全部为零。任何类型变量的“默认值”是其对应类型的典型“零状态”。例如, bool
类型的默认值是 false
。 uint
或 int
类型的默认值是 0
。对于静态大小的数组和 bytes1
到 bytes32
,每个单独的元素将被初始化为与其类型相对应的默认值。 最后,对于动态大小的数组, bytes
和 string
类型,其默认缺省值是一个空数组或字符串。
错误处理:Assert, Require, Revert and Exceptions
Solidity 使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。 便利函数 assert
和 require
可用于检查条件并在条件不满足时抛出异常。assert
函数只能用于测试内部错误,并检查非变量。 require
函数用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。 如果使用得当,分析工具可以评估你的合约,并标示出那些会使 assert
失败的条件和函数调用。 正常工作的代码不会导致一个 assert 语句的失败;如果这发生了,那就说明出现了一个需要你修复的 bug。
还有另外两种触发异常的方法:revert
函数可以用来标记错误并恢复当前的调用。 revert
调用中包含有关错误的详细信息是可能的,这个消息会被返回给调用者。已经不推荐的关键字 throw
也可以用来替代 revert()
(但无法返回错误消息)。
注解
从 0.4.13 版本开始,throw
这个关键字被弃用,并且将来会被逐渐淘汰。
当子调用发生异常时,它们会自动“冒泡”(即重新抛出异常)。这个规则的例外是 send
和低级函数 call
, delegatecall
和 callcode
--如果这些函数发生异常,将返回 false ,而不是“冒泡”。
警告
作为 EVM 设计的一部分,如果被调用合约帐户不存在,则低级函数 call
, delegatecall
和 callcode
将返回 success。因此如果需要使用低级函数时,必须在调用之前检查被调用合约是否存在。
异常捕获还未实现
在下例中,你可以看到如何轻松使用``require``检查输入条件以及如何使用``assert``检查内部错误,注意,你可以给 require
提供一个消息字符串,而 assert
不行。
pragma solidity ^0.4.22;
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;
}
}
下列情况将会产生一个 assert
式异常:
- 如果你访问数组的索引太大或为负数(例如
x[i]
其中i >= x.length
或i < 0
)。 - 如果你访问固定长度
bytesN
的索引太大或为负数。 - 如果你用零当除数做除法或模运算(例如
5 / 0
或23 % 0
)。 - 如果你移位负数位。
- 如果你将一个太大或负数值转换为一个枚举类型。
- 如果你调用内部函数类型的零初始化变量。
- 如果你调用
assert
的参数(表达式)最终结算为 false。
下列情况将会产生一个 require
式异常:
- 调用
throw
。 - 如果你调用
require
的参数(表达式)最终结算为false
。 - 如果你通过消息调用调用某个函数,但该函数没有正确结束(它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),上述函数不包括低级别的操作
call
,send
,delegatecall
或者callcode
。低级操作不会抛出异常,而通过返回false
来指示失败。 - 如果你使用
new
关键字创建合约,但合约没有正确创建(请参阅上条有关”未正确完成“的定义)。 - 如果你对不包含代码的合约执行外部函数调用。
- 如果你的合约通过一个没有
payable
修饰符的公有函数(包括构造函数和 fallback 函数)接收 Ether。 - 如果你的合约通过公有 getter 函数接收 Ether 。
- 如果
.transfer()
失败。
在内部, Solidity 对一个 require
式的异常执行回退操作(指令 0xfd
)并执行一个无效操作(指令 0xfe
)来引发 assert
式异常。 在这两种情况下,都会导致 EVM 回退对状态所做的所有更改。回退的原因是不能继续安全地执行,因为没有实现预期的效果。 因为我们想保留交易的原子性,所以最安全的做法是回退所有更改并使整个交易(或至少是调用)不产生效果。 请注意, assert
式异常消耗了所有可用的调用 gas ,而从 Metropolis 版本起 require
式的异常不会消耗任何 gas。
下边的例子展示了如何在 revert 和 require 中使用错误字符串:
pragma solidity ^0.4.22;
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."
);
// 执行购买操作
}
}
这里提供的字符串应该是经过 ABI 编码 之后的,因为它实际上是调用了 Error(string)
函数。在上边的例子里,revert("Not enough Ether provided.");
会产生如下的十六进制错误返回值:
0x08c379a0 // Error(string) 的函数选择器
0x0000000000000000000000000000000000000000000000000000000000000020 // 数据的偏移量(32)
0x000000000000000000000000000000000000000000000000000000000000001a // 字符串长度(26)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串数据("Not enough Ether provided." 的 ASCII 编码,26字节)