solidity--语言基础

# solidity源文件结构 - `// SPDX-License-Identifier: MIT` - `pragma solidity ^0.5.2;` - `pragma abicoder v1;` - `import "filename";` - 注释 # 智能合约组成 ### 状态变量 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract SimpleStorage { uint storedData; // State variable // ... } ``` ### 函数/方法 ```solidity // 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; } ``` ### 修饰器 ```solidity // 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 // ... } } ``` ### 事件 ```solidity // 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 } } ``` ### 异常 ```solidity // 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; // ... } } ``` ### 结构类型 ```solidity // 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; } } ``` ### 枚举类型 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract Purchase { enum State { Created, Locked, Inactive } // Enum } ``` # 变量类型 Solidity 是一种静态类型语言,这意味着需要指定每个变量(状态和局部)的类型。Solidity 提供了几种基本类型,它们可以组合起来形成复杂类型。 此外,类型可以在包含运算符的表达式中相互交互。有关各种运算符的快速参考,请参阅[运算符的优先顺序](https://docs.soliditylang.org/en/latest/types.html#order)。 “undefined”或“null值的概念在 Solidity 中不存在,但新声明的变量始终具有取决于其类型的[默认值。](https://docs.soliditylang.org/en/latest/control-structures.html#default-value)要处理任何意外值,您应该使用[revert 函数](https://docs.soliditylang.org/en/latest/control-structures.html#assert-and-require)来回滚整个事务,或者返回一个元组,其中第二个`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( )` 显式转换。 - 对于 `uint160`、整型文字、`bytes20` 和合约类型,允许与地址进行显式转换。 - 只有`address` 和合约类型的表达式才能通过显式转换`payable(...)` 转换成`address payable` 类型。 对于合约类型,只有当合约可以接收以太币时才允许这种转换,即合约具有接收或支付回退功能。 请注意,`payable(0)` 是有效的并且是此规则的例外。 ## address类型支持的方法 - `balance` :查询地址的余额 - `transfer`:发送eth到可支付的地址(如果当前合约没有足够多的余额或eth交易被接受地址拒绝,该方法会失败回滚) ```solidity 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"));` ```solidity 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 您可以获得给定枚举的最小值和最大值。 ```solidity // 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 位小数的十进制定点类型和用于对该类型执行算术运算的最小库。 ```solidity // 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`。 ## 方法类型 函数类型是函数的类型。 函数类型的变量可以从函数中赋值,函数类型的函数参数可用于将函数传递给函数并从函数调用返回函数。 函数类型有两种类型——内部函数和外部函数: 内部函数只能在当前合约内部调用(更具体地说,在当前代码单元内部,也包括内部库函数和继承函数),因为它们不能在当前合约的上下文之外执行。 调用内部函数是通过跳转到其入口标签来实现的,就像在内部调用当前合约的函数一样。 外部函数由地址和函数签名组成,它们可以通过外部函数调用传递和返回。 函数类型表示如下: ```solidity function () {internal|external} [pure|view|payable] [returns ()] ``` 与参数类型相反,返回类型不能为空 - 如果函数类型不应返回任何内容,则必须省略整个`returns ()`部分。 默认情况下,函数类型是内部的,因此可以省略`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` 🥸 由于闰秒造成的每年不都是 365 天、每天不都是 24 小时 [leap seconds](https://en.wikipedia.org/wiki/Leap_second) ,所以如果你要使用这些单位计算日期和时间,请注意这个问题。因为闰秒是无法预测的,所以需要借助外部的预言机(oracle,是一种链外数据服务,译者注)来对一个确定的日期代码库进行时间矫正。 这些后缀不能直接用在变量后边。如果想用时间单位(例如 days)来将输入变量换算为时间,你可以用如下方式来完成: ```solidity 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](https://eips.ethereum.org/EIPS/eip-3198) 和 [EIP-1559](https://eips.ethereum.org/EIPS/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](https://learnblockchain.cn/docs/solidity/abi-spec.html#abi) - 对给定参数进行编码 - `abi.encodePacked(...) returns (bytes)`:对给定参数执行 [紧打包编码](https://learnblockchain.cn/docs/solidity/abi-spec.html#abi-packed-mode) ,注意,可以不明确打包编码。 - `abi.encodeWithSelector(bytes4 selector, ...) returns (bytes)`: [ABI](https://learnblockchain.cn/docs/solidity/abi-spec.html#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, (...))` 一致。 🥸 这些编码函数可以用来构造函数调用数据,而不用实际进行调用。此外,`keccak256(abi.encodePacked(a, b))`  是一种计算结构化数据的哈希值(尽管我们也应该关注到:使用不同的函数参数类型也有可能会引起“哈希冲突” )的方式,不推荐使用的 `keccak256(a, b)`  。 ### 错误处理 - **`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](https://learnblockchain.cn/docs/solidity/types.html#address) ,如果需要转移资金到恢复的地址。 ### 地址成员 - **` .balance` (`uint256`)**以 Wei 为单位的 [地址类型 Address](https://learnblockchain.cn/docs/solidity/types.html#address) 的余额。 - **` .code` (`bytes memory`)**在 [地址类型 Address](https://learnblockchain.cn/docs/solidity/types.html#address) 上的代码(可以为空) - **` .codehash` (`bytes32`)**[地址类型 Address](https://learnblockchain.cn/docs/solidity/types.html#address) 的codehash - **`.transfer(uint256 amount)`**向 [地址类型 Address](https://learnblockchain.cn/docs/solidity/types.html#address) 发送数量为 amount 的 Wei,失败时抛出异常,使用固定(不可调节)的 2300 gas 的矿工费。 - **`.send(uint256 amount) returns (bool)`**向 [地址类型 Address](https://learnblockchain.cn/docs/solidity/types.html#address) 发送数量为 amount 的 Wei,失败时返回 `false`,发送 2300 gas 的矿工费用,不可调节。 - **` .call(bytes memory) returns (bool, bytes memory)`**用给定的有效载荷(payload)发出低级 `CALL` 调用,返回成功状态及返回数据,发送所有可用 gas,也可以调节 gas。 - **` .delegatecall(bytes memory) returns (bool, bytes memory)`**用给定的有效载荷 发出低级 `DELEGATECALL` 调用 ,返回成功状态并返回数据,发送所有可用 gas,也可以调节 gas。 发出低级函数 `DELEGATECALL`,失败时返回 `false`,发送所有可用 gas,可调节。 - **` .staticcall(bytes memory) returns (bool, bytes memory)`**用给定的有效载荷 发出低级 `STATICCALL` 调用 ,返回成功状态并返回数据,发送所有可用 gas,也可以调节 gas。 ### 合约相关 - **`this` (当前的合约类型)**当前合约,可以显示转换为 [地址类型 Address](https://learnblockchain.cn/docs/solidity/types.html#address)。 - **`selfdestruct(address payable recipient)`**销毁合约,并把余额发送到指定 [地址类型 Address](https://learnblockchain.cn/docs/solidity/types.html#address)。请注意, `selfdestruct` 具有从EVM继承的一些特性: - 接收合约的 receive 函数 不会执行。   - - 合约仅在交易结束时才真正被销毁,并且 `revert` 可能会“撤消”销毁。 此外,当前合约内的所有函数都可以被直接调用,包括当前函数。 # 表达式和控制结构 ## 控制结构 JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了 `switch` 和 `goto`。 因此 Solidity 中有 `if`, `else`, `while`, `do`, `for`, `break`, `continue`, `return`, `? :` 这些与在 C 或者 JavaScript 中表达相同语义的关键词。 Solidity还支持 `try`/ `catch` 语句形式的异常处理,但仅用于 [外部函数调用](https://learnblockchain.cn/docs/solidity/control-structures.html#external-function-calls) 和合约创建调用。 使用revert 语句  可以触发一个”错误”。 用于表示条件的括号 *不可以* 被省略,单语句体两边的花括号可以被省略。 注意,与 C 和 JavaScript 不同, Solidity 中非布尔类型数值不能转换为布尔类型,因此 `if (1) { ... }` 的写法在 Solidity 中 *无效* 。 ## 函数调用 ### 内部函数调用 当前合约中的函数可以直接(“从内部”)调用,也可以递归调用,就像下边这个无意义的例子一样: ```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(); } } ``` ![](https://upload-images.jianshu.io/upload_images/6687907-e9ea38290c539bc5.png) 这些函数调用在 EVM 中被解释为简单的跳转。这样做的效果就是当前内存不会被清除,例如,函数之间通过传递内存引用进行内部调用是非常高效的。 只能在同一合约实例的函数,可以进行内部调用。 只有在同一合约的函数可以内部调用。仍然应该避免过多的递归调用, 因为每个内部函数调用至少使用一个堆栈槽, 并且最多有1024堆栈槽可用。 ### 外部函数调用 可以使用表达式 `this.g(8);` 和 `c.g(2);` 进行调用,其中 `c` 是合约实例, `g` 合约内实现的函数,但是这两种方式调用函数,称为“外部调用”,它是通过消息调用来进行,而不是直接的代码跳转。 请注意,不可以在构造函数中通过 this 来调用函数,因为此时真实的合约实例还没有被创建。 如果想要调用其他合约的函数,需要外部调用。对于一个外部调用,所有的函数参数都需要被复制到内存。 🥸 从一个合约到另一个合约的函数调用不会创建自己的交易, 它是作为整个交易的一部分的消息调用。 当调用其他合约的函数时,需要在函数调用是指定发送的 Wei 和 gas 数量,可以使用特定选项 `{value: 10, gas: 10000}` 请注意,不建议明确指定gas,因为操作码的 gas 消耗将来可能会发生变化。 任何发送给合约 Wei 将被添加到目标合约的总余额中: ```solidity 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解码器将捕捉到不存在的合约的情况。 请注意,这个检查在 [低级别调用](https://learnblockchain.cn/docs/solidity/units-and-global-variables.html#address-related) 时不被执行,这些调用是对地址而不是合约实例进行操作。 如果被调用合约本身抛出异常或者 gas 用完等,函数调用也会抛出异常。 ⚠️ 任何与其他合约的交互都会产生潜在危险,尤其是在不能预先知道合约代码的情况下。 交互时当前合约会将控制权移交给被调用合约,而被调用合约可能做任何事。即使被调用合约从一个已知父合约继承,继承的合约也只需要有一个正确的接口就可以了。 被调用合约的实现可以完全任意的实现,因此会带来危险。 此外,请小心这个交互调用在返回之前再回调我们的合约,这意味着被调用合约可以通过它自己的函数改变调用合约的状态变量。 一个建议的函数写法是,例如,在合约中状态变量进行各种变化后再调用外部函数,这样,你的合约就不会轻易被滥用的重入攻击 (reentrancy) 所影响 ### ****具名参数函数调用**** 函数调用参数可以按名称以任何顺序给出,如果它们包含在 {} 中,如以下示例所示。 参数列表的名称必须与函数声明中的参数列表一致,但顺序可以是任意的。 ```solidity 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 语句将值返回给调用者。 ```solidity pragma solidity >=0.4.22 <0.9.0; contract C { // 省略参数名称 function func(uint k, uint) public pure returns(uint) { return k; } } ``` ## ****通过 `new` 创建合约** 使用关键字 `new` 可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。 ```solidity // 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”)。 这允许在创建合同时更加灵活:您可以在创建新合同之前派生出新合同的地址。 此外,如果创建合约同时创建其他合约,您也可以依赖此地址。 一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。 ```solidity // 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); } } ``` ⚠️ 使用 create2 创建合约还有一些特别之处。 合约销毁后可以在同一地址重新创建。不过,即使创建字节码(creation bytecode)相同(这是要求,因为否则地址会发生变化),该新创建的合约也可能有不同的部署字节码(deployed bytecode)。 这是因为构造函数可以使用两次创建合约之间可能已更改的外部状态,并在存储合约时将其合并到部署字节码中。 ## 赋值 ### ****解构赋值和返回多值**** Solidity 内部允许元组 (tuple) 类型,也就是一个在编译时元素数量固定的对象列表,列表中的元素可以是不同类型的对象。这些元组可以用来同时返回多个数值,也可以用它们来同时给多个新声明的变量或者既存的变量(或通常的 LValues)赋值: ```solidity 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` , 因为只传递引用而不传递副本。 ```solidity // 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 语句块等。 那些定义在代码块之外的变量,比如函数、合约、自定义类型等等,并不会影响它们的作用域特性。这意味着你可以在实际声明状态变量的语句之前就使用它们,并且递归地调用函数。 基于以上的规则,下边的例子不会出现编译警告,因为那两个变量虽然名字一样,但却在不同的作用域里。 ```solidity pragma solidity >=0.5.0 <0.9.0; contract C { function minimalScoping() pure public { { uint same; same = 1; } { uint same; same = 3; } } } ``` 作为 C99 作用域规则的特例,请注意在下边的例子里,第一次对 `x`  的赋值会改变上一层中声明的变量值。如果外层声明的变量被“覆盖”(就是说被在内部作用域中由一个同名变量所替代)你会得到一个警告。 ```solidity 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` 代码块: ```solidity // 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 示例](https://learnblockchain.cn/docs/solidity/contracts.html#errors)  的形式传回给调用者。 内置的错误 `Error(string)`  和 `Panic(uint256)`  被作为特殊函数使用,下面将解释。 `Error`  用于 “常规” 错误条件,而 `Panic`  用于在(无bug)代码中不应该出现的错误。 ## ****用 `assert` 检查异常(Panic) 和 `require` 检查错误(Error)** 函数 `assert` 和 `require` 可用于检查条件并在条件不满足时抛出异常。 `assert` 函数会创建一个 `Panic(uint256)` 类型的错误。 同样的错误在以下列出的特定情形会被编译器创建。 `assert` 函数应该只用于测试内部错误,检查不变量,正常的函数代码永远不会产生Panic, 甚至是基于一个无效的外部输入时。 如果发生了,那就说明出现了一个需要你修复的 bug。如果使用得当,语言分析工具可以识别出那些会导致 Panic 的 `assert` 条件和函数调用。 下列情况将会产生一个Panic异常: 错误数据会提供的错误码编号,用来指示Panic的类型: 1. 0x00: 用于常规编译器插入的Panic。 2. 0x01: 如果你调用 `assert` 的参数(表达式)结果为 false 。 3. 0x11: 在 `unchecked { ... }` 外,如果算术运算结果向上或向下溢出。 4. 0x12; 如果你用零当除数做除法或模运算(例如 `5 / 0` 或 `23 % 0` )。 5. 0x21: 如果你将一个太大的数或负数值转换为一个枚举类型。 6. 0x22: 如果你访问一个没有正确编码的存储byte数组. 7. 0x31: 如果在空数组上 `.pop()` 。 8. 0x32: 如果你访问 `bytesN` 数组(或切片)的索引太大或为负数。(例如: `x[i]` 而 `i >= x.length` 或 `i < 0`). 9. 0x41: 如果你分配了太多的内内存或创建了太大的数组。 10. 0x51: 如果你调用了零初始化内部函数类型变量。 `require`函数可以创建无错误提示的错误,也可以创建一个 `Error(string)`类型的错误。 `require`函数应该用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。 下列情况将会产生一个 `Error(string)` (或无错误提示)的错误: 1. 如果你调用 `require(x)` ,而 `x` 结果为 `false` 。 2. 如果你使用 `revert()` 或者 `revert("description")` 。 3. 如果你在不包含代码的合约上执行外部函数调用。 4. 如果你通过合约接收以太币,而又没有 `payable` 修饰符的公有函数(包括构造函数和 fallback 函数)。 5. 如果你的合约通过公有 getter 函数接收 Ether 。 在下面的情况下,来自外部调用的错误数据(如果提供的话)被转发,这意味可能 Error 或 Panic 都有可能触发。 1. 如果 `.transfer()` 失败。 2. 如果你通过消息调用调用某个函数,但该函数没有正确结束(例如, 它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),不包括使用低级别 `call` , `send` , `delegatecall` , `callcode` 或 `staticcall` 的函数调用。低级操作不会抛出异常,而通过返回 `false` 来指示失败。 3. 如果您使用 `new` 关键字创建合约,但合约创建没有正确完成。 你可以选择给 `require`提供一个消息字符串,但 `assert`不行。 ⚠️ 如果你没有为 `require`提供一个字符串参数,它会用空错误数据进行 revert, 甚至不包括错误选择器。 在下例中,你可以看到如何轻松使用 `require`检查输入条件以及如何使用 `assert`检查内部错误. ```solidity // 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` 一起使用。 ```solidity 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, ...);`是等价的,例如当参数是字符串的情况。 🥸 `require`是一个像其他函数一样可被执行的函数。 意味着,所有的参数在函数被执行之前就都会被执行。 尤其,在 `require(condition, f())`里,函数 `f`会被执行,即便 `condition`为 True . 如果是调用 `Error(string)`函数,这里提供的字符串将经过 [ABI 编码](https://learnblockchain.cn/docs/solidity/abi-spec.html#abi)。 在上边的例子里, `revert("Not enough Ether provided.");` 会产生如下的十六进制错误返回值: ```solidity 0x08c379a0 // Error(string) 的函数选择器 0x0000000000000000000000000000000000000000000000000000000000000020 // 数据的偏移量(32) 0x000000000000000000000000000000000000000000000000000000000000001a // 字符串长度(26) 0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串数据("Not enough Ether provided." 的 ASCII 编码,26字节) ``` 提示信息可以通过 `try/catch` (下面介绍)来获取到。 ### ****`try/catch`**** 外部调用的失败,可以通过 try/catch 语句来捕获,例如: ```solidity // 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](https://mdnice.com/?platform=6)多平台发布
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • solidity基础-1 ReadMe测试环境:系统win10x64,solidity版本:0.4.18声明:该笔...
    Lnhj阅读 535评论 0 1
  • Solidity Solidity是编写智能合约的语言,运行在ethereum虚拟机上。语法类似于JS,它拥有异常...
    暴走的K哥哥阅读 4,902评论 2 2
  •   Solidity是传说中编写智能合约的脚本语言,运行在EVM中;用以解决区块链中的任务执行。一个目前看起来还非...
    杨强AT南京阅读 5,573评论 0 1
  • 上一篇:智能合约编程语言 - solidity快速入门(上) solidity区块及交易属性 在介绍区块及交易属性...
    端碗吹水阅读 1,966评论 0 3
  • solidity的自定义结构体深入详解结构体,solidity中的自定义类型,我们可以使用关键字struct来进行...
    Lnhj阅读 1,115评论 0 0