本文重点介绍 Solidity 语言的计量单位和全局变量,包括以太币单位、时间单位、特殊变量和函数三个类别。本文内容来源于 Solidity 0.8.12 版本。
以太币单位
以太币 Ether 单位之间的换算就是通过在数字后边加上wei
、gwei
或ether
来实现的,如果数字后面没有单位,缺省为 wei。
assert(1 wei == 1);assert(1 gwei == 1e9);assert(1 ether == 1e18);
货币单位后缀的的效果相当于乘以 10 的幂。
从 0.7.0 版本开始,
finney
和szabo
这两个单位被移除了。
时间单位
数字后面带有seconds
、minitues
、hours
、days
和weeks
后缀的可以用来指定时间单位,其中seconds
是基本单位,时间单位之间的基本换算关系如下:
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
因为有闰秒的原因,所以不是每年等于 365 天,甚至不是每天都有 24 小时。如果您使用这些单位进行日历计算,请务必注意这个问题。由于无法预测闰秒,准确的日历库必须由外部 oracle 更新。
由于上述原因,在 0.5.0 版本中删除了后缀
years
。
这些后缀不能直接应用于变量。如果你想用时间单位days
将函数的输入变量换算成时间,你可以用以下方式:
function f(uint start, uint daysAfter) public {if (block.timestamp >= start + daysAfter * 1 days) {// ...}}
特殊变量和函数
在全局命名空间中已经存在一些特殊的变量和函数,它们主要用于提供关于区块链的信息,或者是一些通用的实用函数。
区块和交易属性
blockhash(uint blockNumber) returns (bytes32)
: 当区块号 blocknumber 是最近 256 个区块之一时,返回给定区块的哈希值,否则返回 0block.basefee
(uint
): 当前区块的基本费用 (EIP-3198和EIP-1559)block.chainid
(uint
): 当前链 IDblock.coinbase
(address payable
): 当前区块的矿工地址block.difficulty
(uint
): 当前区块难度block.gaslimit
(uint
): 当前区块 gas 限制block.number
(uint
): 当前区块号block.timestamp
(uint
): 以 Unix 纪元以来以秒为单位的当前区块时间戳gasleft() returns (uint256)
: 剩余的 gasmsg.data
(bytes calldata
): 完整的 calldatamsg.sender
(address
): 消息发送者 (当前调用)msg.sig
(bytes4
): calldata 的前四个字节 (即函数标识符)msg.value
(uint
): 随消息发送的 wei 的数量tx.gasprice
(uint
): 交易的 gas 价格tx.origin
(address
): 交易发起者 (全调用链)对于每一个外部函数调用,包括
msg.sender
和msg.value
在内所有msg
成员的值都会变化。这里包括对库函数的调用。
不要依赖
block.timestamp
或blockhash
来生成随机数,除非你知道你在做什么。时间戳和块哈希都可能在某种程度上受到矿工的影响。例如,挖矿社区中的恶意矿工可以在一个选定的哈希上运行一个赌场支付函数,如果他们没有收到任何钱,就重试一个不同的哈希。
当前区块的时间戳必须严格大于最后一个区块的时间戳,但唯一的保证是它将位于权威链中两个连续块的时间戳之间。
由于可扩展性的原因,区块哈希值不是对所有区块都可用的。您只能访问最近的 256 个区块的哈希值,所有其他值将为 0。
函数
blockhash
以前被称为block.blockhash
,它在 0.4.22 版本中已弃用,在 0.5.0 版本中被移除。
函数
gasleft
以前被称为msg.gas
,在 0.4.21 版本中已弃用,在 0.5.0 版本中被移除。
在 0.7.0 版本中,
now
(block.timestamp
的别名) 被移除。
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 memory)
: 对给定参数进行 ABI 编码abi.encodePacked(...) returns (bytes memory)
: 对给定的参数执行打包编码。请注意,打包编码可能是不明确的!abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory)
: 对给定的参数从第二个开始 ABI 编码,并返回 4 字节的函数选择器abi.encodeWithSignature(string memory signature, ...) returns (bytes memory)
: 等价于abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)
abi.encodeCall(function functionPointer, (...)) returns (bytes memory)
: 使用字节数组中匹配的参数对functionPointer
的函数调用进行 ABI 编码。并执行完整的类型检查,确保类型与函数签名匹配。结果等价于abi.encodeWithSelector(functionPointer.selector, (...))
这些编码函数可以用于为外部函数调用构建数据,而无需实际调用外部函数。此外,
keccak256(abi.encodePacked(a, b))
是一种计算结构化数据哈希值的方法 (尽管要知道,使用不同的函数参数类型也可能产生 “哈希冲突” )。
字节数组成员
bytes.concat(...) returns (bytes memory)
: 将可变数量的字节数组参数 bytes1,…,bytes32 连接到一个字节数组字符串成员
string.concat(...) returns (string memory)
: 将可变数量的字符串参数连接到一个字符串数组错误处理
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 的检查。mulmod(uint x, uint y, uint k) returns (uint)
: 计算(x * y) % k,其中乘法运算以任意精度执行,且结果超过 2**256 也不会被截取。从 0.5.0 版本开始增加 k != 0 的检查。keccak256(bytes memory) returns (bytes32)
: 计算输入参数的 keccak -256 哈希值。keccak256 曾经有一个名为 sha3 的别名,在 0.5.0 版本中被删除了。
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
: 签名的第二个 32 字节v
: 签名的最后 1 个字节
ecrecover
返回一个address
,而不是address payable
。如需转帐到回复的地址,请参阅 address payable。
地址成员
<address>.balance
(uint256
): 以 wei 为单位的地址类型 address 的余额<address>.code
(bytes memory
): 地址类型 address 的代码 (可以为空)<address>.codehash
(bytes32
): 地址类型 address 的代码哈希值<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)
: 用给定的有效载荷发出低级 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
或者下面更好的方式:用接收者取回钱的模式。
在 0.5.0 之前,Solidity 允许通过合约实例访问地址成员,例如
this.balance
。现在禁止这样做,并且必须对地址进行显式转换:address(this).balance
。
在 0.5.0 版本之前,
.call
、.delegatcall
和.staticall
只返回成功条件,而不返回数据。
合约相关
this
(当前合约类型): 当前合约,可转换为地址类型
selfdestruct(address payable recipient)
销毁当前合约,将其资金发送到指定地址并终止执行。请注意,自毁有一些继承自 EVM 的特性:
接收合约的 receive 函数不会执行合约只有在事务结束时才被真正地销毁,并且revert
可能会“撤销”销毁。
此外,当前合约内的所有函数都可以直接调用,包括当前函数。
在 0.5.0 版本之前,有一个名为
suicide
的函数,其语义与selfdestruct
相同。
类型信息
表达式type(X)
可以用来检索关于X
类型的信息。目前,对该特性的支持有限 (X
仅可以是合约类型和整数类型),但将来可能会扩展。
以下属性可用于契约类型C
:
type(C).name
: 获取合约名type(C).creationCode
: 获取包含合约创建字节码的内存字节数组。可以在内联程序集中用于构建自定义创建例程,特别是通过使用create2
操作码。此属性不能在合于本身或任何派生合约中访问。它导致字节码被包含在调用站点的字节码中,因此会引起循环引用。type(C).runtimeCode
: 获取包含合约运行时字节码的内存字节数组。这是通常由C
构造函数部署的代码。如果C
有一个使用内联汇编的构造函数,这可能与实际部署的字节码不同。还要注意,库在部署时修改它们的运行时字节码,以防止常规调用。与.creationcode
相同的限制也适用于此属性。
除了上述属性外,对于类型I
的接口,还可以使用以下属性:
type(I).interfaceId
: 返回一个 EIP-165 定义的接口I
的bytes4
类型的接口标识符 (ID)。这个标识符 (ID) 被定义为XOR
(异或) 接口内所有的函数的函数选择器 (除继承的函数)。
以下属性可用于整数类型T
:
type(T).min
: 类型T
的最小值type(T).max
: 类型T
的最大值
编写这篇文章的过程,其实也是我对 Solidity 语言基础知识的一个温习和巩固的过程,其中的大部分变量或函数会经常用于智能合约代码中。后续还会有更多的关于智能合约的文章上线,敬请关注和赐教指正。