solidity源文件结构
-
// SPDX-License-Identifier: MIT
-
pragma solidity ^0.5.2;
-
pragma abicoder v1;
-
import "filename";
-
注释
智能合约组成
状态变量
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract SimpleStorage {
uint storedData; // State variable
// ...
}
函数/方法
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract SimpleAuction {
function bid() public payable { // Function
// ...
}
}
// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
return x * 2;
}
修饰器
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract Purchase {
address public seller;
modifier onlySeller() { // Modifier
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}
function abort() public view onlySeller { // Modifier usage
// ...
}
}
事件
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;
contract SimpleAuction {
event HighestBidIncreased(address bidder, uint amount); // Event
function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // Triggering event
}
}
异常
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Not enough funds for transfer. Requested `requested`,
/// but only `available` available.
error NotEnoughFunds(uint requested, uint available);
contract Token {
mapping(address => uint) balances;
function transfer(address to, uint amount) public {
uint balance = balances[msg.sender];
if (balance < amount)
revert NotEnoughFunds(amount, balance);
balances[msg.sender] -= amount;
balances[to] += amount;
// ...
}
}
结构类型
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Ballot {
struct Voter { // Struct
uint weight;
bool voted;
address delegate;
uint vote;
}
}
枚举类型
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Purchase {
enum State { Created, Locked, Inactive } // Enum
}
变量类型
Solidity 是一种静态类型语言,这意味着需要指定每个变量(状态和局部)的类型。Solidity 提供了几种基本类型,它们可以组合起来形成复杂类型。
此外,类型可以在包含运算符的表达式中相互交互。有关各种运算符的快速参考,请参阅运算符的优先顺序。
“undefined”或“null值的概念在 Solidity 中不存在,但新声明的变量始终具有取决于其类型的默认值。要处理任何意外值,您应该使用revert 函数来回滚整个事务,或者返回一个元组,其中第二个bool
值表示成功。
值类型
-
bool
: true、false -
int/uint
:uint 和 int 分别是 uint256 和 int256 的别名。-
具有加、减、乘、除、模、指数计算
-
-
address: -
address
:一个 20 字节的值(以太坊地址的大小)。 -
address payable
:与address一样,但是具有transfer
和send
功能 -
这种区别背后的想法是, address payable
是一个你可以将以太币发送到的地址,而你不应该将以太币发送到一个普通地址,例如,因为它可能是一个不是为接受以太币而构建的智能合约。 -
类型转换: -
允许从 address payable
到address
的隐式转换,而从address
到address payable
的转换必须通过payable(<address>)
显式转换。 -
对于 uint160
、整型文字、bytes20
和合约类型,允许与地址进行显式转换。 -
只有 address
和合约类型的表达式才能通过显式转换payable(...)
转换成address payable
类型。 对于合约类型,只有当合约可以接收以太币时才允许这种转换,即合约具有接收或支付回退功能。 请注意,payable(0)
是有效的并且是此规则的例外。
-
-
address类型支持的方法
-
balance
:查询地址的余额 -
transfer
:发送eth到可支付的地址(如果当前合约没有足够多的余额或eth交易被接受地址拒绝,该方法会失败回滚)
address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);
如果 x 是合约地址,它的代码(更具体地说:它的 Receive Ether Function,如果存在,或者它的 Fallback Function,如果存在)将与转移调用一起执行(这是 EVM 的一个特性,无法阻止 ). 如果执行耗尽 gas 或以任何方式失败,以太币转移将被还原,当前合约将异常停止。
-
send
:send
是transfer
的低层对应。如果执行失败,当前合约不会异常停止,但是send
会返回false
-
call
,delegatecall
和staticcall
:为了与不遵守 ABI 的合约进行交互,或者为了更直接地控制编码,提供了函数call
、delegatecall
和staticcall
。 它们都采用单个字节内存参数并返回成功条件(作为布尔值)和返回的数据(字节内存)。 函数abi.encode
、abi.encodePacked
、abi.encodeWithSelector
和abi.encodeWithSignature
可用于对结构化数据进行编码。-
只有 call
可以发送eth:address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
-
bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);
合约类型
每个合约都定义了自己的类型。 您可以隐式地将合同转换为它们继承自的合同。 合同可以显式转换为地址类型或从地址类型转换。
仅当合同类型具有接收或应付回退功能时,才可能与地址应付类型进行显式转换。 转换仍然使用 address(x) 执行。 如果合约类型没有 receive 或 payable 回退函数,则可以使用 payable(address(x)) 转换为 address payable。 您可以在有关地址类型的部分中找到更多信息。
如果您声明一个合同类型的局部变量 (MyContract c),您可以调用该合同的函数。 注意从具有相同合同类型的地方分配它。
您还可以实例化合约(这意味着它们是新创建的)。
合约的数据表示与地址类型相同,这种类型也用在 ABI 中。
合同不支持任何运营商。
合约类型的成员是合约的外部函数,包括任何标记为公共的状态变量。
对于合约 C,您可以使用 type(C) 来访问有关合约的类型信息。
固定大小的字节数组
值类型 bytes1
、bytes2
、bytes3
、…、bytes32
包含从 1 到 32 的字节序列。
枚举
枚举是在 Solidity 中创建用户定义类型的一种方式。 它们可以显式转换为所有整数类型,但不允许隐式转换。 从整数的显式转换会在运行时检查值是否位于枚举范围内,否则会导致 Panic 错误。 枚举至少需要一个成员,声明时的默认值是第一个成员。 枚举不能超过 256 个成员。
数据表示与 C 中的枚举相同:选项由从 0 开始的后续无符号整数值表示。
使用 type(NameOfEnum).min 和 type(NameOfEnum).max 您可以获得给定枚举的最小值和最大值。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;
contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;
function setGoStraight() public {
choice = ActionChoices.GoStraight;
}
// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity.
function getChoice() public view returns (ActionChoices) {
return choice;
}
function getDefaultChoice() public pure returns (uint) {
return uint(defaultChoice);
}
function getLargestValue() public pure returns (ActionChoices) {
return type(ActionChoices).max;
}
function getSmallestValue() public pure returns (ActionChoices) {
return type(ActionChoices).min;
}
}
用户定义的值类型
用户定义的值类型允许在基本值类型上创建零成本抽象。 这类似于别名,但具有更严格的类型要求。
用户定义的值类型使用type C is V
定义,其中 C 是新引入类型的名称,V 必须是内置值类型(“底层类型”)。 函数 C.wrap
用于将基础类型转换为自定义类型。 同样,函数 C.unwrap
用于将自定义类型转换为基础类型。
C 类型没有任何运算符或附加的成员函数。 特别是,甚至连运算符 == 都没有定义。 不允许与其他类型进行显式和隐式转换。
这种类型的值的数据表示是从底层类型继承的,底层类型也在 ABI 中使用。
以下示例说明了自定义类型 UFixed256x18
,它表示具有 18 位小数的十进制定点类型和用于对该类型执行算术运算的最小库。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;
// Represent a 18 decimal, 256 bit wide fixed point type using a user-defined value type.
type UFixed256x18 is uint256;
/// A minimal library to do fixed point operations on UFixed256x18.
library FixedMath {
uint constant multiplier = 10**18;
/// Adds two UFixed256x18 numbers. Reverts on overflow, relying on checked
/// arithmetic on uint256.
function add(UFixed256x18 a, UFixed256x18 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) + UFixed256x18.unwrap(b));
}
/// Multiplies UFixed256x18 and uint256. Reverts on overflow, relying on checked
/// arithmetic on uint256.
function mul(UFixed256x18 a, uint256 b) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(UFixed256x18.unwrap(a) * b);
}
/// Take the floor of a UFixed256x18 number.
/// @return the largest integer that does not exceed `a`.
function floor(UFixed256x18 a) internal pure returns (uint256) {
return UFixed256x18.unwrap(a) / multiplier;
}
/// Turns a uint256 into a UFixed256x18 of the same value.
/// Reverts if the integer is too large.
function toUFixed256x18(uint256 a) internal pure returns (UFixed256x18) {
return UFixed256x18.wrap(a * multiplier);
}
}
请注意 UFixed256x18.wrap
和 FixedMath.toUFixed256x18
如何具有相同的签名但执行两个截然不同的操作:UFixed256x18.wrap
函数返回一个 UFixed256x18
,它与输入具有相同的数据表示,而 toUFixed256x18
返回一个具有相同数值的 UFixed256x18
。
方法类型
函数类型是函数的类型。 函数类型的变量可以从函数中赋值,函数类型的函数参数可用于将函数传递给函数并从函数调用返回函数。 函数类型有两种类型——内部函数和外部函数:
内部函数只能在当前合约内部调用(更具体地说,在当前代码单元内部,也包括内部库函数和继承函数),因为它们不能在当前合约的上下文之外执行。 调用内部函数是通过跳转到其入口标签来实现的,就像在内部调用当前合约的函数一样。
外部函数由地址和函数签名组成,它们可以通过外部函数调用传递和返回。
函数类型表示如下:
function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]
与参数类型相反,返回类型不能为空 - 如果函数类型不应返回任何内容,则必须省略整个returns (<return types>)
部分。
默认情况下,函数类型是内部的,因此可以省略internal
关键字。 请注意,这仅适用于函数类型。 必须为合约中定义的函数明确指定可见性,它们没有默认值。
转换:
函数类型 A 可隐式转换为函数类型 B 当且仅当它们的参数类型相同、返回类型相同、内部/外部属性相同并且 A 的状态可变性比 B 的状态可变性更具限制性 。 尤其:
-
pure
函数可以转换为view
和non-payable
函数 -
view
函数可以转换为non-payable
函数 -
payable
函数可以转换为non-payable
函数
函数类型之间没有其他转换是可能的。
关于payable
和non-payable
的规则可能有点混乱,但本质上,如果一个函数是payable
的,这意味着它也接受零以太币的支付,所以它也是non-payable
的。 另一方面,non-payable
函数将拒绝发送给它的以太币,因此non-payable函数
无法转换为payable
。 澄清一下,拒绝以太币比不拒绝以太币更具限制性。 这意味着您可以用non-payable
覆盖payable
,但反之则不行。
此外,当您定义一个non-payable
函数指针时,编译器不会强制指向的函数拒绝以太币。 相反,它强制函数指针永远不会用于发送以太币。 这使得将payable
函数指针分配给non-payable
函数指针成为可能,确保两种类型的行为方式相同,即两者都不能用于发送以太币。
如果未初始化函数类型变量,则调用它会导致 Panic 错误。 如果在使用 delete
之后调用函数,也会发生同样的情况。
如果在 Solidity 上下文之外使用外部函数类型,它们将被视为函数类型,它将地址后跟函数标识符一起编码为单个 bytes24 类型。
请注意,当前合约的公共功能既可以用作内部功能,也可以用作外部功能。 要将f
用作内部函数,只需使用f
,如果要使用其外部形式,请使用this.f
。
一个内部类型的函数可以赋值给一个内部函数类型的变量,而不管它定义在哪里。 这包括合约和库的私有、内部和公共功能以及免费功能。 另一方面,外部函数类型只与公共和外部合约函数兼容。
单位和全局变量
Ether单位
-
1 wei == 1
(默认) -
1 gwei == 1e9
-
1 ether == 1e18
时间单位
-
1 == 1 seconds
(默认) -
1 minutes == 60 seconds
-
1 hours == 60 minutes
-
1 days == 24 hours
-
1 weeks == 7 days
这些后缀不能直接用在变量后边。如果想用时间单位(例如 days)来将输入变量换算为时间,你可以用如下方式来完成:
function f(uint start, uint daysAfter) public {
if (block.timestamp >= start + daysAfter * 1 days) {
// ...
}
}
特殊变量和函数
在全局命名空间中已经存在了(预设了)一些特殊的变量和函数,他们主要用来提供关于区块链的信息或一些通用的工具函数。
区块和交易属性
-
blockhash(uint blockNumber) returns (bytes32)
:指定区块的区块哈希 —— 仅可用于最新的 256 个区块且不包括当前区块,否则返回 0 。 -
block.basefee
(uint
): 当前区块的基础费用,参考: ( EIP-3198 和 EIP-1559) -
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
): 交易发起者(完全的调用链)
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), ...)
-
abi.encodeCall(function functionPointer, (...)) returns (bytes memory)
: 使用tuple类型参数ABI 编码调用functionPointer
。执行完整的类型检查, 确保类型匹配函数签名。结果和abi.encodeWithSelector(functionPointer.selector, (...))
一致。
错误处理
-
** 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 ,如果需要转移资金到恢复的地址。
地址成员
-
** <address>.balance
(uint256
)**以 Wei 为单位的 地址类型 Address 的余额。 -
** <address>.code
(bytes memory
)**在 地址类型 Address 上的代码(可以为空) -
<address>.codehash
(bytes32
) 地址类型 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。 发出低级函数DELEGATECALL
,失败时返回false
,发送所有可用 gas,可调节。 -
** <address>.staticcall(bytes memory) returns (bool, bytes memory)
**用给定的有效载荷 发出低级STATICCALL
调用 ,返回成功状态并返回数据,发送所有可用 gas,也可以调节 gas。
合约相关
-
** this
(当前的合约类型)**当前合约,可以显示转换为 地址类型 Address。 -
** selfdestruct(address payable recipient)
**销毁合约,并把余额发送到指定 地址类型 Address。请注意,selfdestruct
具有从EVM继承的一些特性:-
接收合约的 receive 函数 不会执行。 - -
合约仅在交易结束时才真正被销毁,并且 revert
可能会“撤消”销毁。
-
此外,当前合约内的所有函数都可以被直接调用,包括当前函数。
表达式和控制结构
控制结构
JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了 switch
和 goto
。 因此 Solidity 中有 if
, else
, while
, do
, for
, break
, continue
, return
, ? :
这些与在 C 或者 JavaScript 中表达相同语义的关键词。
Solidity还支持 try
/ catch
语句形式的异常处理,但仅用于 外部函数调用 和合约创建调用。 使用revert 语句
可以触发一个”错误”。
用于表示条件的括号 不可以 被省略,单语句体两边的花括号可以被省略。
注意,与 C 和 JavaScript 不同, Solidity 中非布尔类型数值不能转换为布尔类型,因此 if (1) { ... }
的写法在 Solidity 中 无效 。
函数调用
内部函数调用
当前合约中的函数可以直接(“从内部”)调用,也可以递归调用,就像下边这个无意义的例子一样:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// 编译器会有警告提示
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 中被解释为简单的跳转。这样做的效果就是当前内存不会被清除,例如,函数之间通过传递内存引用进行内部调用是非常高效的。 只能在同一合约实例的函数,可以进行内部调用。
只有在同一合约的函数可以内部调用。仍然应该避免过多的递归调用, 因为每个内部函数调用至少使用一个堆栈槽, 并且最多有1024堆栈槽可用。
外部函数调用
可以使用表达式 this.g(8);
和 c.g(2);
进行调用,其中 c
是合约实例, g
合约内实现的函数,但是这两种方式调用函数,称为“外部调用”,它是通过消息调用来进行,而不是直接的代码跳转。 请注意,不可以在构造函数中通过 this 来调用函数,因为此时真实的合约实例还没有被创建。
如果想要调用其他合约的函数,需要外部调用。对于一个外部调用,所有的函数参数都需要被复制到内存。
当调用其他合约的函数时,需要在函数调用是指定发送的 Wei 和 gas 数量,可以使用特定选项 {value: 10, gas: 10000}
请注意,不建议明确指定gas,因为操作码的 gas 消耗将来可能会发生变化。 任何发送给合约 Wei 将被添加到目标合约的总余额中:
pragma solidity >=0.6.2 <0.9.0;
contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public { feed = addr; }
function callFeed() public { feed.info{value: 10, gas: 800}(); }
}
payable
修饰符要用于修饰 info
函数,否则, value
选项将不可用。
由于EVM认为可以调用不存在的合约的调用,因此在 Solidity 语言层面里会使用 extcodesize
操作码来检查要调用的合约是否确实存在(包含代码),如果不存在该合约,则抛出异常。
如果返回数据在调用后被解码,则跳过这个检查,因此ABI解码器将捕捉到不存在的合约的情况。
请注意,这个检查在 低级别调用 时不被执行,这些调用是对地址而不是合约实例进行操作。
如果被调用合约本身抛出异常或者 gas 用完等,函数调用也会抛出异常。
具名参数函数调用
函数调用参数可以按名称以任何顺序给出,如果它们包含在 {} 中,如以下示例所示。 参数列表的名称必须与函数声明中的参数列表一致,但顺序可以是任意的。
pragma solidity >=0.4.0 <0.9.0;
contract C {
mapping(uint => uint) data;
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
}
省略函数参数名称
函数声明中的参数名称和返回值可以省略。 那些省略名称的项目仍会出现在堆栈中,但无法通过名称访问它们。 省略的返回值名称仍然可以通过使用 return 语句将值返回给调用者。
pragma solidity >=0.4.22 <0.9.0;
contract C {
// 省略参数名称
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
**通过 new
创建合约
使用关键字 new
可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) payable {
x = a;
}
}
contract C {
D d = new D(4); // will be executed as part of C's constructor
function createD(uint arg) public {
D newD = new D(arg);
newD.x();
}
function createAndEndowD(uint arg, uint amount) public payable {
// Send ether along with the creation
D newD = new D{value: amount}(arg);
newD.x();
}
}
如示例中所示,可以在使用 value 选项创建 D 的实例时发送 Ether,但无法限制gas的数量。 如果创建失败(由于出栈、余额不足或其他问题),则抛出异常。
加盐合约创建/create2
创建合约时,合约的地址是根据创建合约的地址和一个随着每次合约创建而增加的计数器计算得出的。
如果您指定选项salt
(bytes32 值),那么合约创建将使用不同的机制来提供新合约的地址:
它将根据创建合约的地址、给定的salt
值、已创建合约的(创建)字节码和构造函数参数计算地址。
特别是,不使用计数器(“nonce”)。 这允许在创建合同时更加灵活:您可以在创建新合同之前派生出新合同的地址。 此外,如果创建合约同时创建其他合约,您也可以依赖此地址。
一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint arg) public {
// This complicated expression just tells you how the address
// can be pre-computed. It is just there for illustration.
// You actually only need ``new D{salt: salt}(arg)``.
address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(D).creationCode,
abi.encode(arg)
))
)))));
D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
赋值
解构赋值和返回多值
Solidity 内部允许元组 (tuple) 类型,也就是一个在编译时元素数量固定的对象列表,列表中的元素可以是不同类型的对象。这些元组可以用来同时返回多个数值,也可以用它们来同时给多个新声明的变量或者既存的变量(或通常的 LValues)赋值:
pragma solidity >=0.5.0 <0.9.0;
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
}
}
不可能混合变量声明和非声明变量复制, 即以下是无效的: (x, uint y) = (1, 2);
数组和结构体的复杂性
赋值语义对于像数组和结构体(包括 bytes
和 string
) 这样的非值类型来说会有些复杂。
在下面的示例中, 对 g(x)
的调用对 x
没有影响, 因为它在内存中创建了存储值独立副本。但是, h(x)
成功修改 x
, 因为只传递引用而不传递副本。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
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;
}
}
作用域和声明
变量声明后将有默认初始值,其初始值字节表示全部为零。任何类型变量的“默认值”是其对应类型的典型“零状态”。例如, bool
类型的默认值是 false
。 uint
或 int
类型的默认值是 0
。对于静态大小的数组和 bytes1
到 bytes32
,每个单独的元素将被初始化为与其类型相对应的默认值。 最后,对于动态大小的数组 bytes
和 string
类型,其默认缺省值是一个空数组或空字符串。
对于 enum
类型, 默认值是第一个成员。
变量声明后将有默认初始值,其初始值字节表示全部为零。任何类型变量的“默认值”是其对应类型的典型“零状态”。例如, bool
类型的默认值是 false
。 uint
或 int
类型的默认值是 0
。对于静态大小的数组和 bytes1
到 bytes32
,每个单独的元素将被初始化为与其类型相对应的默认值。 最后,对于动态大小的数组 bytes
和 string
类型,其默认缺省值是一个空数组或空字符串。
对于 enum
类型, 默认值是第一个成员。
Solidity 中的作用域规则遵循了 C99(与其他很多语言一样):变量将会从它们被声明之后可见,直到一对 { }
块的结束。作为一个例外,在 for 循环语句中初始化的变量,其可见性仅维持到 for 循环的结束。
对于参数形式的变量(例如:函数参数、修饰器参数、catch参数等等)在其后接着的代码块内有效。 这些代码块是函数的实现,catch 语句块等。
那些定义在代码块之外的变量,比如函数、合约、自定义类型等等,并不会影响它们的作用域特性。这意味着你可以在实际声明状态变量的语句之前就使用它们,并且递归地调用函数。
基于以上的规则,下边的例子不会出现编译警告,因为那两个变量虽然名字一样,但却在不同的作用域里。
pragma solidity >=0.5.0 <0.9.0;
contract C {
function minimalScoping() pure public {
{
uint same;
same = 1;
}
{
uint same;
same = 3;
}
}
}
作为 C99 作用域规则的特例,请注意在下边的例子里,第一次对 x
的赋值会改变上一层中声明的变量值。如果外层声明的变量被“覆盖”(就是说被在内部作用域中由一个同名变量所替代)你会得到一个警告。
pragma solidity >=0.5.0 <0.9.0;
// 有警告
contract C {
function f() pure public returns (uint) {
uint x = 1;
{
x = 2; // 这个赋值会影响在外层声明的变量
uint x;
}
return x; // x has value 2
}
}
算术运算的检查模式与非检查模式
当对无限制整数执行算术运算,其结果超出结果类型的范围,这是就发生了上溢出或下溢出。
在Solidity 0.8.0之前,算术运算总是会在发生溢出的情况下进行“截断”,从而得靠引入额外检查库来解决这个问题(如 OpenZepplin 的 SafeMath)。
而从Solidity 0.8.0开始,所有的算术运算默认就会进行溢出检查,额外引入库将不再必要。
如果想要之前“截断”的效果,可以使用 unchecked
代码块:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
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;
}
}
调用 f(2, 3)
将返回 2**256-1
, 而 g(2, 3)
会触发失败异常。
unchecked
代码块可以在代码块中的任何位置使用,但不可以替代整个函数代码块,同样不可以嵌套。
此设置仅影响语法上位于 unchecked
块内的语句。 在块中调用的函数不会此影响。
错误处理及异常:Assert, Require, Revert
Solidity 使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。
如果异常在子调用发生,那么异常会自动冒泡到顶层(例如:异常会重新抛出),除非他们在 try/catch
语句中捕获了错误。 但是如果是在 send
和 低级别如: call
, delegatecall
和 staticcall
的调用里发生异常时, 他们会返回 false
(第一个返回值) 而不是冒泡异常。
异常可以包含错误数据,以 error 示例 的形式传回给调用者。 内置的错误 Error(string)
和 Panic(uint256)
被作为特殊函数使用,下面将解释。 Error
用于 “常规” 错误条件,而 Panic
用于在(无bug)代码中不应该出现的错误。
**用 assert
检查异常(Panic) 和 require
检查错误(Error)
函数 assert
和 require
可用于检查条件并在条件不满足时抛出异常。
assert
函数会创建一个 Panic(uint256)
类型的错误。 同样的错误在以下列出的特定情形会被编译器创建。
assert
函数应该只用于测试内部错误,检查不变量,正常的函数代码永远不会产生Panic, 甚至是基于一个无效的外部输入时。 如果发生了,那就说明出现了一个需要你修复的 bug。如果使用得当,语言分析工具可以识别出那些会导致 Panic 的 assert
条件和函数调用。
下列情况将会产生一个Panic异常: 错误数据会提供的错误码编号,用来指示Panic的类型:
-
0x00: 用于常规编译器插入的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: 如果你调用了零初始化内部函数类型变量。
require
函数可以创建无错误提示的错误,也可以创建一个 Error(string)
类型的错误。 require
函数应该用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。
下列情况将会产生一个 Error(string)
(或无错误提示)的错误:
-
如果你调用 require(x)
,而x
结果为false
。 -
如果你使用 revert()
或者revert("description")
。 -
如果你在不包含代码的合约上执行外部函数调用。 -
如果你通过合约接收以太币,而又没有 payable
修饰符的公有函数(包括构造函数和 fallback 函数)。 -
如果你的合约通过公有 getter 函数接收 Ether 。
在下面的情况下,来自外部调用的错误数据(如果提供的话)被转发,这意味可能 Error 或 Panic 都有可能触发。
-
如果 .transfer()
失败。 -
如果你通过消息调用调用某个函数,但该函数没有正确结束(例如, 它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),不包括使用低级别 call
,send
,delegatecall
,callcode
或staticcall
的函数调用。低级操作不会抛出异常,而通过返回false
来指示失败。 -
如果您使用 new
关键字创建合约,但合约创建没有正确完成。
你可以选择给 require
提供一个消息字符串,但 assert
不行。
在下例中,你可以看到如何轻松使用 require
检查输入条件以及如何使用 assert
检查内部错误.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
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;
}
}
在内部, Solidity 对异常执行回退操作(指令 0xfd
),从而让 EVM 回退对状态所做的所有更改。回退的原因是无法安全地继续执行,因为无法达到预期的结果。 因为我们想要保持交易的原子性,最安全的动作是回退所有的更改,并让整个交易(或至少调用)没有任何新影响。
在这两种情况下,调用者都可以使用 try
/ catch
来应对此类失败,但是被调用函数的更改将始终被还原。
revert
可以使用 revert
语句和 revert
函数来直接触发回退。
revert
语句将一个自定义的错误作为直接参数,没有括号:
revert CustomError(arg1, arg2);
由于向后兼容,还有一个 revert()
函数,它使用圆括号接受一个字符串:
revert(); revert(“description”);
错误数据将被传回给调用者,以便在那里捕获到错误数据。 使用 revert()
会触发一个没有任何错误数据的回退,而 revert("description")
会产生一个 Error(string)
错误。
使用一个自定义的错误实例通常会比字符串描述便宜得多。因为你可以使用错误名来描述它,它只被编码为四个字节。更长的描述可以通过NatSpec提供,这不会产生任何费用。
下面的例子显示了如何使用一个错误字符串和一个自定义错误实例,他们和 revert
或相应的 require
一起使用。
contract VendingMachine {
address owner;
error Unauthorized();
function buy(uint amount) public payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 另一个可选的方式:
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// 以下执行购买逻辑
}
function withdraw() public {
if (msg.sender != owner)
revert Unauthorized();
payable(msg.sender).transfer(address(this).balance);
}
}
只要参数没有额外的附加效果,使用 if (!condition) revert(...);
和 require(condition, ...);
是等价的,例如当参数是字符串的情况。
如果是调用 Error(string)
函数,这里提供的字符串将经过 ABI 编码。 在上边的例子里, revert("Not enough Ether provided.");
会产生如下的十六进制错误返回值:
0x08c379a0 // Error(string) 的函数选择器
0x0000000000000000000000000000000000000000000000000000000000000020 // 数据的偏移量(32)
0x000000000000000000000000000000000000000000000000000000000000001a // 字符串长度(26)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串数据("Not enough Ether provided." 的 ASCII 编码,26字节)
提示信息可以通过 try/catch
(下面介绍)来获取到。
try/catch
外部调用的失败,可以通过 try/catch 语句来捕获,例如:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
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 Panic(uint /*errorCode*/) {
// This is executed in case of a panic,
// i.e. a serious error like division by zero
// or overflow. The error code can be used
// to determine the kind of error.
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// This is executed in case revert() was used。
errorCount++;
return (0, false);
}
}
}
try
关键词后面必须有一个表达式,代表外部函数调用或合约创建( new ContractName()
)。
在表达式上的错误不会被捕获(例如,如果它是一个复杂的表达式,还涉及内部函数调用),只有外部调用本身发生的revert 可以捕获。 接下来的 returns
部分(是可选的)声明了与外部调用返回的类型相匹配的返回变量。 在没有错误的情况下,这些变量被赋值,合约将继续执行第一个成功块内代码。 如果到达成功块的末尾,则在 catch
块之后继续执行。
Solidity 根据错误的类型,支持不同种类的捕获代码块:
-
catch Error(string memory reason) { ... }
: 如果错误是由revert("reasonString")
或require(false, "reasonString")
(或导致这种异常的内部错误)引起的,则执行这个catch子句。 -
catch Panic(uint errorCode) { ... }
: 如果错误是由 panic 引起的(如:assert
失败,除以0,无效的数组访问,算术溢出等),将执行这个catch子句。 -
catch (bytes memory lowLevelData) { ... }
: 如果错误签名不符合任何其他子句,如果在解码错误信息时出现了错误,或者如果异常没有一起提供错误数据。在这种情况下,子句声明的变量提供了对低级错误数据的访问。 -
catch { ... }
: 如果你对错误数据不感兴趣,你可以直接使用catch { ... }
(甚至是作为唯一的catch子句) 而不是前面几个catch子句。
为了捕捉所有的错误情况,你至少要有子句 catch { ... }
或 catch (bytes memory lowLevelData) { ... }
.
在 returns
和 catch
子句中声明的变量只在后面的块的范围内有效。
本文由 mdnice 多平台发布