Solidity

Solidity logo

Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在以太坊虚拟机(EVM)上运行。

Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性。

下面您将会看到,使用 Solidity 语言,可以为投票、众筹、秘密竞价(盲拍)、多重签名的钱包以及其他应用创建合约。

注解

目前尝试 Solidity 编程的最好的方式是使用 Remix (需要时间加载,请耐心等待)。Remix 是一个基于 Web 浏览器的 IDE,它可以让你编写 Solidity 智能合约,然后部署并运行该智能合约。

警告

因为软件是人编写的,就会有 bug,所以,创建智能合约也应该遵循软件开发领域熟知的最佳实践。这些实践包括代码审查、测试、审计和正确性证明。也请注意,有时候用户在代码方面比软件的作者更谙熟。最后,区块链本身有些东西需要留意,请参考 安全考量

翻译

本文档由社区志愿者翻译成多种语言,但是 英语版本 作为主要参考。

可用的 Solidity 集成

  • Remix
    基于浏览器的 IDE,集成了编译器和 Solidity 运行时环境,不需要服务端组件。
  • IntelliJ IDEA plugin
    IntelliJ IDEA 的 Solidity 插件(可用于其他所有的 JetBrains IDE)
  • Visual Studio Extension
    Microsoft Visual Studio 的 Solidity 插件,包含 Solidity 编译器。
  • Package for SublimeText — Solidity language syntax
    SublimeText 编辑器的语法高亮包。
  • Etheratom
    Atom 编辑器的插件,支持高亮、编译和运行时环境(兼容后端节点和虚拟机)。
  • Atom Solidity Linter
    Atom 编辑器的插件,提供 Solidity 语言的 Lint 检查(静态检查)。
  • Atom Solium Linter
    Atom 的可配置的 Solidty 静态检查器,基于 Solium。
  • Solium
    一种静态检查器,识别和修复 Solidity 中的风格以及安全问题。
  • Solhint
    一种静态检查器,提供安全和风格指南以及智能合约验证的最佳实践规则。
  • Visual Studio Code extension
    Microsoft Visual Studio Code 插件,包含语法高亮和 Solidity 编译器。
  • Emacs Solidity
    Emacs 编辑器的插件,提供语法高亮和编译错误报告。
  • Vim Solidity
    Vim 编辑器的插件,提供语法高亮。
  • Vim Syntastic
    Vim 编辑器的插件,提供编译检查。

不再维护:

  • Mix IDE
    基于 Qt 的 IDE,可以设计、调试和测试 Solidity 智能合约。
  • Ethereum Studio
    专门的网页 IDE,也提供一个完整以太坊环境的脚本访问。

Solidity 工具列表

  • Dapp
    Solidity 语言的构建工具、包管理器以及部署助手。
  • Solidity REPL
    一个命令行控制台,可以让你立刻尝试 Solidity 语言。
  • solgraph
    可视化的 Solidity 控制流,并能标明潜在的安全漏洞。
  • evmdis
    EVM 反汇编程序,可以执行字节码的静态分析,能提供比 EVM 操作更高级的抽象。
  • Doxity
    Solidity 语言的文档生成器。

第三方 Solidity 解析器和语法

语言文档

下面的页面中,我们首先会看到一个使用 Solidity 写的 简单智能合约,随后讲解 区块链 基础,然后是 以太坊虚拟机

下一节会通过给出有用的 合约样例 ,解释 Solidity 的几个*特性* ,记住你都可以 在你的浏览器中 尝试这些合约!

最后也是最长的一节会深入讲解 Solidity 的所有方面。

如果还有问题,你可以尝试搜索或在 Ethereum Stackexchange 上提问,或者到我们的`gitter 频道 <https://gitter.im/ethereum/solidity/>`_ 来。随时欢迎改善 Solidity 或本文档的想法!

目录

Keyword Index, Search Page

智能合约概述

简单的智能合约

让我们先看一下最基本的例子。现在就算你都不理解也不要紧,后面我们会有更深入的讲解。

存储

pragma solidity ^0.4.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

第一行就是告诉大家源代码使用Solidity版本0.4.0写的,并且使用0.4.0以上版本运行也没问题(最高到0.5.0,但是不包含0.5.0)。这是为了确保合约不会在新的编译器版本中突然行为异常。关键字 pragma 的含义是,一般来说,pragmas(编译指令)是告知编译器如何处理源代码的指令的(例如, pragma once )。

Solidity中合约的含义就是一组代码(它的 函数 )和数据(它的 状态 ),它们位于以太坊区块链的一个特定地址上。 代码行 uint storedData; 声明一个类型为 uint (256位无符号整数)的状态变量,叫做 storedData 。 你可以认为它是数据库里的一个位置,可以通过调用管理数据库代码的函数进行查询和变更。对于以太坊来说,上述的合约就是拥有合约(owning contract)。在这种情况下,函数 setget 可以用来变更或取出变量的值。

要访问一个状态变量,并不需要像 this. 这样的前缀,虽然这是其他语言常见的做法。

该合约能完成的事情并不多(由于以太坊构建的基础架构的原因):它能允许任何人在合约中存储一个单独的数字,并且这个数字可以被世界上任何人访问,且没有可行的办法阻止你发布这个数字。当然,任何人都可以再次调用 set ,传入不同的值,覆盖你的数字,但是这个数字仍会被存储在区块链的历史记录中。随后,我们会看到怎样施加访问限制,以确保只有你才能改变这个数字。

注解

所有的标识符(合约名称,函数名称和变量名称)都只能使用ASCII字符集。UTF-8编码的数据可以用字符串变量的形式存储。

警告

小心使用Unicode文本,因为有些字符虽然长得相像(甚至一样),但其字符码是不同的,其编码后的字符数组也会不一样。

子货币(Subcurrency)例子

下面的合约实现了一个最简单的加密货币。这里,币确实可以无中生有地产生,但是只有创建合约的人才能做到(实现一个不同的发行计划也不难)。而且,任何人都可以给其他人转币,不需要注册用户名和密码 —— 所需要的只是以太坊密钥对。

pragma solidity ^0.4.21;

contract Coin {
    // 关键字“public”让这些变量可以从外部读取
    address public minter;
    mapping (address => uint) public balances;

    // 轻客户端可以通过事件针对变化作出高效的反应
    event Sent(address from, address to, uint amount);

    // 这是构造函数,只有当合约创建时运行
    function Coin() public {
        minter = msg.sender;
    }

    function mint(address receiver, uint amount) public {
        if (msg.sender != minter) return;
        balances[receiver] += amount;
    }

    function send(address receiver, uint amount) public {
        if (balances[msg.sender] < amount) return;
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

这个合约引入了一些新的概念,让我们逐一解读。

address public minter; 这一行声明了一个可以被公开访问的 address 类型的状态变量。 address 类型是一个160位的值,且不允许任何算数操作。这种类型适合存储合约地址或外部人员的密钥对。关键字 public 自动生成一个函数,允许你在这个合约之外访问这个状态变量的当前值。如果没有这个关键字,其他的合约没有办法访问这个变量。由编译器生成的函数的代码大致如下所示:

function minter() returns (address) { return minter; }

当然,加一个和上面完全一样的函数是行不通的,因为我们会有同名的一个函数和一个变量,这里,主要是希望你能明白——编译器已经帮你实现了。

下一行, mapping (address => uint) public balances; 也创建一个公共状态变量,但它是一个更复杂的数据类型。 该类型将address映射为无符号整数。 Mappings 可以看作是一个 哈希表 它会执行虚拟初始化,以使所有可能存在的键都映射到一个字节表示为全零的值。 但是,这种类比并不太恰当,因为它既不能获得映射的所有键的列表,也不能获得所有值的列表。 因此,要么记住你添加到mapping中的数据(使用列表或更高级的数据类型会更好),要么在不需要键列表或值列表的上下文中使用它,就如本例。 而由 public 关键字创建的getter函数 getter function 则是更复杂一些的情况, 它大致如下所示:

function balances(address _account) public view returns (uint) {
    return balances[_account];
}

正如你所看到的,你可以通过该函数轻松地查询到账户的余额。

event Sent(address from, address to, uint amount); 这行声明了一个所谓的“事件(event)”,它会在 send 函数的最后一行被发出。用户界面(当然也包括服务器应用程序)可以监听区块链上正在发送的事件,而不会花费太多成本。一旦它被发出,监听该事件的listener都将收到通知。而所有的事件都包含了 fromtoamount 三个参数,可方便追踪事务。 为了监听这个事件,你可以使用如下代码:

Coin.Sent().watch({}, '', function(error, result) {
    if (!error) {
        console.log("Coin transfer: " + result.args.amount +
            " coins were sent from " + result.args.from +
            " to " + result.args.to + ".");
        console.log("Balances now:\n" +
            "Sender: " + Coin.balances.call(result.args.from) +
            "Receiver: " + Coin.balances.call(result.args.to));
    }
})

这里请注意自动生成的 balances 函数是如何从用户界面调用的。

特殊函数 Coin 是在创建合约期间运行的构造函数,不能在事后调用。 它永久存储创建合约的人的地址: msg (以及 txblock ) 是一个神奇的全局变量,其中包含一些允许访问区块链的属性。 msg.sender 始终是当前(外部)函数调用的来源地址。

最后,真正被用户或其他合约所调用的,以完成本合约功能的方法是 mintsend。 如果 mint 被合约创建者外的其他人调用则什么也不会发生。 另一方面, send 函数可被任何人用于向他人发送币 (当然,前提是发送者拥有这些币)。记住,如果你使用合约发送币给一个地址,当你在区块链浏览器上查看该地址时是看不到任何相关信息的。因为,实际上你发送币和更改余额的信息仅仅存储在特定合约的数据存储器中。通过使用事件,你可以非常简单地为你的新币创建一个“区块链浏览器”来追踪交易和余额。

区块链基础

对于程序员来说,区块链这个概念并不难理解,这是因为大多数难懂的东西 (挖矿, 哈希椭圆曲线密码学点对点网络(P2P) 等) 都只是用于提供特定的功能和承诺。你只需接受这些既有的特性功能,不必关心底层技术,比如,难道你必须知道亚马逊的 AWS 内部原理,你才能使用它吗?

交易/事务

区块链是全球共享的事务性数据库,这意味着每个人都可加入网络来阅读数据库中的记录。如果你想改变数据库中的某些东西,你必须创建一个被所有其他人所接受的事务。事务一词意味着你想做的(假设您想要同时更改两个值),要么一点没做,要么全部完成。此外,当你的事务被应用到数据库时,其他事务不能修改数据库。

举个例子,设想一张表,列出电子货币中所有账户的余额。如果请求从一个账户转移到另一个账户,数据库的事务特性确保了如果从一个账户扣除金额,它总被添加到另一个账户。如果由于某些原因,无法添加金额到目标账户时,源账户也不会发生任何变化。

此外,交易总是由发送人(创建者)签名。

这样,就可非常简单地为数据库的特定修改增加访问保护机制。在电子货币的例子中,一个简单的检查可以确保只有持有账户密钥的人才能从中转账。

区块

在比特币中,要解决的一个主要难题,被称为“双花攻击 (double-spend attack)”:如果网络存在两笔交易,都想花光同一个账户的钱时(即所谓的冲突)会发生什么情况?交易互相冲突?

简单的回答是你不必在乎此问题。网络会为你自动选择一条交易序列,并打包到所谓的“区块”中,然后它们将在所有参与节点中执行和分发。如果两笔交易互相矛盾,那么最终被确认为后发生的交易将被拒绝,不会被包含到区块中。

这些块按时间形成了一个线性序列,这正是“区块链”这个词的来源。区块以一定的时间间隔添加到链上 —— 对于以太坊,这间隔大约是17秒。

作为“顺序选择机制”(也就是所谓的“挖矿”)的一部分,可能有时会发生块(blocks)被回滚的情况,但仅在链的“末端”。末端增加的块越多,其发生回滚的概率越小。因此你的交易被回滚甚至从区块链中抹除,这是可能的,但等待的时间越长,这种情况发生的概率就越小。

以太坊虚拟机

概述

以太坊虚拟机 EVM 是智能合约的运行环境。它不仅是沙盒封装的,而且是完全隔离的,也就是说在 EVM 中运行代码是无法访问网络、文件系统和其他进程的。甚至智能合约之间的访问也是受限的。

账户

以太坊中有两类账户(它们共用同一个地址空间): 外部账户 由公钥-私钥对(也就是人)控制; 合约账户 由和账户一起存储的代码控制.

外部账户的地址是由公钥决定的,而合约账户的地址是在创建该合约时确定的(这个地址通过合约创建者的地址和从该地址发出过的交易数量计算得到的,也就是所谓的“nonce”)

无论帐户是否存储代码,这两类账户对 EVM 来说是一样的。

每个账户都有一个键值对形式的持久化存储。其中 key 和 value 的长度都是256位,我们称之为 存储

此外,每个账户有一个以太币余额( balance )(单位是“Wei”),余额会因为发送包含以太币的交易而改变。

交易

交易可以看作是从一个帐户发送到另一个帐户的消息(这里的账户,可能是相同的或特殊的零帐户,请参阅下文)。它能包含一个二进制数据(合约负载)和以太币。

如果目标账户含有代码,此代码会被执行,并以 payload 作为入参。

如果目标账户是零账户(账户地址为 0 ),此交易将创建一个 新合约 。 如前文所述,合约的地址不是零地址,而是通过合约创建者的地址和从该地址发出过的交易数量计算得到的(所谓的“nonce”)。 这个用来创建合约的交易的 payload 会被转换为 EVM 字节码并执行。执行的输出将作为合约代码被永久存储。这意味着,为创建一个合约,你不需要发送实际的合约代码,而是发送能够产生合约代码的代码。

注解

在合约创建的过程中,它的代码还是空的。所以直到构造函数执行结束,你都不应该在其中调用合约自己函数。

Gas

一经创建,每笔交易都收取一定数量的 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)"这个基本的数据类型来进行操作。具备常用的算术、位、逻辑和比较操作。也可以做到有条件和无条件跳转。此外,合约可以访问当前区块的相关属性,比如它的编号和时间戳。

消息调用

合约可以通过消息调用的方式来调用其它合约或者发送以太币到非合约账户。消息调用和交易非常类似,它们都有一个源、目标、数据、以太币、gas和返回数据。事实上每个交易都由一个顶层消息调用组成,这个消息调用又可创建更多的消息调用。

合约可以决定在其内部的消息调用中,对于剩余的 gas ,应发送和保留多少。如果在内部消息调用时发生了out-of-gas异常(或其他任何异常),这将由一个被压入栈顶的错误值所指明。此时,只有与该内部消息调用一起发送的gas会被消耗掉。并且,Solidity中,发起调用的合约默认会触发一个手工的异常,以便异常可以从调用栈里“冒泡出来”。 如前文所述,被调用的合约(可以和调用者是同一个合约)会获得一块刚刚清空过的内存,并可以访问调用的payload——由被称为 calldata 的独立区域所提供的数据。调用执行结束后,返回数据将被存放在调用方预先分配好的一块内存中。 调用深度被 限制 为 1024 ,因此对于更加复杂的操作,我们应使用循环而不是递归。

委托调用/代码调用和库

有一种特殊类型的消息调用,被称为 委托调用(delegatecall) 。它和一般的消息调用的区别在于,目标地址的代码将在发起调用的合约的上下文中执行,并且 msg.sendermsg.value 不变。 这意味着一个合约可以在运行时从另外一个地址动态加载代码。存储、当前地址和余额都指向发起调用的合约,只有代码是从被调用地址获取的。 这使得 Solidity 可以实现”库“能力:可复用的代码库可以放在一个合约的存储上,如用来实现复杂的数据结构的库。

日志

有一种特殊的可索引的数据结构,其存储的数据可以一路映射直到区块层级。这个特性被称为 日志(logs) ,Solidity用它来实现 事件(events) 。合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。因为部分日志数据被存储在 布隆过滤器(Bloom filter) 中,我们可以高效并且加密安全地搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。

创建

合约甚至可以通过一个特殊的指令来创建其他合约(不是简单的调用零地址)。创建合约的调用 create calls 和普通消息调用的唯一区别在于,负载会被执行,执行的结果被存储为合约代码,调用者/创建者在栈上得到新合约的地址。

自毁

合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作 selfdestruct 。合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码从状态中被移除。

警告

尽管一个合约的代码中没有显式地调用 selfdestruct ,它仍然有可能通过 delegatecallcallcode 执行自毁操作。

注解

旧合约的删减可能会,也可能不会被以太坊的各种客户端程序实现。另外,归档节点可选择无限期保留合约存储和代码。

注解

目前, 外部账户 不能从状态中移除。

安装Solidity编译器

版本

Solidity的版本遵循 语义化版本原则,作为发布版本的补充, 每日开发构建 (nightly development builds)也是可用的。这个每日开发构建不保证能正常工作,尽管尽了最大的努力,但仍可能包含未记录的和/或重大的改动。我们推荐使用最新的发布版本。下面的包安装程序将使用最新发布版本。

Remix

我们推荐使用 Remix 来开发简单合约和快速学习 Solidity。

Remix 可在线使用,而无需安装任何东西。如果你想离线使用,可按 https://github.com/ethereum/browser-solidity/tree/gh-pages 的页面说明下载 zip 文件来使用。 该页面有进一步详细说明如何安装 Solidity 命令行编译器到你计算机上。如果你刚好要处理大型合约,或者需要更多的编译选项,那么你应该选择使用命令行编译器 solc。

npm / Node.js

使用 npm 可以便捷地安装Solidity编译器solcjs。但该 solcjs 程序的功能相对于本页下面的所有其他选项都要少。在 使用命令行编译器 一章中,我们假定你使用的是完整功能的编译器。 所以,如果你是从 npm 安装 solcjs ,就此打住,直接跳到 solc-js 去了解。

注意: solc-js 项目是利用 Emscripten 从 C++ 版的 solc 跨平台编译为 JavaScript 的,因此,可在 JavaScript 项目中使用 solcjs(如同 Remix)。 具体介绍请参考 solc-js 代码库。

npm install -g solc

注解

在命令行中,使用 solcjs 而非 solcsolcjs 的命令行选项同 solc 和一些工具(如 geth )是不兼容的,因此不要期望 solcjs 能像 solc 一样工作。

Docker

我们为编译器提供了最新的docker构建。 stable 仓库里的是已发布的版本,nightly 仓库则是在开发分支中的带有不稳定变更的版本。

docker run ethereum/solc:stable solc --version

目前,docker 镜像只含有 solc 的可执行程序,因此你需要额外的工作去把源代码和输出目录连接起来。

二进制包

可在 solidity/releases 下载 Solidity 的二进制安装包。

对于 Ubuntu ,我们也提供 PPAs 。通过以下命令,可获取最新的稳定版本:

sudo add-apt-repository ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install solc

当然,你也可安装尝鲜的开发者版本:

sudo add-apt-repository ppa:ethereum/ethereum
sudo add-apt-repository ppa:ethereum/ethereum-dev
sudo apt-get update
sudo apt-get install solc

同时,也提供可安装 所有支持的Linux版本 下的 snap package 。通过以下命令,可获取最新的稳定版本:

sudo snap install solc

或者,如果你想测试 develop 分支下的最新变更,可通过如下方式安装开发者版本:

sudo snap install solc --edge

同样,Arch Linux 也有提供安装包,但仅限于最新的开发者版本:

pacman -S solidity

在写本文时,Homebrew 上还没有提供预构建的二进制包(因为我们从 Jenkins 迁移到了 TravisCI )。 我们将尽快提供 homebrew 下的二进制安装包,但至少从源码构建的方式还是行得通的:

brew update
brew upgrade
brew tap ethereum/ethereum
brew install solidity

如果你需要特定版本的 Solidity ,你需要从 Github 上安装一个 Homebrew formula。 你可查阅 solidity.rb commits on Github 的提交记录,去寻找包含 solidity.rb 文件改动的特殊提交。然后使用 brew 进行安装:

brew unlink solidity
# Install 0.4.8
brew install https://raw.githubusercontent.com/ethereum/homebrew-ethereum/77cce03da9f289e5a3ffe579840d3c5dc0a62717/solidity.rb

Gentoo Linux 下也提供了安装包,可使用 emerge 进行安装:

emerge dev-lang/solidity

从源代码编译

克隆代码库

执行以下命令,克隆源代码:

git clone --recursive https://github.com/ethereum/solidity.git
cd solidity

如果你想参与 Solidity 的开发, 你可分叉 Solidity 源码库后,用你个人的分叉库作为第二远程源:

cd solidity
git remote add personal git@github.com:[username]/solidity.git

Solidity 有 Git 子模块,需确保完全加载它们:

git submodule update --init --recursive

先决条件 - macOS

在 macOS 中,需确保有安装最新版的 Xcode, Xcode 包含 Clang C++ 编译器, 而 Xcode IDE 和其他苹果开发工具是 OS X 下编译 C++ 应用所必须的。 如果你是第一次安装 Xcode 或者刚好更新了 Xcode 新版本,则在使用命令行构建前,需同意 Xcode 的使用协议:

sudo xcodebuild -license accept

Solidity 在 OS X 下构建,必须 安装 Homebrew 包管理器来安装依赖。 如果你想从头开始,这里是 卸载 Homebrew 的方法

先决条件 - Windows

在Windows下构建Solidity,需下载的依赖软件包:

软件 备注
Git for Windows C从Github上获取源码的命令行工具
CMake 跨平台构建文件生成器
Visual Studio 2017 Build Tools C++ 编译器
Visual Studio 2017 (Optional) C++ 编译器和开发环境

如果你已经有了 IDE,仅需要编译器和相关的库,你可以安装 Visual Studio 2017 Build Tools。

Visual Studio 2017 提供了 IDE 以及必要的编译器和库。所以如果你还没有一个 IDE 并且想要开发 Solidity,那么 Visual Studio 2017 将是一个可以使你获得所有工具的简单选择。

这里是一个在 Visual Studio 2017 Build Tools 或 Visual Studio 2017 中应该安装的组件列表:

  • Visual Studio C++ core features
  • VC++ 2017 v141 toolset (x86,x64)
  • Windows Universal CRT SDK
  • Windows 8.1 SDK
  • C++/CLI support

外部依赖

在 macOS、Windows和其他 Linux 发行版上,有一个脚本可以“一键”安装所需的外部依赖库。本来是需要人工参与的多步操作,现在只需一行命令:

./scripts/install_deps.sh

Windows 下执行:

scripts\install_deps.bat

命令行构建

确保你已安装外部依赖(见上面)

Solidity 使用 CMake 来配置构建。Linux、macOS 和其他 Unix系统上的构建方式都差不多:

mkdir build
cd build
cmake .. && make

也有更简单的:

#note: 将安装 solc  soltest  usr/local/bin 目录
./scripts/build.sh

对于 Windows 执行:

mkdir build
cd build
cmake -G "Visual Studio 15 2017 Win64" ..

这组指令的最后一句,会在 build 目录下创建一个 solidity.sln 文件,双击后,默认会使用 Visual Studio 打开。我们建议在VS上创建 RelWithDebugInfo 配置文件。

或者用命令创建:

cmake --build . --config RelWithDebInfo

CMake参数

如果你对 CMake 命令选项有兴趣,可执行 cmake .. -LH 进行查看。

版本号字符串详解

Solidity 版本名包含四部分:

  • 版本号
  • 预发布版本号,通常为 develop.YYYY.MM.DD 或者 nightly.YYYY.MM.DD
  • commit.GITHASH 格式展示的提交号
  • 由若干条平台、编译器详细信息构成的平台标识

如果本地有修改,则 commit 部分有后缀 .mod

这些部分按照 Semver 的要求来组合, Solidity 预发布版本号等价于 Semver 预发布版本号, Solidity 提交号和平台标识则组成 Semver 的构建元数据。

发行版样例:0.4.8+commit.60cc1668.Emscripten.clang.

预发布版样例: 0.4.9-nightly.2017.1.17+commit.6ecb4aa3.Emscripten.clang

版本信息详情

在版本发布之后,补丁版本号会增加,因为我们假定只有补丁级别的变更会在之后发生。当变更被合并后,版本应该根据semver和变更的剧烈程度进行调整。最后,发行版本总是与当前每日构建版本的版本号一致,但没有 prerelease 指示符。

例如:

  1. 0.4.0 版本发布
  2. 从现在开始,每晚构建为 0.4.1 版本
  3. 引入非破坏性变更 —— 不改变版本号
  4. 引入破坏性变更 —— 版本跳跃到 0.5.0
  5. 0.5.0 版本发布

该方式与 version pragma 一起运行良好。

根据例子学习Solidity

投票

以下的合约相当复杂,但展示了很多Solidity的功能。它实现了一个投票合约。 当然,电子投票的主要问题是如何将投票权分配给正确的人员以及如何防止被操纵。 我们不会在这里解决所有的问题,但至少我们会展示如何进行委托投票,同时,计票又是 自动和完全透明的

我们的想法是为每个(投票)表决创建一份合约,为每个选项提供简称。 然后作为合约的创造者——即主席,将给予每个独立的地址以投票权。

地址后面的人可以选择自己投票,或者委托给他们信任的人来投票。

在投票时间结束时,winningProposal() 将返回获得最多投票的提案。

pragma solidity ^0.4.22;

/// @title 委托投票
contract Ballot {
    // 这里声明了一个新的复合类型用于稍后的变量
    // 它用来表示一个选民
    struct Voter {
        uint weight; // 计票的权重
        bool voted;  // 若为真,代表该人已投票
        address delegate; // 被委托人
        uint vote;   // 投票提案的索引
    }

    // 提案的类型
    struct Proposal {
        bytes32 name;   // 简称(最长32个字节)
        uint voteCount; // 得票数
    }

    address public chairperson;

    // 这声明了一个状态变量,为每个可能的地址存储一个 `Voter`。
    mapping(address => Voter) public voters;

    // 一个 `Proposal` 结构类型的动态数组
    Proposal[] public proposals;

    /// 为 `proposalNames` 中的每个提案,创建一个新的(投票)表决
    constructor(bytes32[] proposalNames) public {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;
        //对于提供的每个提案名称,
        //创建一个新的 Proposal 对象并把它添加到数组的末尾。
        for (uint i = 0; i < proposalNames.length; i++) {
            // `Proposal({...})` 创建一个临时 Proposal 对象,
            // `proposals.push(...)` 将其添加到 `proposals` 的末尾
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    // 授权 `voter` 对这个(投票)表决进行投票
    // 只有 `chairperson` 可以调用该函数。
    function giveRightToVote(address voter) public {
        // 若 `require` 的第一个参数的计算结果为 `false`,
        // 则终止执行,撤销所有对状态和以太币余额的改动。
        // 在旧版的 EVM 中这曾经会消耗所有 gas,但现在不会了。
        // 使用 require 来检查函数是否被正确地调用,是一个好习惯。
        // 你也可以在 require 的第二个参数中提供一个对错误情况的解释。
        require(
            msg.sender == chairperson,
            "Only chairperson can give right to vote."
        );
        require(
            !voters[voter].voted,
            "The voter already voted."
        );
        require(voters[voter].weight == 0);
        voters[voter].weight = 1;
    }

    /// 把你的投票委托到投票者 `to`。
    function delegate(address to) public {
        // 传引用
        Voter storage sender = voters[msg.sender];
        require(!sender.voted, "You already voted.");

        require(to != msg.sender, "Self-delegation is disallowed.");

        // 委托是可以传递的,只要被委托者 `to` 也设置了委托。
        // 一般来说,这种循环委托是危险的。因为,如果传递的链条太长,
        // 则可能需消耗的gas要多于区块中剩余的(大于区块设置的gasLimit),
        // 这种情况下,委托不会被执行。
        // 而在另一些情况下,如果形成闭环,则会让合约完全卡住。
        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;

            // 不允许闭环委托
            require(to != msg.sender, "Found loop in delegation.");
        }

        // `sender` 是一个引用, 相当于对 `voters[msg.sender].voted` 进行修改
        sender.voted = true;
        sender.delegate = to;
        Voter storage delegate_ = voters[to];
        if (delegate_.voted) {
            // 若被委托者已经投过票了,直接增加得票数
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            // 若被委托者还没投票,增加委托者的权重
            delegate_.weight += sender.weight;
        }
    }

    /// 把你的票(包括委托给你的票),
    /// 投给提案 `proposals[proposal].name`.
    function vote(uint proposal) public {
        Voter storage sender = voters[msg.sender];
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposal;

        // 如果 `proposal` 超过了数组的范围,则会自动抛出异常,并恢复所有的改动
        proposals[proposal].voteCount += sender.weight;
    }

    /// @dev 结合之前所有的投票,计算出最终胜出的提案
    function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }

    // 调用 winningProposal() 函数以获取提案数组中获胜者的索引,并以此返回获胜者的名称
    function winnerName() public view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

可能的优化

当前,为了把投票权分配给所有参与者,需要执行很多交易。你有没有更好的主意?

秘密竞价(盲拍)

在本节中,我们将展示如何轻松地在以太坊上创建一个秘密竞价的合约。 我们将从公开拍卖开始,每个人都可以看到出价,然后将此合约扩展到盲拍合约, 在竞标期结束之前无法看到实际出价。

简单的公开拍卖

以下简单的拍卖合约的总体思路是每个人都可以在投标期内发送他们的出价。 出价已经包含了资金/以太币,来将投标人与他们的投标绑定。 如果最高出价提高了(被其他出价者的出价超过),之前出价最高的出价者可以拿回她的钱。 在投标期结束后,受益人需要手动调用合约来接收他的钱 - 合约不能自己激活接收。

pragma solidity ^0.4.22;

contract SimpleAuction {
    // 拍卖的参数。
    address public beneficiary;
    // 时间是unix的绝对时间戳(自1970-01-01以来的秒数)
    // 或以秒为单位的时间段。
    uint public auctionEnd;

    // 拍卖的当前状态
    address public highestBidder;
    uint public highestBid;

    //可以取回的之前的出价
    mapping(address => uint) pendingReturns;

    // 拍卖结束后设为 true,将禁止所有的变更
    bool ended;

    // 变更触发的事件
    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    // 以下是所谓的 natspec 注释,可以通过三个斜杠来识别。
    // 当用户被要求确认交易时将显示。

    /// 以受益者地址 `_beneficiary` 的名义,
    /// 创建一个简单的拍卖,拍卖时间为 `_biddingTime` 秒。
    constructor(
        uint _biddingTime,
        address _beneficiary
    ) public {
        beneficiary = _beneficiary;
        auctionEnd = now + _biddingTime;
    }

    /// 对拍卖进行出价,具体的出价随交易一起发送。
    /// 如果没有在拍卖中胜出,则返还出价。
    function bid() public payable {
        // 参数不是必要的。因为所有的信息已经包含在了交易中。
        // 对于能接收以太币的函数,关键字 payable 是必须的。

        // 如果拍卖已结束,撤销函数的调用。
        require(
            now <= auctionEnd,
            "Auction already ended."
        );

        // 如果出价不够高,返还你的钱
        require(
            msg.value > highestBid,
            "There already is a higher bid."
        );

        if (highestBid != 0) {
            // 返还出价时,简单地直接调用 highestBidder.send(highestBid) 函数,
            // 是有安全风险的,因为它有可能执行一个非信任合约。
            // 更为安全的做法是让接收方自己提取金钱。
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    /// 取回出价(当该出价已被超越)
    function withdraw() public returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // 这里很重要,首先要设零值。
            // 因为,作为接收调用的一部分,
            // 接收者可以在 `send` 返回之前,重新调用该函数。
            pendingReturns[msg.sender] = 0;

            if (!msg.sender.send(amount)) {
                // 这里不需抛出异常,只需重置未付款
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    /// 结束拍卖,并把最高的出价发送给受益人
    function auctionEnd() public {
        // 对于可与其他合约交互的函数(意味着它会调用其他函数或发送以太币),
        // 一个好的指导方针是将其结构分为三个阶段:
        // 1. 检查条件
        // 2. 执行动作 (可能会改变条件)
        // 3. 与其他合约交互
        // 如果这些阶段相混合,其他的合约可能会回调当前合约并修改状态,
        // 或者导致某些效果(比如支付以太币)多次生效。
        // 如果合约内调用的函数包含了与外部合约的交互,
        // 则它也会被认为是与外部合约有交互的。

        // 1. 条件
        require(now >= auctionEnd, "Auction not yet ended.");
        require(!ended, "auctionEnd has already been called.");

        // 2. 生效
        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

        // 3. 交互
        beneficiary.transfer(highestBid);
    }
}

秘密竞拍(盲拍)

之前的公开拍卖接下来将被扩展为一个秘密竞拍。 秘密竞拍的好处是在投标结束前不会有时间压力。 在一个透明的计算平台上进行秘密竞拍听起来像是自相矛盾,但密码学可以实现它。

投标期间 ,投标人实际上并没有发送她的出价,而只是发送一个哈希版本的出价。 由于目前几乎不可能找到两个(足够长的)值,其哈希值是相等的,因此投标人可通过该方式提交报价。 在投标结束后,投标人必须公开他们的出价:他们不加密的发送他们的出价,合约检查出价的哈希值是否与投标期间提供的相同。

另一个挑战是如何使拍卖同时做到 绑定和秘密 : 唯一能阻止投标者在她赢得拍卖后不付款的方式是,让她将钱连同出价一起发出。 但由于资金转移在 以太坊Ethereum 中不能被隐藏,因此任何人都可以看到转移的资金。

下面的合约通过接受任何大于最高出价的值来解决这个问题。 当然,因为这只能在披露阶段进行检查,有些出价可能是 无效 的, 并且,这是故意的(与高出价一起,它甚至提供了一个明确的标志来标识无效的出价): 投标人可以通过设置几个或高或低的无效出价来迷惑竞争对手。

pragma solidity >0.4.23 <0.5.0;

contract BlindAuction {
    struct Bid {
        bytes32 blindedBid;
        uint deposit;
    }

    address public beneficiary;
    uint public biddingEnd;
    uint public revealEnd;
    bool public ended;

    mapping(address => Bid[]) public bids;

    address public highestBidder;
    uint public highestBid;

    // 可以取回的之前的出价
    mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);

    /// 使用 modifier 可以更便捷的校验函数的入参。
    /// `onlyBefore` 会被用于后面的 `bid` 函数:
    /// 新的函数体是由 modifier 本身的函数体,并用原函数体替换 `_;` 语句来组成的。
    modifier onlyBefore(uint _time) { require(now < _time); _; }
    modifier onlyAfter(uint _time) { require(now > _time); _; }

    constructor(
        uint _biddingTime,
        uint _revealTime,
        address _beneficiary
    ) public {
        beneficiary = _beneficiary;
        biddingEnd = now + _biddingTime;
        revealEnd = biddingEnd + _revealTime;
    }

    /// 可以通过 `_blindedBid` = keccak256(value, fake, secret)
    /// 设置一个秘密竞拍。
    /// 只有在出价披露阶段被正确披露,已发送的以太币才会被退还。
    /// 如果与出价一起发送的以太币至少为 “value” 且 “fake” 不为真,则出价有效。
    /// 将 “fake” 设置为 true ,然后发送满足订金金额但又不与出价相同的金额是隐藏实际出价的方法。
    /// 同一个地址可以放置多个出价。
    function bid(bytes32 _blindedBid)
        public
        payable
        onlyBefore(biddingEnd)
    {
        bids[msg.sender].push(Bid({
            blindedBid: _blindedBid,
            deposit: msg.value
        }));
    }

    /// 披露你的秘密竞拍出价。
    /// 对于所有正确披露的无效出价以及除最高出价以外的所有出价,你都将获得退款。
    function reveal(
        uint[] _values,
        bool[] _fake,
        bytes32[] _secret
    )
        public
        onlyAfter(biddingEnd)
        onlyBefore(revealEnd)
    {
        uint length = bids[msg.sender].length;
        require(_values.length == length);
        require(_fake.length == length);
        require(_secret.length == length);

        uint refund;
        for (uint i = 0; i < length; i++) {
            Bid storage bid = bids[msg.sender][i];
            (uint value, bool fake, bytes32 secret) =
                    (_values[i], _fake[i], _secret[i]);
            if (bid.blindedBid != keccak256(value, fake, secret)) {
                // 出价未能正确披露
                // 不返还订金
                continue;
            }
            refund += bid.deposit;
            if (!fake && bid.deposit >= value) {
                if (placeBid(msg.sender, value))
                    refund -= value;
            }
            // 使发送者不可能再次认领同一笔订金
            bid.blindedBid = bytes32(0);
        }
        msg.sender.transfer(refund);
    }

    // 这是一个 "internal" 函数, 意味着它只能在本合约(或继承合约)内被调用
    function placeBid(address bidder, uint value) internal
            returns (bool success)
    {
        if (value <= highestBid) {
            return false;
        }
        if (highestBidder != address(0)) {
            // 返还之前的最高出价
            pendingReturns[highestBidder] += highestBid;
        }
        highestBid = value;
        highestBidder = bidder;
        return true;
    }

    /// 取回出价(当该出价已被超越)
    function withdraw() public {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // 这里很重要,首先要设零值。
            // 因为,作为接收调用的一部分,
            // 接收者可以在 `transfer` 返回之前重新调用该函数。(可查看上面关于‘条件 -> 影响 -> 交互’的标注)
            pendingReturns[msg.sender] = 0;

            msg.sender.transfer(amount);
        }
    }

    /// 结束拍卖,并把最高的出价发送给受益人
    function auctionEnd()
        public
        onlyAfter(revealEnd)
    {
        require(!ended);
        emit AuctionEnded(highestBidder, highestBid);
        ended = true;
        beneficiary.transfer(highestBid);
    }
}

安全的远程购买

pragma solidity ^0.4.22;

contract Purchase {
    uint public value;
    address public seller;
    address public buyer;
    enum State { Created, Locked, Inactive }
    State public state;

    //确保 `msg.value` 是一个偶数。
    //如果它是一个奇数,则它将被截断。
    //通过乘法检查它不是奇数。
    constructor() public payable {
        seller = msg.sender;
        value = msg.value / 2;
        require((2 * value) == msg.value, "Value has to be even.");
    }

    modifier condition(bool _condition) {
        require(_condition);
        _;
    }

    modifier onlyBuyer() {
        require(
            msg.sender == buyer,
            "Only buyer can call this."
        );
        _;
    }

    modifier onlySeller() {
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    }

    modifier inState(State _state) {
        require(
            state == _state,
            "Invalid state."
        );
        _;
    }

    event Aborted();
    event PurchaseConfirmed();
    event ItemReceived();

    ///中止购买并回收以太币。
    ///只能在合约被锁定之前由卖家调用。
    function abort()
        public
        onlySeller
        inState(State.Created)
    {
        emit Aborted();
        state = State.Inactive;
        seller.transfer(address(this).balance);
    }

    /// 买家确认购买。
    /// 交易必须包含 `2 * value` 个以太币。
    /// 以太币会被锁定,直到 confirmReceived 被调用。
    function confirmPurchase()
        public
        inState(State.Created)
        condition(msg.value == (2 * value))
        payable
    {
        emit PurchaseConfirmed();
        buyer = msg.sender;
        state = State.Locked;
    }

    /// 确认你(买家)已经收到商品。
    /// 这会释放被锁定的以太币。
    function confirmReceived()
        public
        onlyBuyer
        inState(State.Locked)
    {
        emit ItemReceived();
        // 首先修改状态很重要,否则的话,由 `transfer` 所调用的合约可以回调进这里(再次接收以太币)。
        state = State.Inactive;

        // 注意: 这实际上允许买方和卖方阻止退款 - 应该使用取回模式。
        buyer.transfer(value);
        seller.transfer(address(this).balance);
    }
}

微支付通道

To be written.

深入理解Solidity

本章将为你提供所有关于Solidity的、你需要知道的知识。 如果你发现缺少了什么,请在 Gitter 上联系我们; 或者在 Github 上创建 pull request 。

Solidity 源文件结构

源文件中可以包含任意多个合约定义、导入指令和杂注指令。

版本杂注

为了避免未来被可能引入不兼容变更的编译器所编译,源文件可以(也应该)被所谓的版本 杂注pragma 所注解。 我们力图把这类变更做到尽可能小,特别是,我们需要以一种当修改语义时必须同步修改语法的方式引入变更,当然这有时候也难以做到。 因此,至少对含重大变更的版本,通读变更日志永远是好办法。 这些版本的版本号始终是 0.x.0 或者 x.0.0 的形式。

版本杂注使用如下:

pragma solidity ^0.4.0;

这样,源文件将既不允许低于 0.4.0 版本的编译器编译, 也不允许高于(包含) 0.5.0 版本的编译器编译(第二个条件因使用 ^ 被添加)。 这种做法的考虑是,编译器在 0.5.0 版本之前不会有重大变更,所以可确保源代码始终按预期被编译。 上面例子中不固定编译器的具体版本号,因此编译器的补丁版也可以使用。

可以使用更复杂的规则来指定编译器的版本,表达式遵循 npm 版本语义。

注解

Pragma 是 pragmatic information 的简称,微软 Visual C++ 文档 中译为杂注。 Solidity 中沿用 C ,C++ 等中的编译指令概念,用于告知编译器 如何 编译。 ——译者注

导入其他源文件

语法与语义

虽然 Solidity 不知道 "default export" 为何物, 但是 Solidity 所支持的导入语句,其语法同 JavaScript(从 ES6 起)非常类似。

注解

ES6 即 ECMAScript 6.0,ES6是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布。 ——译者注

在全局层面上,可使用如下格式的导入语句:

import "filename";

此语句将从 “filename” 中导入所有的全局符号到当前全局作用域中(不同于 ES6,Solidity 是向后兼容的)。

import * as symbolName from "filename";

...创建一个新的全局符号 symbolName,其成员均来自 "filename" 中全局符号。

import {symbol1 as alias, symbol2} from "filename";

...创建新的全局符号 aliassymbol2,分别从 "filename" 引用 symbol1symbol2

另一种语法不属于 ES6,但或许更简便:

import "filename" as symbolName;

这条语句等同于 import * as symbolName from "filename";

路径

上文中的 filename 总是会按路径来处理,以 / 作为目录分割符、以 . 标示当前目录、以 .. 表示父目录。 当 ... 后面跟随的字符是 / 时,它们才能被当做当前目录或父目录。 只有路径以当前目录 . 或父目录 .. 开头时,才能被视为相对路径。

import "./x" as x; 语句导入当前源文件同目录下的文件 x 。 如果用 import "x" as x; 代替,可能会引入不同的文件(在全局 include directory 中)。

最终导入哪个文件取决于编译器(见下文)到底是怎样解析路径的。 通常,目录层次不必严格映射到本地文件系统, 它也可以映射到能通过诸如 ipfs,http 或者 git 发现的资源。

在实际的编译器中使用

当运行编译器时,它不仅能指定如何发现路径的第一个元素,还可指定路径前缀 重映射remapping。 例如,github.com/ethereum/dapp-bin/library 会被重映射到 /usr/local/dapp-bin/library , 此时编译器将从重映射位置读取文件。如果重映射到多个路径,优先尝试重映射路径最长的一个。 这允许将比如 "" 被映射到 "/usr/local/include/solidity" 来进行“回退重映射”。 同时,这些重映射可取决于上下文,允许你配置要导入的包,比如同一个库的不同版本。

solc:

对于 solc(命令行编译器),这些重映射以 context:prefix=target 形式的参数提供。 其中,context:=target 部分是可选的(此时 target 默认为 prefix )。 所有重映射的值都是被编译过的常规文件(包括他们的依赖),这个机制完全是向后兼容的(只要文件名不包含 = 或 : ), 因此这不是一个破坏性修改。 在 content 目录或其子目录中的源码文件中,所有导入语句里以 prefix 开头的导入文件都将被用 target 替换 prefix 来重定向。

举个例子,如果你已克隆 github.com/ethereum/dapp-bin/ 到本地 /usr/local/dapp-bin , 可在源文件中使用:

import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping;

然后运行编译器:

solc github.com/ethereum/dapp-bin/=/usr/local/dapp-bin/ source.sol

举个更复杂的例子,假设你依赖了一些使用了非常旧版本的 dapp-bin 的模块。 旧版本的 dapp-bin 已经被 checkout 到 /usr/local/dapp-bin_old ,此时你可使用:

solc module1:github.com/ethereum/dapp-bin/=/usr/local/dapp-bin/ \
module2:github.com/ethereum/dapp-bin/=/usr/local/dapp-bin_old/ \
source.sol

这样, module2 中的所有导入都指向旧版本,而 module1 中的导入则获取新版本。

注意, solc 只允许包含来自特定目录的文件:它们必须位于显式地指定的源文件目录(或子目录)中,或者重映射的目标目录(或子目录)中。 如果你想直接用绝对路径来包含文件,只需添加重映射 =/

如果有多个重映射指向一个有效文件,那么具有最长公共前缀的重映射会被选用。

Remix:

Remix 提供一个为 github 源代码平台的自动重映射,它将通过网络自动获取文件: 比如,你可以使用 import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping; 导入一个 map 迭代器。

未来, Remix 可能支持其他源代码平台。

注释

可以使用单行注释(//)和多行注释(/*...*/

// 这是一个单行注释。

/*
这是一个
多行注释。
*/

此外,有另一种注释称为 natspec 注释,其文档还尚未编写。 它们是用三个反斜杠(///)或双星号开头的块(/** ... */)书写,它们应该直接在函数声明或语句上使用。 可在注释中使用 Doxygen 样式的标签来文档化函数、 标注形式校验通过的条件,和提供一个当用户试图调用一个函数时显示给用户的 确认文本

在下面的例子中,我们记录了合约的标题、两个入参和两个返回值的说明:

pragma solidity ^0.4.0;

/** @title 形状计算器。 */
contract shapeCalculator {
    /** @dev 求矩形表明面积与周长。
    * @param w 矩形宽度。
    * @param h 矩形高度。
    * @return s 求得表面积。
    * @return p 求得周长。
    */
    function rectangle(uint w, uint h) returns (uint s, uint p) {
        s = w * h;
        p = 2 * (w + h);
    }
}

合约结构

在 Solidity 中,合约类似于面向对象编程语言中的类。 每个合约中可以包含 状态变量函数函数修饰器事件结构类型、 和 枚举类型 的声明,且合约可以从其他合约继承。

状态变量

状态变量是永久地存储在合约存储中的值。

pragma solidity ^0.4.0;

contract SimpleStorage {
    uint storedData; // 状态变量
    // ...
}

有效的状态变量类型参阅 类型 章节, 对状态变量可见性有可能的选择参阅 可见性和 getter 函数

函数

函数是合约中代码的可执行单元。

pragma solidity ^0.4.0;

contract SimpleAuction {
    function bid() public payable { // 函数
        // ...
    }
}

函数调用 可发生在合约内部或外部,且函数对其他合约有不同程度的可见性( 可见性和 getter 函数)。

函数修饰器

函数修饰器可以用来以声明的方式改良函数语义(参阅合约章节中 函数 )。

pragma solidity ^0.4.22;

contract Purchase {
    address public seller;

    modifier onlySeller() { // 修饰器
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    }

    function abort() public onlySeller { // Modifier usage
        // ...
    }
}

事件

事件是能方便地调用以太坊虚拟机日志功能的接口。

pragma solidity ^0.4.21;
contract SimpleAuction {
    event HighestBidIncreased(address bidder, uint amount); // 事件

    function bid() public payable {
        // ...
        emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
    }
}

有关如何声明事件和如何在 dapp 中使用事件的信息,参阅合约章节中的 事件

结构类型

结构是可以将几个变量分组的自定义类型(参阅类型章节中的 结构体)。

pragma solidity ^0.4.0;

contract Ballot {
    struct Voter { // 结构
        uint weight;
        bool voted;
        address delegate;
        uint vote;
    }
}

枚举类型

枚举可用来创建由一定数量的“常量值”构成的自定义类型(参阅类型章节中的 枚举类型)。

pragma solidity ^0.4.0;

contract Purchase {
    enum State { Created, Locked, Inactive } // 枚举
}

类型

Solidity 是一种静态类型语言,这意味着每个变量(状态变量和局部变量)都需要在编译时指定变量的类型(或至少可以推导出变量类型——参考下文的 类型推断 )。 Solidity 提供了几种基本类型,可以用来组合出复杂类型。

除此之外,类型之间可以在包含运算符号的表达式中进行交互。 关于各种运算符号,可以参考 操作符优先级

值类型

以下类型也称为值类型,因为这些类型的变量将始终按值来传递。 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。

布尔类型

bool :可能的取值为字面常数值 truefalse

运算符:

  • ! (逻辑非)
  • && (逻辑与, "and" )
  • || (逻辑或, "or" )
  • == (等于)
  • != (不等于)

运算符 ||&& 都遵循同样的短路( short-circuiting )规则。就是说在表达式 f(x) || g(y) 中, 如果 f(x) 的值为 true ,那么 g(y) 就不会被执行,即使会出现一些副作用。

整型

int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8uint256 (无符号,从 8 位到 256 位)以及 int8int256,以 8 位为步长递增。 uintint 分别是 uint256int256 的别名。

运算符:

  • 比较运算符: <=<==!=>=> (返回布尔值)
  • 位运算符: &|^ (异或), ~ (位取反)
  • 算数运算符: +- , 一元运算 - , 一元运算 +*/% (取余) , ** (幂), << (左移位) , >> (右移位)

除法总是会截断的(仅被编译为 EVM 中的 DIV 操作码), 但如果操作数都是 字面常数(literals) (或者字面常数表达式),则不会截断。

除以零或者模零运算都会引发运行时异常。

移位运算的结果取决于运算符左边的类型。 表达式 x << yx * 2**y 是等价的, x >> yx / 2**y 是等价的。这意味对一个负数进行移位会导致其符号消失。 按负数位移动会引发运行时异常。

警告

由有符号整数类型负值右移所产生的结果跟其它语言中所产生的结果是不同的。 在 Solidity 中,右移和除是等价的,因此对一个负数进行右移操作会导致向 0 的取整(截断)。 而在其它语言中, 对负数进行右移类似于(向负无穷)取整。

定长浮点型

警告

Solidity 还没有完全支持定长浮点型。可以声明定长浮点型的变量,但不能给它们赋值或把它们赋值给其他变量。。

fixed / ufixed:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxNfixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixedfixed 分别是 ufixed128x19fixed128x19 的别名。

运算符:

  • 比较运算符:<=<==!=>=> (返回值是布尔型)
  • 算术运算符:+-, 一元运算 -, 一元运算 +*/% (取余数)

注解

浮点型(在许多语言中的 floatdouble 类型,更准确地说是 IEEE 754 类型)和定长浮点型之间最大的不同点是, 在前者中整数部分和小数部分(小数点后的部分)需要的位数是灵活可变的,而后者中这两部分的长度受到严格的规定。 一般来说,在浮点型中,几乎整个空间都用来表示数字,但只有少数的位来表示小数点的位置。

地址类型

address:地址类型存储一个 20 字节的值(以太坊地址的大小)。 地址类型也有成员变量,并作为所有合约的基础。

运算符:

  • <=<==!=>=>

注解

从 0.5.0 版本开始,合约不会从地址类型派生,但仍然可以显式地转换成地址类型。

地址类型成员变量
  • balancetransfer

快速参考,请见 地址相关

可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个地址发送 以太币Ether (以 wei 为单位):

address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

注解

如果 x 是一个合约地址,它的代码(更具体来说是它的 fallback 函数,如果有的话)会跟 transfer 函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币Ether 交易会被打回,当前的合约也会在终止的同时抛出异常。

  • send

sendtransfer 的低级版本。如果执行失败,当前的合约不会因为异常而终止,但 send 会返回 false

警告

在使用 send 的时候会有些风险:如果调用栈深度是 1024 会导致发送失败(这总是可以被调用者强制),如果接收者用光了 gas 也会导致发送失败。 所以为了保证 以太币Ether 发送的安全,一定要检查 send 的返回值,使用 transfer 或者更好的办法: 使用一种接收者可以取回资金的模式。

  • callcallcodedelegatecall

此外,为了与不符合 应用二进制接口Application Binary Interface(ABI) 的合约交互,于是就有了可以接受任意类型任意数量参数的 call 函数。 这些参数会被打包到以 32 字节为单位的连续区域中存放。 其中一个例外是当第一个参数被编码成正好 4 个字节的情况。 在这种情况下,这个参数后边不会填充后续参数编码,以允许使用函数签名。

address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);

call 返回的布尔值表明了被调用的函数已经执行完毕(true)或者引发了一个 EVM 异常(false)。 无法访问返回的真实数据(为此我们需要事先知道编码和大小)。

可以使用 .gas() 修饰器modifier 调整提供的 gas 数量

namReg.call.gas(1000000)("register", "MyName");

类似地,也能控制提供的 以太币Ether 的值

nameReg.call.value(1 ether)("register", "MyName");

最后一点,这些 修饰器modifier 可以联合使用。每个修改器出现的顺序不重要

nameReg.call.gas(1000000).value(1 ether)("register", "MyName");

注解

目前还不能在重载函数中使用 gas 或者 value 修饰器modifier

一种解决方案是给 gas 和值引入一个特例,并重新检查它们是否在重载的地方出现。

类似地,也可以使用 delegatecall: 区别在于只使用给定地址的代码,其它属性(存储,余额,……)都取自当前合约。 delegatecall 的目的是使用存储在另外一个合约中的库代码。 用户必须确保两个合约中的存储结构都适用于 delegatecall。 在 homestead 版本之前,只有一个功能类似但作用有限的 callcode 的函数可用,但它不能获取委托方的 msg.sendermsg.value

这三个函数 calldelegatecallcallcode 都是非常低级的函数,应该只把它们当作 最后一招 来使用,因为它们破坏了 Solidity 的类型安全性。

注解

所有合约都继承了地址(address)的成员变量,因此可以使用 this.balance 查询当前合约的余额。

注解

不鼓励使用 callcode,在未来也会将其移除。

警告

这三个函数都属于低级函数,需要谨慎使用。 具体来说,任何未知的合约都可能是恶意的。 你在调用一个合约的同时就将控制权交给了它,它可以反过来调用你的合约, 因此,当调用返回时要为你的状态变量的改变做好准备。

定长字节数组

关键字有:bytes1bytes2bytes3, ..., bytes32bytebytes1 的别名。

运算符:

  • 比较运算符:<=<==!=>=> (返回布尔型)
  • 位运算符: &|^ (按位异或), ~ (按位取反), << (左移位), >> (右移位)
  • 索引访问:如果 xbytesI 类型,那么 x[k] (其中 0 <= k < I)返回第 k 个字节(只读)。

该类型可以和作为右操作数的任何整数类型进行移位运算(但返回结果的类型和左操作数类型相同),右操作数表示需要移动的位数。 进行负数位移运算会引发运行时异常。

成员变量:

  • .length 表示这个字节数组的长度(只读).

注解

可以将 byte[] 当作字节数组使用,但这种方式非常浪费存储空间,准确来说,是在传入调用时,每个元素会浪费 31 字节。 更好地做法是使用 bytes

变长字节数组
bytes:
变长字节数组,参见 数组。它并不是值类型。
string:
变长 UTF-8 编码字符串类型,参见 数组。并不是值类型。
地址字面常数(Address Literals)

比如像 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 这样的通过了地址校验和测试的十六进制字面常数属于 address 类型。 长度在 39 到 41 个数字的,没有通过校验和测试而产生了一个警告的十六进制字面常数视为正常的有理数字面常数。

注解

混合大小写的地址校验和格式定义在 EIP-55 中。

有理数和整数字面常数

整数字面常数由范围在 0-9 的一串数字组成,表现成十进制。 例如,69 表示数字 69。 Solidity 中是没有八进制的,因此前置 0 是无效的。

十进制小数字面常数带有一个 .,至少在其一边会有一个数字。 比如:1..1,和 1.3

科学符号也是支持的,尽管指数必须是整数,但底数可以是小数。 比如:2e10-2e102e-102.5e1

数值字面常数表达式本身支持任意精度,除非它们被转换成了非字面常数类型(也就是说,当它们出现在非字面常数表达式中时就会发生转换)。 这意味着在数值常量表达式中, 计算不会溢出而除法也不会截断。

例如, (2**800 + 1) - 2**800 的结果是字面常数 1 (属于 uint8 类型),尽管计算的中间结果已经超过了 以太坊虚拟机Ethereum Virtual Machine(EVM) 的机器字长度。 此外, .5 * 8 的结果是整型 4 (尽管有非整型参与了计算)。

只要操作数是整型,任意整型支持的运算符都可以被运用在数值字面常数表达式中。 如果两个中的任一个数是小数,则不允许进行位运算。如果指数是小数的话,也不支持幂运算(因为这样可能会得到一个无理数)。

注解

Solidity 对每个有理数都有对应的数值字面常数类型。 整数字面常数和有理数字面常数都属于数值字面常数类型。 除此之外,所有的数值字面常数表达式(即只包含数值字面常数和运算符的表达式)都属于数值字面常数类型。 因此数值字面常数表达式 1 + 22 + 1 的结果跟有理数三的数值字面常数类型相同。

警告

在早期版本中,整数字面常数的除法也会截断,但在现在的版本中,会将结果转换成一个有理数。即 5 / 2 并不等于 2,而是等于 2.5

注解

数值字面常数表达式只要在非字面常数表达式中使用就会转换成非字面常数类型。 在下面的例子中,尽管我们知道 b 的值是一个整数,但 2.5 + a 这部分表达式并不进行类型检查,因此编译不能通过。

uint128 a = 1;
uint128 b = 2.5 + a + 0.5;
字符串字面常数

字符串字面常数是指由双引号或单引号引起来的字符串("foo" 或者 'bar')。 不像在 C 语言中那样带有结束符;"foo" 相当于 3 个字节而不是 4 个。 和整数字面常数一样,字符串字面常数的类型也可以发生改变,但它们可以隐式地转换成 bytes1,……,bytes32,如果合适的话,还可以转换成 bytes 以及 string

字符串字面常数支持转义字符,例如 \n\xNN\uNNNN\xNN 表示一个 16 进制值,最终转换成合适的字节, 而 \uNNNN 表示 Unicode 编码值,最终会转换为 UTF-8 的序列。

十六进制字面常数

十六进制字面常数以关键字 hex 打头,后面紧跟着用单引号或双引号引起来的字符串(例如,hex"001122FF")。 字符串的内容必须是一个十六进制的字符串,它们的值将使用二进制表示。

十六进制字面常数跟字符串字面常数很类似,具有相同的转换规则。

枚举类型
pragma solidity ^0.4.16;

contract test {
    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        choice = ActionChoices.GoStraight;
    }

    // 由于枚举类型不属于 |ABI| 的一部分,因此对于所有来自 Solidity 外部的调用,
    // "getChoice" 的签名会自动被改成 "getChoice() returns (uint8)"。
    // 整数类型的大小已经足够存储所有枚举类型的值,随着值的个数增加,
    // 可以逐渐使用 `uint16` 或更大的整数类型。
    function getChoice() public view returns (ActionChoices) {
        return choice;
    }

    function getDefaultChoice() public pure returns (uint) {
        return uint(defaultChoice);
    }
}
函数类型

函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量,也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。 函数类型有两类:- 内部(internal) 函数和 外部(external) 函数:

内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。 调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。

外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。

函数类型表示成如下的形式

function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

与参数类型相反,返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个 returns (<return types>) 部分。

函数类型默认是内部函数,因此不需要声明 internal 关键字。 与此相反的是,合约中的函数本身默认是 public 的,只有当它被当做类型名称时,默认才是内部函数。

有两种方法可以访问当前合约中的函数:一种是直接使用它的名字,f ,另一种是使用 this.f 。 前者适用于内部函数,后者适用于外部函数。

如果当函数类型的变量还没有初始化时就调用它的话会引发一个异常。 如果在一个函数被 delete 之后调用它也会发生相同的情况。

如果外部函数类型在 Solidity 的上下文环境以外的地方使用,它们会被视为 function 类型。 该类型将函数地址紧跟其函数标识一起编码为一个 bytes24 类型。。

请注意,当前合约的 public 函数既可以被当作内部函数也可以被当作外部函数使用。 如果想将一个函数当作内部函数使用,就用 f 调用,如果想将其当作外部函数,使用 this.f

除此之外,public(或 external)函数也有一个特殊的成员变量称作 selector,可以返回 ABI 函数选择器:

pragma solidity ^0.4.16;

contract Selector {
  function f() public view returns (bytes4) {
    return this.f.selector;
  }
}

如果使用内部函数类型的例子:

pragma solidity ^0.4.16;

library ArrayUtils {
  // 内部函数可以在内部库函数中使用,
  // 因为它们会成为同一代码上下文的一部分
  function map(uint[] memory self, function (uint) pure returns (uint) f)
    internal
    pure
    returns (uint[] memory r)
  {
    r = new uint[](self.length);
    for (uint i = 0; i < self.length; i++) {
      r[i] = f(self[i]);
    }
  }
  function reduce(
    uint[] memory self,
    function (uint, uint) pure returns (uint) f
  )
    internal
    pure
    returns (uint r)
  {
    r = self[0];
    for (uint i = 1; i < self.length; i++) {
      r = f(r, self[i]);
    }
  }
  function range(uint length) internal pure returns (uint[] memory r) {
    r = new uint[](length);
    for (uint i = 0; i < r.length; i++) {
      r[i] = i;
    }
  }
}

contract Pyramid {
  using ArrayUtils for *;
  function pyramid(uint l) public pure returns (uint) {
    return ArrayUtils.range(l).map(square).reduce(sum);
  }
  function square(uint x) internal pure returns (uint) {
    return x * x;
  }
  function sum(uint x, uint y) internal pure returns (uint) {
    return x + y;
  }
}

另外一个使用外部函数类型的例子:

pragma solidity ^0.4.11;

contract Oracle {
  struct Request {
    bytes data;
    function(bytes memory) external callback;
  }
  Request[] requests;
  event NewRequest(uint);
  function query(bytes data, function(bytes memory) external callback) public {
    requests.push(Request(data, callback));
    NewRequest(requests.length - 1);
  }
  function reply(uint requestID, bytes response) public {
    // 这里要验证 reply 来自可信的源
    requests[requestID].callback(response);
  }
}

contract OracleUser {
  Oracle constant oracle = Oracle(0x1234567); // 已知的合约
  function buySomething() {
    oracle.query("USD", this.oracleResponse);
  }
  function oracleResponse(bytes response) public {
    require(msg.sender == address(oracle));
    // 使用数据
  }
}

注解

Lambda 表达式或者内联函数的引入在计划内,但目前还没支持。

引用类型

比起之前讨论过的值类型,在处理复杂的类型(即占用的空间超过 256 位的类型)时,我们需要更加谨慎。 由于拷贝这些类型变量的开销相当大,我们不得不考虑它的存储位置,是将它们保存在 ** 内存memory ** (并不是永久存储)中, 还是 ** 存储storage ** (保存状态变量的地方)中。

数据位置

所有的复杂类型,即 数组结构 类型,都有一个额外属性,“数据位置”,说明数据是保存在 内存memory 中还是 存储storage 中。 根据上下文不同,大多数时候数据有默认的位置,但也可以通过在类型名后增加关键字 storagememory 进行修改。 函数参数(包括返回的参数)的数据位置默认是 memory, 局部变量的数据位置默认是 storage,状态变量的数据位置强制是 storage (这是显而易见的)。

也存在第三种数据位置, calldata ,这是一块只读的,且不会永久存储的位置,用来存储函数参数。 外部函数的参数(非返回参数)的数据位置被强制指定为 calldata ,效果跟 memory 差不多。

数据位置的指定非常重要,因为它们影响着赋值行为: 在 存储storage内存memory 之间两两赋值,或者 存储storage 向状态变量(甚至是从其它状态变量)赋值都会创建一份独立的拷贝。 然而状态变量向局部变量赋值时仅仅传递一个引用,而且这个引用总是指向状态变量,因此后者改变的同时前者也会发生改变。 另一方面,从一个 内存memory 存储的引用类型向另一个 内存memory 存储的引用类型赋值并不会创建拷贝。

pragma solidity ^0.4.0;

contract C {
    uint[] x; // x 的数据存储位置是 storage

    // memoryArray 的数据存储位置是 memory
    function f(uint[] memoryArray) public {
        x = memoryArray; // 将整个数组拷贝到 storage 中,可行
        var y = x;  // 分配一个指针(其中 y 的数据存储位置是 storage),可行
        y[7]; // 返回第 8 个元素,可行
        y.length = 2; // 通过 y 修改 x,可行
        delete x; // 清除数组,同时修改 y,可行
        // 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组, /
        // 但 storage 是“静态”分配的:
        // y = memoryArray;
        // 下面这一行也不可行,因为这会“重置”指针,
        // 但并没有可以让它指向的合适的存储位置。
        // delete y;

        g(x); // 调用 g 函数,同时移交对 x 的引用
        h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝
    }

    function g(uint[] storage storageArray) internal {}
    function h(uint[] memoryArray) public {}
}
总结
强制指定的数据位置:
  • 外部函数的参数(不包括返回参数): calldata
  • 状态变量: storage
默认数据位置:
  • 函数参数(包括返回参数): memory
  • 所有其它局部变量: storage
数组

数组可以在声明时指定长度,也可以动态调整大小。 对于 存储storage 的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或者结构体)。 对于 内存memory 的数组来说,元素类型不能是映射类型,如果作为 public 函数的参数,它只能是 ABI 类型。

一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]。 举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组,应声明为 uint[][5] (注意这里跟其它语言比,数组长度的声明位置是反的)。 要访问第三个动态数组的第二个元素,你应该使用 x[2][1](数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反,也就是说,x[2] 是从右边减少了一级)。。

bytesstring 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 stringbytes 相同,但(暂时)不允许用长度或索引来访问。

注解

如果想要访问以字节表示的字符串 s,请使用 bytes(s).length / bytes(s)[7] = 'x';。 注意这时你访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。

可以将数组标识为 public,从而让 Solidity 创建一个 getter。 之后必须使用数字下标作为参数来访问 getter。

创建内存数组

可使用 new 关键字在内存中创建变长数组。 与 存储storage 数组相反的是,你 不能 通过修改成员变量 .length 改变 内存memory 数组的大小。

pragma solidity ^0.4.16;

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        // 这里我们有 a.length == 7 以及 b.length == len
        a[6] = 8;
    }
}
数组字面常数 / 内联数组

数组字面常数是写作表达式形式的数组,并且不会立即赋值给变量。

pragma solidity ^0.4.16;

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) public pure {
        // ...
    }
}

数组字面常数是一种定长的 内存memory 数组类型,它的基础类型由其中元素的普通类型决定。 例如,[1, 2, 3] 的类型是 uint8[3] memory,因为其中的每个字面常数的类型都是 uint8。 正因为如此,有必要将上面这个例子中的第一个元素转换成 uint 类型。 目前需要注意的是,定长的 内存memory 数组并不能赋值给变长的 内存memory 数组,下面是个反例:

// 这段代码并不能编译。

pragma solidity ^0.4.0;

contract C {
    function f() public {
        // 这一行引发了一个类型错误,因为 unint[3] memory
        // 不能转换成 uint[] memory。
        uint[] x = [uint(1), 3, 4];
    }
}

已经计划在未来移除这样的限制,但目前数组在 ABI 中传递的问题造成了一些麻烦。

成员
length:
数组有 length 成员变量表示当前数组的长度。 动态数组可以在 存储storage (而不是 内存memory )中通过改变成员变量 .length 改变数组大小。 并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度。 一经创建,内存memory 数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。
push:
变长的 存储storage 数组以及 bytes 类型(而不是 string 类型)都有一个叫做 push 的成员函数,它用来附加新的元素到数组末尾。 这个函数将返回新的数组长度。

警告

在外部函数中目前还不能使用多维数组。

警告

由于 以太坊虚拟机Ethereum Virtual Machine(EVM) 的限制,不能通过外部函数调用返回动态的内容。 例如,如果通过 web3.js 调用 contract C { function f() returns (uint[]) { ... } } 中的 f 函数,它会返回一些内容,但通过 Solidity 不可以。

目前唯一的变通方法是使用大型的静态数组。

pragma solidity ^0.4.16;

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // 注意下面的代码并不是一对动态数组,
    // 而是一个数组元素为一对变量的动态数组(也就是数组元素为长度为 2 的定长数组的动态数组)。
    bool[2][] m_pairsOfFlags;
    // newPairs 存储在 memory 中 —— 函数参数默认的存储位置

    function setAllFlagPairs(bool[2][] newPairs) public {
        // 向一个 storage 的数组赋值会替代整个数组
        m_pairsOfFlags = newPairs;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 访问一个不存在的数组下标会引发一个异常
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 如果 newSize 更小,那么超出的元素会被清除
        m_pairsOfFlags.length = newSize;
    }

    function clear() public {
        // 这些代码会将数组全部清空
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // 这里也是实现同样的功能
        m_pairsOfFlags.length = 0;
    }

    bytes m_byteData;

    function byteArrays(bytes data) public {
        // 字节的数组(语言意义中的 byte 的复数 ``bytes``)不一样,因为它们不是填充式存储的,
        // 但可以当作和 "uint8[]" 一样对待
        m_byteData = data;
        m_byteData.length += 7;
        m_byteData[3] = byte(8);
        delete m_byteData[2];
    }

    function addFlag(bool[2] flag) public returns (uint) {
        return m_pairsOfFlags.push(flag);
    }

    function createMemoryArray(uint size) public pure returns (bytes) {
        // 使用 `new` 创建动态 memory 数组:
        uint[2][] memory arrayOfPairs = new uint[2][](size);
        // 创建一个动态字节数组:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = byte(i);
        return b;
    }
}
结构体

Solidity 支持通过构造结构体的形式定义新的类型,以下是一个结构体使用的示例:

pragma solidity ^0.4.11;

contract CrowdFunding {
    // 定义的新类型包含两个属性。
    struct Funder {
        address addr;
        uint amount;
    }

    struct Campaign {
        address beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;
    }

    uint numCampaigns;
    mapping (uint => Campaign) campaigns;

    function newCampaign(address beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID 作为一个变量返回
        // 创建新的结构体示例,存储在 storage 中。我们先不关注映射类型。
        campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
    }

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // 以给定的值初始化,创建一个新的临时 memory 结构体,
        // 并将其拷贝到 storage 中。
        // 注意你也可以使用 Funder(msg.sender, msg.value) 来初始化。
        c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;
    }

    function checkGoalReached(uint campaignID) public returns (bool reached) {
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal)
            return false;
        uint amount = c.amount;
        c.amount = 0;
        c.beneficiary.transfer(amount);
        return true;
    }
}

上面的合约只是一个简化版的众筹合约,但它已经足以让我们理解结构体的基础概念。 结构体类型可以作为元素用在映射和数组中,其自身也可以包含映射和数组作为成员变量。

尽管结构体本身可以作为映射的值类型成员,但它并不能包含自身。 这个限制是有必要的,因为结构体的大小必须是有限的。

注意在函数中使用结构体时,一个结构体是如何赋值给一个局部变量(默认存储位置是 存储storage )的。 在这个过程中并没有拷贝这个结构体,而是保存一个引用,所以对局部变量成员的赋值实际上会被写入状态。

当然,你也可以直接访问结构体的成员而不用将其赋值给一个局部变量,就像这样, campaigns[campaignID].amount = 0

映射

映射类型在声明时的形式为 mapping(_KeyType => _ValueType)。 其中 _KeyType 可以是除了映射、变长数组、合约、枚举以及结构体以外的几乎所有类型。 _ValueType 可以是包括映射类型在内的任何类型。

映射可以视作 哈希表 <https://en.wikipedia.org/wiki/Hash_table>,它们在实际的初始化过程中创建每个可能的 key, 并将其映射到字节形式全是零的值:一个类型的 默认值。然而下面是映射与哈希表不同的地方: 在映射中,实际上并不存储 key,而是存储它的 keccak256 哈希值,从而便于查询实际的值。

正因为如此,映射是没有长度的,也没有 key 的集合或 value 的集合的概念。

只有状态变量(或者在 internal 函数中的对于存储变量的引用)可以使用映射类型。。

可以将映射声明为 public,然后来让 Solidity 创建一个 getter_KeyType 将成为 getter 的必须参数,并且 getter 会返回 _ValueType

_ValueType 也可以是一个映射。这时在使用 getter 时将将需要递归地传入每个 _KeyType 参数。

pragma solidity ^0.4.0;

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingUser {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        m.update(100);
        return m.balances(this);
    }
}

注解

映射不支持迭代,但可以在此之上实现一个这样的数据结构。 例子可以参考 可迭代的映射

涉及 LValues 的运算符

如果 a 是一个 LValue(即一个变量或者其它可以被赋值的东西),以下运算符都可以使用简写:

a += e 等同于 a = a + e。 其它运算符 -=*=/=%=|=&= 以及 ^= 都是如此定义的。 a++a-- 分别等同于 a += 1a -= 1,但表达式本身的值等于 a 在计算之前的值。 与之相反,--a++a 虽然最终 a 的结果与之前的表达式相同,但表达式的返回值是计算之后的值。

删除

delete a 的结果是将 a 的类型在初始化时的值赋值给 a。即对于整型变量来说,相当于 a = 0, 但 delete 也适用于数组,对于动态数组来说,是将数组的长度设为 0,而对于静态数组来说,是将数组中的所有元素重置。 如果对象是结构体,则将结构体中的所有属性重置。

delete 对整个映射是无效的(因为映射的键可以是任意的,通常也是未知的)。 因此在你删除一个结构体时,结果将重置所有的非映射属性,这个过程是递归进行的,除非它们是映射。 然而,单个的键及其映射的值是可以被删除的。

理解 delete a 的效果就像是给 a 赋值很重要,换句话说,这相当于在 a 中存储了一个新的对象。

pragma solidity ^0.4.0;

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() public {
        uint x = data;
        delete x; // 将 x 设为 0,并不影响数据
        delete data; // 将 data 设为 0,并不影响 x,因为它仍然有个副本
        uint[] storage y = dataArray;
        delete dataArray;
        // 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,y 也将受到影响,
        // 因为它是一个存储位置是 storage 的对象的别名。
        // 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。
    }
}

基本类型之间的转换

隐式转换

如果一个运算符用在两个不同类型的变量之间,那么编译器将隐式地将其中一个类型转换为另一个类型(不同类型之间的赋值也是一样)。 一般来说,只要值类型之间的转换在语义上行得通,而且转换的过程中没有信息丢失,那么隐式转换基本都是可以实现的: uint8 可以转换成 uint16int128 转换成 int256,但 int8 不能转换成 uint256 (因为 uint256 不能涵盖某些值,例如,-1)。 更进一步来说,无符号整型可以转换成跟它大小相等或更大的字节类型,但反之不能。 任何可以转换成 uint160 的类型都可以转换成 address 类型。

显式转换

如果某些情况下编译器不支持隐式转换,但是你很清楚你要做什么,这种情况可以考虑显式转换。 注意这可能会发生一些无法预料的后果,因此一定要进行测试,确保结果是你想要的! 下面的示例是将一个 int8 类型的负数转换成 uint

int8 y = -3;
uint x = uint(y);

这段代码的最后,x 的值将是 0xfffff..fd (64 个 16 进制字符),因为这是 -3 的 256 位补码形式。

如果一个类型显式转换成更小的类型,相应的高位将被舍弃

uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678

类型推断

为了方便起见,没有必要每次都精确指定一个变量的类型,编译器会根据分配该变量的第一个表达式的类型自动推断该变量的类型

uint24 x = 0x123;
var y = x;

这里 y 的类型将是 uint24。不能对函数参数或者返回参数使用 var

警告

类型只能从第一次赋值中推断出来,因此以下代码中的循环是无限的, 原因是``i`` 的类型是 uint8,而这个类型变量的最大值比 2000 小。 for (var i = 0; i < 2000; i++) { ... }

单位和全局变量

以太币Ether 单位

以太币Ether 单位之间的换算就是在数字后边加上 weifinneyszaboether 来实现的,如果后面没有单位,缺省为 Wei。例如 2 ether == 2000 finney 的逻辑判断值为 true

时间单位

秒是缺省时间单位,在时间单位之间,数字后面带有 secondsminuteshoursdaysweeksyears 的可以进行换算,基本换算关系如下:

  • 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):剩余的 gas
  • msg.data (bytes): 完整的 calldata
  • msg.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): 交易发起者(完全的调用链)

注解

对于每一个**外部函数**调用,包括 msg.sendermsg.value 在内所有 msg 成员的值都会变化。这里包括对库函数的调用。

注解

不要依赖 block.timestampnowblockhash 产生随机数,除非你知道自己在做什么。

时间戳和区块哈希在一定程度上都可能受到挖矿矿工影响。例如,挖矿社区中的恶意矿工可以用某个给定的哈希来运行赌场合约的 payout 函数,而如果他们没收到钱,还可以用一个不同的哈希重新尝试。

当前区块的时间戳必须严格大于最后一个区块的时间戳,但这里唯一能确保的只是它会是在权威链上的两个连续区块的时间戳之间的数值。

注解

基于可扩展因素,区块哈希不是对所有区块都有效。你仅仅可以访问最近 256 个区块的哈希,其余的哈希均为零。

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)

更多详情请参考 ABI紧打包编码

错误处理
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("\x00\x12")keccak256(uint16(0x12)) 是一样的。

请注意,常量值会使用存储它们所需要的最少字节数进行打包。例如:keccak256(0) == keccak256(uint8(0))keccak256(0x12345678) == keccak256(uint32(0x12345678))

在一个私链上,你很有可能碰到由于 sha256ripemd160 或者 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 不同的是,它们可能返回任意数量的参数作为输出。

输入参数

输入参数的声明方式与变量相同。但是有一个例外,未使用的参数可以省略参数名。 例如,如果我们希望合约接受有两个整数形参的函数的外部调用,我们会像下面这样写

pragma solidity ^0.4.16;

contract Simple {
    function taker(uint _a, uint _b) public pure {
        // 用 _a 和 _b 实现相关功能.
    }
}
输出参数

输出参数的声明方式在关键词 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。

输入参数和输出参数可以在函数体中用作表达式。因此,它们也可用在等号左边被赋值。

控制结构

JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了 switchgoto。 因此 Solidity 中有 ifelsewhiledoforbreakcontinuereturn? : 这些与在 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):

pragma solidity >0.4.23 <0.5.0;

contract C {
    uint[] data;

    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);
        //元组的末尾元素可以省略(这也适用于变量声明)。
        (data.length,,) = f(); // 将长度设置为 7
        //省略元组中末尾元素的写法,仅可以在赋值操作的左侧使用,除了这个例外:
        (x,) = (1,);
        //(1,) 是指定单元素元组的唯一方法,因为 (1)
        //相当于 1。
    }
}

注解

直到 0.4.24 版本,给具有更少的元素数的元组赋值都可以可能的,无论是在左边还是右边(比如在最后空出若干元素)。现在,这已经不推荐了,赋值操作的两边应该具有相同个数的组成元素。

数组和结构体的复杂性

赋值语义对于像数组和结构体这样的非值类型来说会有些复杂。 为状态变量 赋值 经常会创建一个独立副本。另一方面,对局部变量的赋值只会为基本类型(即 32 字节以内的静态类型)创建独立的副本。如果结构体或数组(包括 bytesstring)被从状态变量分配给局部变量,局部变量将保留对原始状态变量的引用。对局部变量的第二次赋值不会修改状态变量,只会改变引用。赋值给局部变量的成员(或元素)则 改变 状态变量。

作用域和声明

变量声明后将有默认初始值,其初始值字节表示全部为零。任何类型变量的“默认值”是其对应类型的典型“零状态”。例如, bool 类型的默认值是 falseuintint 类型的默认值是 0 。对于静态大小的数组和 bytes1bytes32 ,每个单独的元素将被初始化为与其类型相对应的默认值。 最后,对于动态大小的数组, bytesstring 类型,其默认缺省值是一个空数组或字符串。

Solidity 中的作用域规则遵循了 C99(与其他很多语言一样):变量将会从它们被声明之后可见,直到一对 { } 块的结束。作为一个例外,在 for 循环语句中初始化的变量,其可见性仅维持到 for 循环的结束。

那些定义在代码块之外的变量,比如函数、合约、自定义类型等等,并不会影响它们的作用域特性。这意味着你可以在实际声明状态变量的语句之前就使用它们,并且递归地调用函数。

基于以上的规则,下边的例子不会出现编译警告,因为那两个变量虽然名字一样,但却在不同的作用域里。

pragma solidity >0.4.24;
contract C {
    function minimalScoping() pure public {
        {
            uint same2 = 0;
        }

        {
            uint same2 = 0;
        }
    }
}

作为 C99 作用域规则的特例,请注意在下边的例子里,第一次对 x 的赋值会改变上一层中声明的变量值。如果外层声明的变量被“影子化”(就是说被在内部作用域中由一个同名变量所替代)你会得到一个警告。

pragma solidity >0.4.24;
contract C {
    function f() pure public returns (uint) {
        uint x = 1;
        {
            x = 2; // 这个赋值会影响在外层声明的变量
            uint x;
        }
        return x; // x has value 2
    }
}

警告

在 Solidity 0.5.0 之前的版本,作用域规则都沿用了 Javascript 的规则,即一个变量可以声明在函数的任意位置,都可以使他在整个函数范围内可见。而这种规则会从 0.5.0 版本起被打破。从 0.5.0 版本开始,下面例子中的代码段会导致编译错误。
// 这将无法编译通过

pragma solidity >0.4.24;
contract C {
    function f() pure public returns (uint) {
        x = 2;
        uint x;
        return x;
    }
}

错误处理:Assert, Require, Revert and Exceptions

Solidity 使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。 便利函数 assertrequire 可用于检查条件并在条件不满足时抛出异常。assert 函数只能用于测试内部错误,并检查非变量。 require 函数用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。 如果使用得当,分析工具可以评估你的合约,并标示出那些会使 assert 失败的条件和函数调用。 正常工作的代码不会导致一个 assert 语句的失败;如果这发生了,那就说明出现了一个需要你修复的 bug。

还有另外两种触发异常的方法:revert 函数可以用来标记错误并恢复当前的调用。 revert 调用中包含有关错误的详细信息是可能的,这个消息会被返回给调用者。已经不推荐的关键字 throw 也可以用来替代 revert() (但无法返回错误消息)。

注解

从 0.4.13 版本开始,throw 这个关键字被弃用,并且将来会被逐渐淘汰。

当子调用发生异常时,它们会自动“冒泡”(即重新抛出异常)。这个规则的例外是 send 和低级函数 calldelegatecallcallcode --如果这些函数发生异常,将返回 false ,而不是“冒泡”。

警告

作为 EVM 设计的一部分,如果被调用合约帐户不存在,则低级函数 calldelegatecallcallcode 将返回 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 式异常:

  1. 如果你访问数组的索引太大或为负数(例如 x[i] 其中 i >= x.lengthi < 0)。
  2. 如果你访问固定长度 bytesN 的索引太大或为负数。
  3. 如果你用零当除数做除法或模运算(例如 5 / 023 % 0 )。
  4. 如果你移位负数位。
  5. 如果你将一个太大或负数值转换为一个枚举类型。
  6. 如果你调用内部函数类型的零初始化变量。
  7. 如果你调用 assert 的参数(表达式)最终结算为 false。

下列情况将会产生一个 require 式异常:

  1. 调用 throw
  2. 如果你调用 require 的参数(表达式)最终结算为 false
  3. 如果你通过消息调用调用某个函数,但该函数没有正确结束(它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),上述函数不包括低级别的操作 callsenddelegatecall 或者 callcode 。低级操作不会抛出异常,而通过返回 false 来指示失败。
  4. 如果你使用 new 关键字创建合约,但合约没有正确创建(请参阅上条有关”未正确完成“的定义)。
  5. 如果你对不包含代码的合约执行外部函数调用。
  6. 如果你的合约通过一个没有 payable 修饰符的公有函数(包括构造函数和 fallback 函数)接收 Ether。
  7. 如果你的合约通过公有 getter 函数接收 Ether 。
  8. 如果 .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字节)

合约

Solidity 合约类似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。 调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。

创建合约

可以通过以太坊交易“从外部”或从 Solidity 合约内部创建合约。

一些集成开发环境,例如 Remix, 通过使用一些用户界面元素使创建过程更加流畅。 在以太坊上编程创建合约最好使用 JavaScript API web3.js。 现在,我们已经有了一个叫做 web3.eth.Contract 的方法能够更容易的创建合约。

创建合约时,会执行一次构造函数(与合约同名的函数)。构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。

在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js 则不必关心这个问题。

如果一个合约想要创建另一个合约,那么创建者必须知晓被创建合约的源代码(和二进制代码)。 这意味着不可能循环创建依赖项。

pragma solidity ^0.4.16;

contract OwnedToken {
    // TokenCreator 是如下定义的合约类型.
    // 不创建新合约的话,也可以引用它。
    TokenCreator creator;
    address owner;
    bytes32 name;

    // 这是注册 creator 和设置名称的构造函数。
    function OwnedToken(bytes32 _name) public {
        // 状态变量通过其名称访问,而不是通过例如 this.owner 的方式访问。
        // 这也适用于函数,特别是在构造函数中,你只能像这样(“内部地”)调用它们,
        // 因为合约本身还不存在。
        owner = msg.sender;
        // 从 `address` 到 `TokenCreator` ,是做显式的类型转换
        // 并且假定调用合约的类型是 TokenCreator,没有真正的方法来检查这一点。
        creator = TokenCreator(msg.sender);
        name = _name;
    }

    function changeName(bytes32 newName) public {
        // 只有 creator (即创建当前合约的合约)能够更改名称 —— 因为合约是隐式转换为地址的,
        // 所以这里的比较是可行的。
        if (msg.sender == address(creator))
            name = newName;
    }

    function transfer(address newOwner) public {
        // 只有当前所有者才能发送 token。
        if (msg.sender != owner) return;
        // 我们也想询问 creator 是否可以发送。
        // 请注意,这里调用了一个下面定义的合约中的函数。
        // 如果调用失败(比如,由于 gas 不足),会立即停止执行。
        if (creator.isTokenTransferOK(owner, newOwner))
            owner = newOwner;
    }
}

contract TokenCreator {
    function createToken(bytes32 name)
       public
       returns (OwnedToken tokenAddress)
    {
        // 创建一个新的 Token 合约并且返回它的地址。
        // 从 JavaScript 方面来说,返回类型是简单的 `address` 类型,因为
        // 这是在 ABI 中可用的最接近的类型。
        return new OwnedToken(name);
    }

    function changeName(OwnedToken tokenAddress, bytes32 name)  public {
        // 同样,`tokenAddress` 的外部类型也是 `address` 。
        tokenAddress.changeName(name);
    }

    function isTokenTransferOK(address currentOwner, address newOwner)
        public
        view
        returns (bool ok)
    {
        // 检查一些任意的情况。
        address tokenAddress = msg.sender;
        return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
    }
}

可见性和 getter 函数

由于 Solidity 有两种函数调用(内部调用不会产生实际的 EVM 调用或称为“消息调用”,而外部调用则会产生一个 EVM 调用), 函数和状态变量有四种可见性类型。 函数可以指定为 externalpublicinternal 或者 private,默认情况下函数类型为 public。 对于状态变量,不能设置为 external ,默认是 internal

external
外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。 当收到大量数据的时候,外部函数有时候会更有效率。
public
public 函数是合约接口的一部分,可以在内部或通过消息调用。对于公共状态变量, 会自动生成一个 getter 函数(见下面)。
internal
这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this 调用。
private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

注解

合约中的所有内容对外部观察者都是可见的。设置一些 private 类型只能阻止其他合约访问和修改这些信息, 但是对于区块链外的整个世界它仍然是可见的。

可见性标识符的定义位置,对于状态变量来说是在类型后面,对于函数是在参数列表和返回关键字中间。

pragma solidity ^0.4.16;

contract C {
    function f(uint a) private pure returns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;
}

在下面的例子中,D 可以调用 c.getData() 来获取状态存储中 data 的值,但不能调用 f 。 合约 E 继承自 C ,因此可以调用 compute

// 下面代码编译错误

pragma solidity ^0.4.0;

contract C {
    uint private data;

    function f(uint a) private returns(uint b) { return a + 1; }
    function setData(uint a) public { data = a; }
    function getData() public returns(uint) { return data; }
    function compute(uint a, uint b) internal returns (uint) { return a+b; }
}

contract D {
    function readData() public {
        C c = new C();
        uint local = c.f(7); // 错误:成员 `f` 不可见
        c.setData(3);
        local = c.getData();
        local = c.compute(3, 5); // 错误:成员 `compute` 不可见
    }
}

contract E is C {
    function g() public {
        C c = new C();
        uint val = compute(3, 5); // 访问内部成员(从继承合约访问父合约成员)
    }
}
Getter 函数

编译器自动为所有 public 状态变量创建 getter 函数。对于下面给出的合约,编译器会生成一个名为 data 的函数, 该函数不会接收任何参数并返回一个 uint ,即状态变量 data 的值。可以在声明时完成状态变量的初始化。

pragma solidity ^0.4.0;

contract C {
    uint public data = 42;
}

contract Caller {
    C c = new C();
    function f() public {
        uint local = c.data();
    }
}

getter 函数具有外部可见性。如果在内部访问 getter(即没有 this. ),它被认为一个状态变量。 如果它是外部访问的(即用 this. ),它被认为为一个函数。

pragma solidity ^0.4.0;

contract C {
    uint public data;
    function x() public {
        data = 3; // 内部访问
        uint val = this.data(); // 外部访问
    }
}

下一个例子稍微复杂一些:

pragma solidity ^0.4.0;

contract Complex {
    struct Data {
        uint a;
        bytes3 b;
        mapping (uint => uint) map;
    }
    mapping (uint => mapping(bool => Data[])) public data;
}

这将会生成以下形式的函数

function data(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b) {
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
}

请注意,因为没有好的方法来提供映射的键,所以结构中的映射被省略。

函数 修饰器modifier

使用 修饰器modifier 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。 修饰器modifier 是合约的可继承属性, 并可能被派生合约覆盖。

pragma solidity ^0.4.11;

contract owned {
    function owned() public { owner = msg.sender; }
    address owner;

    // 这个合约只定义一个修饰器,但并未使用: 它将会在派生合约中用到。
    // 修饰器所修饰的函数体会被插入到特殊符号 _; 的位置。
    // 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异常。
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
}

contract mortal is owned {
    // 这个合约从 `owned` 继承了 `onlyOwner` 修饰符,并将其应用于 `close` 函数,
    // 只有在合约里保存的 owner 调用 `close` 函数,才会生效。
    function close() public onlyOwner {
        selfdestruct(owner);
    }
}

contract priced {
    // 修改器可以接收参数:
    modifier costs(uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}

contract Register is priced, owned {
    mapping (address => bool) registeredAddresses;
    uint price;

    function Register(uint initialPrice) public { price = initialPrice; }

    // 在这里也使用关键字 `payable` 非常重要,否则函数会自动拒绝所有发送给它的以太币。
    function register() public payable costs(price) {
        registeredAddresses[msg.sender] = true;
    }

    function changePrice(uint _price) public onlyOwner {
        price = _price;
    }
}

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(!locked);
        locked = true;
        _;
        locked = false;
    }

    // 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用  `f`。
    // `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。
    function f() public noReentrancy returns (uint) {
        require(msg.sender.call());
        return 7;
    }
}

如果同一个函数有多个 修饰器modifier,它们之间以空格隔开,修饰器modifier 会依次检查执行。

警告

在早期的 Solidity 版本中,有 修饰器modifier 的函数,return 语句的行为表现不同。

修饰器modifier 或函数体中显式的 return 语句仅仅跳出当前的 修饰器modifier 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 修饰器modifier 中的定义的 “_” 之后继续执行。

修饰器modifier 的参数可以是任意表达式,在此上下文中,所有在函数中可见的符号,在 修饰器modifier 中均可见。 在 修饰器modifier 中引入的符号在函数中不可见(可能被重载改变)。

Constant 状态变量

状态变量可以被声明为 constant。在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。 任何通过访问 storage,区块链数据(例如 now, this.balance 或者 block.number)或执行数据( msg.gas ) 或对外部合约的调用来给它们赋值都是不允许的。 在内存分配上有边界效应(side-effect)的表达式是允许的,但对其他内存对象产生边界效应的表达式则不行。 内建(built-in)函数 keccak256sha256ripemd160ecrecoveraddmodmulmod 是允许的(即使他们确实会调用外部合约)。

允许带有边界效应的内存分配器的原因是这将允许构建复杂的对象,比如查找表(lookup-table)。 此功能尚未完全可用。

编译器不会为这些变量预留存储,它们的每次出现都会被替换为相应的常量表达式(这将可能被优化器计算为实际的某个值)。

不是所有类型的状态变量都支持用 constant 来修饰,当前支持的仅有值类型和字符串。

pragma solidity ^0.4.0;

contract C {
    uint constant x = 32**22 + 8;
    string constant text = "abc";
    bytes32 constant myHash = keccak256("abc");
}

函数

View 函数

可以将函数声明为 view 类型,这种情况下要保证不修改状态。

下面的语句被认为是修改状态:

  1. 修改状态变量。
  2. 产生事件
  3. 创建其它合约
  4. 使用 selfdestruct
  5. 通过调用发送以太币。
  6. 调用任何没有标记为 view 或者 pure 的函数。
  7. 使用低级调用。
  8. 使用包含特定操作码的内联汇编。
pragma solidity ^0.4.16;

contract C {
    function f(uint a, uint b) public view returns (uint) {
        return a * (b + 42) + now;
    }
}

注解

constantview 的别名。

注解

Getter 方法被标记为 view

警告

编译器没有强制 view 方法不能修改状态。

Pure 函数

函数可以声明为 pure ,在这种情况下,承诺不读取或修改状态。

除了上面解释的状态修改语句列表之外,以下被认为是从状态中读取:

  1. 读取状态变量。
  2. 访问 this.balance 或者 <address>.balance
  3. 访问 blocktxmsg 中任意成员 (除 msg.sigmsg.data 之外)。
  4. 调用任何未标记为 pure 的函数。
  5. 使用包含某些操作码的内联汇编。
pragma solidity ^0.4.16;

contract C {
    function f(uint a, uint b) public pure returns (uint) {
        return a * (b + 42);
    }
}

警告

编译器没有强制 pure 方法不能读取状态。

Fallback 函数

合约可以有一个未命名的函数。这个函数不能有参数也不能有返回值。 如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。

除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable。 如果不存在这样的函数,则合约不能通过常规交易接收以太币。

在这样的上下文中,通常只有很少的 gas 可以用来完成这个函数调用(准确地说,是 2300 gas),所以使 fallback 函数的调用尽量廉价很重要。 请注意,调用 fallback 函数的交易(而不是内部调用)所需的 gas 要高得多,因为每次交易都会额外收取 21000 gas 或更多的费用,用于签名检查等操作。

具体来说,以下操作会消耗比 fallback 函数更多的 gas:

  • 写入存储
  • 创建合约
  • 调用消耗大量 gas 的外部函数
  • 发送以太币

请确保您在部署合约之前彻底测试您的 fallback 函数,以确保执行成本低于 2300 个 gas。

注解

即使 fallback 函数不能有参数,仍然可以使用 msg.data 来获取随调用提供的任何有效数据。

警告

一个没有定义 fallback 函数的合约,直接接收以太币(没有函数调用,即使用 sendtransfer)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。所以如果你想让你的合约接收以太币,必须实现 fallback 函数。

警告

一个没有 payable fallback 函数的合约,可以作为 coinbase transaction (又名 miner block reward )的接收者或者作为 selfdestruct 的目标来接收以太币。

一个合约不能对这种以太币转移做出反应,因此也不能拒绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无法绕过这个问题。

这也意味着 this.balance 可以高于合约中实现的一些手工记帐的总和(即在 fallback 函数中更新的累加器)。

pragma solidity ^0.4.0;

contract Test {
    // 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
    // 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
    function() public { x = 1; }
    uint x;
}


// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract Sink {
    function() public payable { }
}

contract Caller {
    function callTest(Test test) public {
        test.call(0xabcdef01); // 不存在的哈希
        // 导致 test.x 变成 == 1。
        // 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
        // test.send(2 ether);
    }
}
函数重载

合约可以具有多个不同参数的同名函数。这也适用于继承函数。以下示例展示了合约 A 中的重载函数 f

pragma solidity ^0.4.16;

contract A {
    function f(uint _in) public pure returns (uint out) {
        out = 1;
    }

    function f(uint _in, bytes32 _key) public pure returns (uint out) {
        out = 2;
    }
}

重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。

// 以下代码无法编译
pragma solidity ^0.4.16;

contract A {
    function f(B _in) public pure returns (B out) {
        out = _in;
    }

    function f(address _in) public pure returns (address out) {
        out = _in;
    }
}

contract B {
}

以上两个 f 函数重载都接受了 ABI 的地址类型,虽然它们在 Solidity 中被认为是不同的。

重载解析和参数匹配

通过将当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。 如果所有参数都可以隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。

注解

返回参数不作为重载解析的依据。

pragma solidity ^0.4.16;

contract A {
    function f(uint8 _in) public pure returns (uint8 out) {
        out = _in;
    }

    function f(uint256 _in) public pure returns (uint256 out) {
        out = _in;
    }
}

调用 f(50) 会导致类型错误,因为 50 既可以被隐式转换为 uint8 也可以被隐式转换为 uint256。 另一方面,调用 f(256) 则会解析为 f(uint256) 重载,因为 256 不能隐式转换为 uint8

事件

事件允许我们方便地使用 EVM 的日志基础设施。 我们可以在 dapp 的用户界面中监听事件,EVM 的日志机制可以反过来“调用”用来监听事件的 Javascript 回调函数。

事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。 这些日志与地址相关联,被并入区块链中,只要区块可以访问就一直存在(在 Frontier 和 Homestead 版本中会被永久保存,在 Serenity 版本中可能会改动)。 日志和事件在合约内不可直接被访问(甚至是创建日志的合约也不能访问)。

对日志的 SPV(Simplified Payment Verification)证明是可能的,如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中。 但需要留意的是,由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。

最多三个参数可以接收 indexed 属性,从而使它们可以被搜索:在用户界面上可以使用 indexed 参数的特定值来进行过滤。

如果数组(包括 stringbytes)类型被标记为索引项,则它们的 keccak-256 哈希值会被作为 topic 保存。

除非你用 anonymous 说明符声明事件,否则事件签名的哈希值是 topic 之一。 同时也意味着对于匿名事件无法通过名字来过滤。

所有非索引参数都将存储在日志的数据部分中。

注解

索引参数本身不会被保存。你只能搜索它们的值(来确定相应的日志数据是否存在),而不能获取它们的值本身。

pragma solidity ^0.4.0;

contract ClientReceipt {
    event Deposit(
        address indexed _from,
        bytes32 indexed _id,
        uint _value
    );

    function deposit(bytes32 _id) public payable {
        // 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。
        Deposit(msg.sender, _id, msg.value);
    }
}

使用 JavaScript API 调用事件的用法如下:

var abi = /* abi 由编译器产生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* 地址 */);

var event = clientReceipt.Deposit();

// 监视变化
event.watch(function(error, result){
    // 结果包括对 `Deposit` 的调用参数在内的各种信息。
    if (!error)
        console.log(result);
});

// 或者通过回调立即开始观察
var event = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});
日志的底层接口

通过函数 log0log1log2log3log4 可以访问日志机制的底层接口。 logi 接受 i + 1bytes32 类型的参数。其中第一个参数会被用来做为日志的数据部分, 其它的会做为 topic。上面的事件调用可以以相同的方式执行。

pragma solidity ^0.4.10;

contract C {
    function f() public payable {
        bytes32 _id = 0x420042;
        log3(
            bytes32(msg.value),
            bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
            bytes32(msg.sender),
            _id
        );
    }
}

其中的长十六进制数的计算方法是 keccak256("Deposit(address,hash256,uint256)"),即事件的签名。

继承

通过复制包括多态的代码,Solidity 支持多重继承。

所有的函数调用都是虚拟的,这意味着最远的派生函数会被调用,除非明确给出合约名称。

当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约的代码被复制到创建的合约中。

总的来说,Solidity 的继承系统与 Python的继承系统 ,非常 相似,特别是多重继承方面。

下面的例子进行了详细的说明。

pragma solidity ^0.4.16;

contract owned {
    function owned() { owner = msg.sender; }
    address owner;
}

// 使用 is 从另一个合约派生。派生合约可以访问所有非私有成员,包括内部函数和状态变量,
// 但无法通过 this 来外部访问。
contract mortal is owned {
    function kill() {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

// 这些抽象合约仅用于给编译器提供接口。
// 注意函数没有函数体。
// 如果一个合约没有实现所有函数,则只能用作接口。
contract Config {
    function lookup(uint id) public returns (address adr);
}

contract NameReg {
    function register(bytes32 name) public;
    function unregister() public;
 }

// 可以多重继承。请注意,owned 也是 mortal 的基类,
// 但只有一个 owned 实例(就像 C++ 中的虚拟继承)。
contract named is owned, mortal {
    function named(bytes32 name) {
        Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
        NameReg(config.lookup(1)).register(name);
    }

    // 函数可以被另一个具有相同名称和相同数量/类型输入的函数重载。
    // 如果重载函数有不同类型的输出参数,会导致错误。
    // 本地和基于消息的函数调用都会考虑这些重载。
    function kill() public {
        if (msg.sender == owner) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // 仍然可以调用特定的重载函数。
            mortal.kill();
        }
    }
}

// 如果构造函数接受参数,
// 则需要在声明(合约的构造函数)时提供,
// 或在派生合约的构造函数位置以修饰器调用风格提供(见下文)。
contract PriceFeed is owned, mortal, named("GoldFeed") {
   function updateInfo(uint newInfo) public {
      if (msg.sender == owner) info = newInfo;
   }

   function get() public view returns(uint r) { return info; }

   uint info;
}

注意,在上边的代码中,我们调用 mortal.kill() 来“转发”销毁请求。 这样做法是有问题的,在下面的例子中可以看到:

pragma solidity ^0.4.0;

contract owned {
    function owned() public { owner = msg.sender; }
    address owner;
}

contract mortal is owned {
    function kill() public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is mortal {
    function kill() public { /* 清除操作 1 */ mortal.kill(); }
}

contract Base2 is mortal {
    function kill() public { /* 清除操作 2 */ mortal.kill(); }
}

contract Final is Base1, Base2 {
}

调用 Final.kill() 时会调用最远的派生重载函数 Base2.kill,但是会绕过 Base1.kill, 主要是因为它甚至都不知道 Base1 的存在。解决这个问题的方法是使用 super:

pragma solidity ^0.4.0;

contract owned {
    function owned() public { owner = msg.sender; }
    address owner;
}

contract mortal is owned {
    function kill() public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is mortal {
    function kill() public { /* 清除操作 1 */ super.kill(); }
}


contract Base2 is mortal {
    function kill() public { /* 清除操作 2 */ super.kill(); }
}

contract Final is Base1, Base2 {
}

如果 Base2 调用 super 的函数,它不会简单在其基类合约上调用该函数。 相反,它在最终的继承关系图谱的下一个基类合约中调用这个函数,所以它会调用 Base1.kill() (注意最终的继承序列是——从最远派生合约开始:Final, Base2, Base1, mortal, ownerd)。 在类中使用 super 调用的实际函数在当前类的上下文中是未知的,尽管它的类型是已知的。 这与普通的虚拟方法查找类似。

基类构造函数的参数

派生合约需要提供基类构造函数需要的所有参数。这可以通过两种方式来完成:

pragma solidity ^0.4.0;

contract Base {
    uint x;
    function Base(uint _x) public { x = _x; }
}

contract Derived is Base(7) {
    function Derived(uint _y) Base(_y * _y) public {
    }
}

一种方法直接在继承列表中调用基类构造函数(is Base(7))。 另一种方法是像 修饰器modifier 使用方法一样, 作为派生合约构造函数定义头的一部分,(Base(_y * _y))。 如果构造函数参数是常量并且定义或描述了合约的行为,使用第一种方法比较方便。 如果基类构造函数的参数依赖于派生合约,那么必须使用第二种方法。 如果像这个简单的例子一样,两个地方都用到了,优先使用 修饰器modifier 风格的参数。

多重继承与线性化

编程语言实现多重继承需要解决几个问题。 一个问题是 钻石问题。 Solidity 借鉴了 Python 的方式并且使用“ C3 线性化 ”强制一个由基类构成的 DAG(有向无环图)保持一个特定的顺序。 这最终反映为我们所希望的唯一化的结果,但也使某些继承方式变为无效。尤其是,基类在 is 后面的顺序很重要。 在下面的代码中,Solidity 会给出“ Linearization of inheritance graph impossible ”这样的错误。

// 以下代码编译出错

pragma solidity ^0.4.0;

contract X {}
contract A is X {}
contract C is A, X {}

代码编译出错的原因是 C 要求 X 重写 A (因为定义的顺序是 A, X ), 但是 A 本身要求重写 X,无法解决这种冲突。

可以通过一个简单的规则来记忆: 以从“最接近的基类”(most base-like)到“最远的继承”(most derived)的顺序来指定所有的基类。

继承有相同名字的不同类型成员

当继承导致一个合约具有相同名字的函数和 修饰器modifier 时,这会被认为是一个错误。 当事件和 修饰器modifier 同名,或者函数和事件同名时,同样会被认为是一个错误。 有一种例外情况,状态变量的 getter 可以覆盖一个 public 函数。

抽象合约

合约函数可以缺少实现,如下例所示(请注意函数声明头由 ; 结尾):

pragma solidity ^0.4.0;

contract Feline {
    function utterance() public returns (bytes32);
}

这些合约无法成功编译(即使它们除了未实现的函数还包含其他已经实现了的函数),但他们可以用作基类合约:

pragma solidity ^0.4.0;

contract Feline {
    function utterance() public returns (bytes32);
}

contract Cat is Feline {
    function utterance() public returns (bytes32) { return "miaow"; }
}

如果合约继承自抽象合约,并且没有通过重写来实现所有未实现的函数,那么它本身就是抽象的。

接口

接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制:

  1. 无法继承其他合约或接口。
  2. 无法定义构造函数。
  3. 无法定义变量。
  4. 无法定义结构体
  5. 无法定义枚举。

将来可能会解除这里的某些限制。

接口基本上仅限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换应该不会丢失任何信息。

接口由它们自己的关键字表示:

pragma solidity ^0.4.11;

interface Token {
    function transfer(address recipient, uint amount) public;
}

就像继承其他合约一样,合约可以继承接口。

库与合约类似,它们只需要在特定的地址部署一次,并且它们的代码可以通过 EVM 的 DELEGATECALL (Homestead 之前使用 CALLCODE 关键字)特性进行重用。 这意味着如果库函数被调用,它的代码在调用合约的上下文中执行,即 this 指向调用合约,特别是可以访问调用合约的存储。 因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。 因为我们假定库是无状态的,所以如果它们不修改状态(也就是说,如果它们是 view 或者 pure 函数), 库函数仅可以通过直接调用来使用(即不使用 DELEGATECALL 关键字), 特别是,除非能规避 Solidity 的类型系统,否则是不可能销毁任何库的。

库可以看作是使用他们的合约的隐式的基类合约。虽然它们在继承关系中不会显式可见,但调用库函数与调用显式的基类合约十分类似 (如果 L 是库的话,可以使用 L.f() 调用库函数)。此外,就像库是基类合约一样,对所有使用库的合约,库的 internal 函数都是可见的。 当然,需要使用内部调用约定来调用内部函数,这意味着所有内部类型,内存类型都是通过引用而不是复制来传递。 为了在 EVM 中实现这些,内部库函数的代码和从其中调用的所有函数都在编译阶段被拉取到调用合约中,然后使用一个 JUMP 调用来代替 DELEGATECALL

下面的示例说明如何使用库(但也请务必看看 using for 有一个实现 set 更好的例子)。

pragma solidity ^0.4.16;

library Set {
  // 我们定义了一个新的结构体数据类型,用于在调用合约中保存数据。
  struct Data { mapping(uint => bool) flags; }

  // 注意第一个参数是“storage reference”类型,因此在调用中参数传递的只是它的存储地址而不是内容。
  // 这是库函数的一个特性。如果该函数可以被视为对象的方法,则习惯称第一个参数为 `self` 。
  function insert(Data storage self, uint value)
      public
      returns (bool)
  {
      if (self.flags[value])
          return false; // 已经存在
      self.flags[value] = true;
      return true;
  }

  function remove(Data storage self, uint value)
      public
      returns (bool)
  {
      if (!self.flags[value])
          return false; // 不存在
      self.flags[value] = false;
      return true;
  }

  function contains(Data storage self, uint value)
      public
      view
      returns (bool)
  {
      return self.flags[value];
  }
}

contract C {
    Set.Data knownValues;

    function register(uint value) public {
        // 不需要库的特定实例就可以调用库函数,
        // 因为当前合约就是“instance”。
        require(Set.insert(knownValues, value));
    }
    // 如果我们愿意,我们也可以在这个合约中直接访问 knownValues.flags。
}

当然,你不必按照这种方式去使用库:它们也可以在不定义结构数据类型的情况下使用。 函数也不需要任何存储引用参数,库可以出现在任何位置并且可以有多个存储引用参数。

调用 Set.containsSet.insertSet.remove 都被编译为外部调用( DELEGATECALL )。 如果使用库,请注意实际执行的是外部函数调用。 msg.sendermsg.valuethis 在调用中将保留它们的值, (在 Homestead 之前,因为使用了 CALLCODE,改变了 msg.sendermsg.value)。

以下示例展示了如何在库中使用内存类型和内部函数来实现自定义类型,而无需支付外部函数调用的开销:

pragma solidity ^0.4.16;

library BigInt {
    struct bigint {
        uint[] limbs;
    }

    function fromUint(uint x) internal pure returns (bigint r) {
        r.limbs = new uint[](1);
        r.limbs[0] = x;
    }

    function add(bigint _a, bigint _b) internal pure returns (bigint r) {
        r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
        uint carry = 0;
        for (uint i = 0; i < r.limbs.length; ++i) {
            uint a = limb(_a, i);
            uint b = limb(_b, i);
            r.limbs[i] = a + b + carry;
            if (a + b < a || (a + b == uint(-1) && carry > 0))
                carry = 1;
            else
                carry = 0;
        }
        if (carry > 0) {
            // 太差了,我们需要增加一个 limb
            uint[] memory newLimbs = new uint[](r.limbs.length + 1);
            for (i = 0; i < r.limbs.length; ++i)
                newLimbs[i] = r.limbs[i];
            newLimbs[i] = carry;
            r.limbs = newLimbs;
        }
    }

    function limb(bigint _a, uint _limb) internal pure returns (uint) {
        return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
    }

    function max(uint a, uint b) private pure returns (uint) {
        return a > b ? a : b;
    }
}

contract C {
    using BigInt for BigInt.bigint;

    function f() public pure {
        var x = BigInt.fromUint(7);
        var y = BigInt.fromUint(uint(-1));
        var z = x.add(y);
    }
}

由于编译器无法知道库的部署位置,我们需要通过链接器将这些地址填入最终的字节码中 (请参阅 使用命令行编译器 以了解如何使用命令行编译器来链接字节码)。 如果这些地址没有作为参数传递给编译器,编译后的十六进制代码将包含 __Set______ 形式的占位符(其中 Set 是库的名称)。 可以手动填写地址来将那 40 个字符替换为库合约地址的十六进制编码。

与合约相比,库的限制:

  • 没有状态变量
  • 不能够继承或被继承
  • 不能接收以太币

(将来有可能会解除这些限制)

库的调用保护

如果库的代码是通过 CALL 来执行,而不是 DELEGATECALL 或者 CALLCODE 那么执行的结果会被回退, 除非是对 view 或者 pure 函数的调用。

EVM 没有为合约提供检测是否使用 CALL 的直接方式,但是合约可以使用 ADDRESS 操作码找出正在运行的“位置”。 生成的代码通过比较这个地址和构造时的地址来确定调用模式。

更具体地说,库的运行时代码总是从一个 push 指令开始,它在编译时是 20 字节的零。当部署代码运行时,这个常数 被内存中的当前地址替换,修改后的代码存储在合约中。在运行时,这导致部署时地址是第一个被 push 到堆栈上的常数, 对于任何 non-view 和 non-pure 函数,调度器代码都将对比当前地址与这个常数是否一致。

Using For

指令 using A for B; 可用于附加库函数(从库 A)到任何类型(B)。 这些函数将接收到调用它们的对象作为它们的第一个参数(像 Python 的 self 变量)。

using A for *; 的效果是,库 A 中的函数被附加在任意的类型上。

在这两种情况下,所有函数都会被附加一个参数,即使它们的第一个参数类型与对象的类型不匹配。 函数调用和重载解析时才会做类型检查。

using A for B; 指令仅在当前作用域有效,目前仅限于在当前合约中,后续可能提升到全局范围。 通过引入一个模块,不需要再添加代码就可以使用包括库函数在内的数据类型。

让我们用这种方式将 中的 set 例子重写:

pragma solidity ^0.4.16;

// 这是和之前一样的代码,只是没有注释。
library Set {
  struct Data { mapping(uint => bool) flags; }

  function insert(Data storage self, uint value)
      public
      returns (bool)
  {
      if (self.flags[value])
        return false; // 已经存在
      self.flags[value] = true;
      return true;
  }

  function remove(Data storage self, uint value)
      public
      returns (bool)
  {
      if (!self.flags[value])
          return false; // 不存在
      self.flags[value] = false;
      return true;
  }

  function contains(Data storage self, uint value)
      public
      view
      returns (bool)
  {
      return self.flags[value];
  }
}

contract C {
    using Set for Set.Data; // 这里是关键的修改
    Set.Data knownValues;

    function register(uint value) public {
        // Here, all variables of type Set.Data have
        // corresponding member functions.
        // The following function call is identical to
        // `Set.insert(knownValues, value)`
        // 这里, Set.Data 类型的所有变量都有与之相对应的成员函数。
        // 下面的函数调用和 `Set.insert(knownValues, value)` 的效果完全相同。
        require(knownValues.insert(value));
    }
}

也可以像这样扩展基本类型:

pragma solidity ^0.4.16;

library Search {
    function indexOf(uint[] storage self, uint value)
        public
        view
        returns (uint)
    {
        for (uint i = 0; i < self.length; i++)
            if (self[i] == value) return i;
        return uint(-1);
    }
}

contract C {
    using Search for uint[];
    uint[] data;

    function append(uint value) public {
        data.push(value);
    }

    function replace(uint _old, uint _new) public {
        // 执行库函数调用
        uint index = data.indexOf(_old);
        if (index == uint(-1))
            data.push(_new);
        else
            data[index] = _new;
    }
}

注意,所有库调用都是实际的 EVM 函数调用。这意味着如果传递内存或值类型,都将产生一个副本,即使是 self 变量。 使用存储引用变量是唯一不会发生拷贝的情况。

Solidity汇编

Solidity 定义了一种汇编语言,在没有 Solidity 的情况下也可以使用。这种汇编语言也可以嵌入到 Solidity 源代码中当作“内联汇编”使用。 我们从如何使用内联汇编开始,介绍它如何区别于独立汇编语言,然后详细讲述这种汇编语言。

内联汇编

为了实现更细粒度的控制,尤其是为了通过编写库来增强语言,可以利用接近虚拟机的语言将内联汇编与 Solidity 语句结合在一起使用。 由于 EVM 是基于栈的虚拟机,因此通常很难准确地定位栈内插槽(存储位置)的地址,并为操作码提供正确的栈内位置来获取参数。 Solidity 的内联汇编试图通过提供以下特性来解决这个问题以及手工编写汇编代码时可能出现的问题:

  • 函数风格操作码: mul(1, add(2, 3)) 而不是 push1 3 push1 2 add push1 1 mul
  • 汇编局部变量: let x := add(2, 3)  let y := mload(0x40)  x := add(x, y)
  • 可以访问外部变量: function f(uint x) public { assembly { x := sub(x, 1) } }
  • 标签: let x := 10  repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))
  • 循环: for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
  • if 语句: if slt(x, 0) { x := sub(0, x) }
  • switch 语句: switch x case 0 { y := mul(x, 2) } default { y := 0 }
  • 函数调用: function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) }   }

现在我们详细讲解内联汇编语言。

警告

内联汇编是一种在底层访问以太坊虚拟机的语言。这抛弃了很多 Solidity 提供的重要安全特性。

注解

TODO:写出在内联汇编中作用域规则的细微差别,以及在使用库合约的内部函数时产生的复杂性。此外,还要编写有关编译器定义的符号。

例子

下面例子展示了一个库合约的代码,它可以取得另一个合约的代码,并将其加载到一个 bytes 变量中。 这对于“常规 Solidity”来说是根本不可能的,汇编库合约则可以通过这种方式来增强语言特性。

pragma solidity ^0.4.0;

library GetCode {
    function at(address _addr) public view returns (bytes o_code) {
        assembly {
            // 获取代码大小,这需要汇编语言
            let size := extcodesize(_addr)
            // 分配输出字节数组 – 这也可以不用汇编语言来实现
            // 通过使用 o_code = new bytes(size)
            o_code := mload(0x40)
            // 包括补位在内新的“memory end”
            mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            // 把长度保存到内存中
            mstore(o_code, size)
            // 实际获取代码,这需要汇编语言
            extcodecopy(_addr, add(o_code, 0x20), 0, size)
        }
    }
}

在优化器无法生成高效代码的情况下,内联汇编也可能更有好处。请注意,由于编译器无法对汇编语句进行相关的检查,所以编写汇编代码肯定更加困难; 因此只有在处理一些相对复杂的问题时才需要使用它,并且你需要明确知道自己要做什么。

pragma solidity ^0.4.16;

library VectorSum {
    // 因为目前的优化器在访问数组时无法移除边界检查,
    // 所以这个函数的执行效率比较低。
    function sumSolidity(uint[] _data) public view returns (uint o_sum) {
        for (uint i = 0; i < _data.length; ++i)
            o_sum += _data[i];
    }

    // 我们知道我们只能在数组范围内访问数组元素,所以我们可以在内联汇编中不做边界检查。
    // 由于 ABI 编码中数组数据的第一个字(32 字节)的位置保存的是数组长度,
    // 所以我们在访问数组元素时需要加入 0x20 作为偏移量。
    function sumAsm(uint[] _data) public view returns (uint o_sum) {
        for (uint i = 0; i < _data.length; ++i) {
            assembly {
                o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
            }
        }
    }

    // 和上面一样,但在内联汇编内完成整个代码。
    function sumPureAsm(uint[] _data) public view returns (uint o_sum) {
        assembly {
           // 取得数组长度(前 32 字节)
           let len := mload(_data)

           // 略过长度字段。
           //
           // 保持临时变量以便它可以在原地增加。
           //
           // 注意:对 _data 数值的增加将导致 _data 在这个汇编语句块之后不再可用。
           //      因为无法再基于 _data 来解析后续的数组数据。
           let data := add(_data, 0x20)

           // 迭代到数组数据结束
           for
               { let end := add(data, mul(len, 0x20)) }
               lt(data, end)
               { data := add(data, 0x20) }
           {
               o_sum := add(o_sum, mload(data))
           }
        }
    }
}
语法

和 Solidity 一样,Assembly 也会解析注释、文字和标识符,所以你可以使用通常的 ///* */ 来进行注释。 内联汇编程序由 assembly { ... } 来标记,在这些大括号内可以使用以下内容(更多详细信息请参阅后面部分)。

  • 字面常数,也就是 0x12342"abc" (不超过 32 个字符的字符串)
  • 操作码(在“instruction style”内),比如 mload sload dup1 sstore,操作码列表请看后面
  • 函数风格操作码,比如 add(1,mlod(0))
  • 标签,比如 name:
  • 变量声明,比如 let x := 7let x := add(y, 3) 或者 let x (初始值将被置为 empty(0))
  • 标识符(标签或者汇编局部变量以及用作内联汇编时的外部变量),比如 jump(name)3 x add
  • 赋值(在“instruction style”内),比如 3 =: x
  • 函数风格赋值,比如 x := add(y,3)
  • 一些控制局部变量作用域的语句块,比如 {let x := 3 { let y := add(x,1) }}
操作码

本文档不是以太坊虚拟机的详细描述,但下边的列表可以作为操作码参考。

如果一个操作码需要参数(总是来自堆栈顶部),它们会在括号中给出。请注意:参数顺序可以看作是在非函数风格中逆序(下面会解释)。 标有 - 的操作码不会向栈中压入(push)数据,标有 * 的操作码有特殊操作,而所有其他操作码都只会将一个数据压入(push)栈中。 用 FHBC 标记的操作码代表它们从 Frontier、Homestead、Byzantium 或 Constantinople 开始被引入。 Constantinople 目前仍在计划中,所以标记为 C 的指令目前都会导致一个非法指令异常。

在下表中,mem[a...b) 表示从位置 a 开始至(不包括)位置 b 的内存字节数,storage[p] 表示位置 p 处的存储内容。

pushijumpdest 这两个操作码不能直接用。

在语法表中,操作码是作为预定义标识符提供的。

Instruction     Explanation
stop - F 停止执行,与 return(0,0) 等价
add(x, y)   F x + y
sub(x, y)   F x - y
mul(x, y)   F x * y
div(x, y)   F x / y
sdiv(x, y)   F x / y,以二进制补码作为符号
mod(x, y)   F x % y
smod(x, y)   F x % y,以二进制补码作为符号
exp(x, y)   F x 的 y 次幂
not(x)   F ~x,对 x 按位取反
lt(x, y)   F 如果 x < y 为 1,否则为 0
gt(x, y)   F 如果 x > y 为 1,否则为 0
slt(x, y)   F 如果 x < y 为 1,否则为 0,以二进制补码作为符号
sgt(x, y)   F 如果 x > y 为 1,否则为 0,以二进制补码作为符号
eq(x, y)   F 如果 x == y 为 1,否则为 0
iszero(x)   F 如果 x == 0 为 1,否则为 0
and(x, y)   F x 和 y 的按位与
or(x, y)   F x 和 y 的按位或
xor(x, y)   F x 和 y 的按位异或
byte(n, x)   F x 的第 n 个字节,这个索引是从 0 开始的
shl(x, y)   C 将 y 逻辑左移 x 位
shr(x, y)   C 将 y 逻辑右移 x 位
sar(x, y)   C 将 y 算术右移 x 位
addmod(x, y, m)   F 任意精度的 (x + y) % m
mulmod(x, y, m)   F 任意精度的 (x * y) % m
signextend(i, x)   F 对 x 的最低位到第 (i * 8 + 7) 进行符号扩展
keccak256(p, n)   F keccak(mem[p...(p + n)))
jump(label) - F 跳转到标签 / 代码位置
jumpi(label, cond) - F 如果条件为非零,跳转到标签
pc   F 当前代码位置
pop(x) - F 删除(弹出)栈顶的 x 个元素
dup1 ... dup16   F 将栈内第 i 个元素(从栈顶算起)复制到栈顶
swap1 ... swap16 * F 将栈顶元素和其下第 i 个元素互换
mload(p)   F mem[p...(p + 32))
mstore(p, v) - F mem[p...(p + 32)) := v
mstore8(p, v) - F mem[p] := v & 0xff (仅修改一个字节)
sload(p)   F storage[p]
sstore(p, v) - F storage[p] := v
msize   F 内存大小,即最大可访问内存索引
gas   F 执行可用的 gas
address   F 当前合约 / 执行上下文的地址
balance(a)   F 地址 a 的余额,以 wei 为单位
caller   F 调用发起者(不包括 delegatecall
callvalue   F 随调用发送的 Wei 的数量
calldataload(p)   F 位置 p 的调用数据(32 字节)
calldatasize   F 调用数据的字节数大小
calldatacopy(t, f, s) - F 从调用数据的位置 f 的拷贝 s 个字节到内存的位置 t
codesize   F 当前合约 / 执行上下文地址的代码大小
codecopy(t, f, s) - F 从代码的位置 f 开始拷贝 s 个字节到内存的位置 t
extcodesize(a)   F 地址 a 的代码大小
extcodecopy(a, t, f, s) - F 和 codecopy(t, f, s) 类似,但从地址 a 获取代码
returndatasize   B 最后一个 returndata 的大小
returndatacopy(t, f, s) - B 从 returndata 的位置 f 拷贝 s 个字节到内存的位置 t
create(v, p, s)   F 用 mem[p...(p + s)) 中的代码创建一个新合约、发送 v wei 并返回 新地址
create2(v, n, p, s)   C 用 mem[p...(p + s)) 中的代码,在地址 keccak256(<address> . n . keccak256(mem[p...(p + s))) 上 创建新合约、发送 v wei 并返回新地址
call(g, a, v, in, insize, out, outsize)   F 使用 mem[in...(in + insize)) 作为输入数据, 提供 g gas 和 v wei 对地址 a 发起消息调用, 输出结果数据保存在 mem[out...(out + outsize)), 发生错误(比如 gas 不足)时返回 0,正确结束返回 1
callcode(g, a, v, in, insize, out, outsize)   F call 等价,但仅使用地址 a 中的代码 且保持当前合约的执行上下文
delegatecall(g, a, in, insize, out, outsize)   F callcode 等价且保留 callercallvalue
staticcall(g, a, in, insize, out, outsize)   F call(g, a, 0, in, insize, out, outsize) 等价 但不允许状态修改
return(p, s) - F 终止运行,返回 mem[p...(p + s)) 的数据
revert(p, s) - B 终止运行,撤销状态变化,返回 mem[p...(p + s)) 的数据
selfdestruct(a) - F 终止运行,销毁当前合约并且把资金发送到地址 a
invalid - F 以无效指令终止运行
log0(p, s) - F 以 mem[p...(p + s)) 的数据产生不带 topic 的日志
log1(p, s, t1) - F 以 mem[p...(p + s)) 的数据和 topic t1 产生日志
log2(p, s, t1, t2) - F 以 mem[p...(p + s)) 的数据和 topic t1、t2 产生日志
log3(p, s, t1, t2, t3) - F 以 mem[p...(p + s)) 的数据和 topic t1、t2、t3 产生日志
log4(p, s, t1, t2, t3, t4) - F 以 mem[p...(p + s)) 的数据和 topic t1、t2、t3 和 t4 产生日志
origin   F 交易发起者地址
gasprice   F 交易所指定的 gas 价格
blockhash(b)   F 区块号 b 的哈希 - 目前仅适用于不包括当前区块的最后 256 个区块
coinbase   F 当前的挖矿收益者地址
timestamp   F 从当前 epoch 开始的当前区块时间戳(以秒为单位)
number   F 当前区块号
difficulty   F 当前区块难度
gaslimit   F 当前区块的 gas 上限
字面常量

你可以直接键入十进制或十六进制符号来作为整型常量使用,这会自动生成相应的 PUSHi 指令。 下面的代码将计算 2 加 3(等于 5),然后计算其与字符串 “abc” 的按位与。字符串在存储时为左对齐,且长度不能超过 32 字节。

assembly { 2 3 add "abc" and }
函数风格

你可以像使用字节码那样在操作码之后键入操作码。例如,把 3 与内存位置 0x80 处的数据相加就是

3 0x80 mload add 0x80 mstore

由于通常很难看到某些操作码的实际参数是什么,所以 Solidity 内联汇编还提供了一种“函数风格”表示法,同样功能的代码可以写做

mstore(0x80, add(mload(0x80), 3))

函数风格表达式内不能使用指令风格的写法,即 1 2 mstore(0x80, add) 是无效汇编语句, 它必须写成 mstore(0x80, add(2, 1)) 这种形式。对于不带参数的操作码,括号可以省略。

注意,在函数风格写法中参数的顺序与指令风格相反。如果使用函数风格写法,第一个参数将会位于栈顶。

访问外部变量和函数

通过简单使用它们名称就可以访问 Solidity 变量和其他标识符。对于内存变量,这会将地址而不是值压入栈中。 存储变量是不同的,因为存储变量的值可能不占用完整的存储槽,因此其“地址”由存储槽和槽内的字节偏移量组成。 为了获取变量 x 所使用的存储槽,你可以使用 x_slot,并用的 x_offset 获取其字节偏移量。

在赋值语句中(见下文),我们甚至可以使用 Solidity 局部变量来赋值。

对于内联汇编而言的外部函数也可以被访问:汇编会将它们的入口标签(带有虚拟函数解析)压入栈中。Solidity 中的调用语义为:

  • 调用者压入 return labelarg1arg2、...、argn
  • 被调用方返回 ret1ret2、...、retm

这个特性使用起来还是有点麻烦,因为在调用过程中堆栈偏移量发生了根本变化,因此对局部变量的引用将会出错。

pragma solidity ^0.4.11;

contract C {
    uint b;
    function f(uint x) public returns (uint r) {
        assembly {
            r := mul(x, sload(b_slot)) // 因为偏移量为 0,所以可以忽略
        }
    }
}

注解

如果你访问一个实际数据位数小于 256 位的数据类型(比如 uint64addressbytes16byte), 不要对这种类型经过编码后未使用的数据位上的数值做任何假设。尤其是不要假设它们肯定为 0。 安全起见,在某个上下文中使用这种数据之前,请一定先将其数据清空为 0,这非常重要: uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ } 要清空有符号类型,你可以使用 signextend 操作码。

标签

注解

标签已经不推荐使用。请使用函数、循环、if 或 switch 语句。

EVM 汇编的另一个问题是 jump 和 jumpi 函数使用绝对地址,这些绝对地址很容易改变。 Solidity 内联汇编提供了标签,以便更容易地使用 jump。注意,标签具有底层特征,使用循环、if 和 switch 指令(参见下文)而不使用标签也能写出高效汇编代码。 以下代码用来计算斐波那契数列中的一个元素。

{
    let n := calldataload(4)
    let a := 1
    let b := a
loop:
    jumpi(loopend, eq(n, 0))
    a add swap1
    n := sub(n, 1)
    jump(loop)
loopend:
    mstore(0, a)
    return(0, 0x20)
}

请注意:只有汇编程序知道当前栈高度时,才能自动访问堆栈变量。如果 jump 源和目标的栈高度不同,访问将失败。 虽然我们可以这么使用 jump,但在这种情况下,你不应该去访问任何栈里的变量(即使是汇编变量)。

此外,栈高度分析器还可以通过操作码(而不是根据控制流)检查代码操作码,因此在下面的情况下,汇编程序对标签 two 处的堆栈高度会产生错误的印象:

{
    let x := 8
    jump(two)
    one:
        // 这里的栈高度是 2(因为我们压入了 x 和 7),
        // 但因为汇编程序是按顺序读取代码的,
        // 它会认为栈高度是 1。
        // 在这里访问栈变量 x 会导致错误。
        x := 9
        jump(three)
    two:
        7 // 把某个数据压入栈中
        jump(one)
    three:
}
汇编局部变量声明

你可以使用 let 关键字来声明只在内联汇编中可见的变量,实际上只在当前的 {...} 块中可见。 下面发生的事情应该是:let 指令将创建一个为变量保留的新数据槽,并在到达块末尾时自动删除。 你需要为变量提供一个初始值,它可以只是 0,但它也可以是一个复杂的函数风格表达式。

pragma solidity ^0.4.16;

contract C {
    function f(uint x) public view returns (uint b) {
        assembly {
            let v := add(x, 1)
            mstore(0x80, v)
            {
                let y := add(sload(v), 1)
                b := y
            } // y 会在这里被“清除”
            b := add(b, v)
        } // v 会在这里被“清除”
    }
}
赋值

可以给汇编局部变量和函数局部变量赋值。请注意:当给指向内存或存储的变量赋值时,你只是更改指针而不是数据。

有两种赋值方式:函数风格和指令风格。对于函数风格赋值(变量 := ),你需要在函数风格表达式中提供一个值,它恰好可以产生一个栈里的值; 对于指令风格赋值(=: 变量),则仅从栈顶部获取数据。对于这两种方式,冒号均指向变量名称。赋值则是通过用新值替换栈中的变量值来实现的。

{
    let v := 0 // 作为变量声明的函数风格赋值
    let g := add(v, 2)
    sload(10)
    =: v // 指令风格的赋值,将 sload(10) 的结果赋给 v
}

注解

指令风格的赋值已经不推荐。

If

if 语句可以用于有条件地执行代码,且没有“else”部分;如果需要多种选择,你可以考虑使用“switch”(见下文)。

{
    if eq(value, 0) { revert(0, 0) }
}

代码主体的花括号是必需的。

Switch

作为“if/else”的非常初级的版本,你可以使用 switch 语句。它计算表达式的值并与几个常量进行比较。选出与匹配常数对应的分支。 与某些编程语言容易出错的情况不同,控制流不会从一种情形继续执行到下一种情形。我们可以设定一个 fallback 或称为 default 的默认情况。

{
    let x := 0
    switch calldataload(4)
    case 0 {
        x := calldataload(0x24)
    }
    default {
        x := calldataload(0x44)
    }
    sstore(0, div(x, 2))
}

Case 列表里面不需要大括号,但 case 主体需要。

循环

汇编语言支持一个简单的 for-style 循环。For-style 循环有一个头,它包含初始化部分、条件和迭代后处理部分。 条件必须是函数风格表达式,而另外两个部分都是语句块。如果起始部分声明了某个变量,这些变量的作用域将扩展到循环体中(包括条件和迭代后处理部分)。

下面例子是计算某个内存区域中的数值总和。

{
    let x := 0
    for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
        x := add(x, mload(i))
    }
}

For 循环也可以写成像 while 循环一样:只需将初始化部分和迭代后处理两部分留空。

{
    let x := 0
    let i := 0
    for { } lt(i, 0x100) { } {     // while(i < 0x100)
        x := add(x, mload(i))
        i := add(i, 0x20)
    }
}
函数

汇编语言允许定义底层函数。底层函数需要从栈中取得它们的参数(和返回 PC),并将结果放入栈中。调用函数的方式与执行函数风格操作码相同。

函数可以在任何地方定义,并且在声明它们的语句块中可见。函数内部不能访问在函数之外定义的局部变量。这里没有严格的 return 语句。

如果调用会返回多个值的函数,则必须使用 a,b:= f(x)let a,b:= f(x) 的方式把它们赋值到一个元组。

下面例子通过平方和乘法实现了幂运算函数。

{
    function power(base, exponent) -> result {
        switch exponent
        case 0 { result := 1 }
        case 1 { result := base }
        default {
            result := power(mul(base, base), div(exponent, 2))
            switch mod(exponent, 2)
                case 1 { result := mul(base, result) }
        }
    }
}
注意事项

内联汇编语言可能具有相当高级的外观,但实际上它是非常低级的编程语言。函数调用、循环、if 语句和 switch 语句通过简单的重写规则进行转换, 然后,汇编程序为你做的唯一事情就是重新组织函数风格操作码、管理 jump 标签、计算访问变量的栈高度,还有在到达语句块末尾时删除局部汇编变量的栈数据。 特别是对于最后两种情况,汇编程序仅会按照代码的顺序计算栈的高度,而不一定遵循控制流程;了解这一点非常重要。此外,swap 等操作只会交换栈内的数据,而不是变量位置。

Solidity 惯例

与 EVM 汇编语言相比,Solidity 能够识别小于 256 位的类型,例如 uint24。为了提高效率,大多数算术运算只将它们视为 256 位数字, 仅在必要时才清除未使用的数据位,即在将它们写入内存或执行比较之前才会这么做。这意味着,如果从内联汇编中访问这样的变量,你必须先手工清除那些未使用的数据位。

Solidity 以一种非常简单的方式管理内存:在 0x40 的位置有一个“空闲内存指针”。如果你打算分配内存,只需从此处开始使用内存,然后相应地更新指针即可。

内存的开头 64 字节可以用来作为临时分配的“暂存空间”。“空闲内存指针”之后的 32 字节位置(即从 0x60 开始的位置)将永远为 0,可以用来初始化空的动态内存数组。

在 Solidity 中,内存数组的元素总是占用 32 个字节的倍数(是的,甚至对于 byte[] 都是这样,只有 bytesstring 不是这样)。 多维内存数组就是指向内存数组的指针。动态数组的长度存储在数组的第一个槽中,其后才是数组元素。

警告

静态内存数组没有长度字段,但很快就会增加,这是为了可以更好地进行静态数组和动态数组之间的转换,所以请不要依赖这点。

独立汇编

以上内联汇编描述的汇编语言也可以单独使用,实际上,计划是将其用作 Solidity 编译器的中间语言。在这种意义下,它试图实现以下几个目标:

1、即使代码是由 Solidity 的编译器生成的,用它编写的程序应该也是可读的。 2、从汇编到字节码的翻译应该尽可能少地包含“意外”。 3、控制流应该易于检测,以帮助进行形式化验证和优化。

为了实现第一个和最后一个目标,汇编提供了高级结构:如 for 循环、if 语句、switch 语句和函数调用。 应该可以编写不使用明确的 SWAPDUPJUMPJUMPI 语句的汇编程序,因为前两个混淆了数据流,而最后两个混淆了控制流。 此外,形式为 mul(add(x, y), 7) 的函数风格语句优于如 7 y x add mul 的指令风格语句,因为在第一种形式中更容易查看哪个操作数用于哪个操作码。

第二个目标是通过采用一种非常规则的方式来将高级高级指令结构便以为字节码。 汇编程序执行的唯一非局部操作是用户自定义标识符(函数、变量、...)的名称查找,它遵循非常简单和固定的作用域规则并从栈中清除局部变量。

作用域:在其中声明的标识符(标签、变量、函数、汇编)仅在声明的语句块中可见(包括当前语句块中的嵌套语句块)。 即使它们在作用范围内,越过函数边界访问局部变量也是非法的。阴影化是禁止的。在声明之前不能访问局部变量,但标签、函数和汇编是可以的。 汇编是特殊的语句块,例如用于返回运行时代码或创建合约等。在子汇编外部的汇编语句块中声明的标示符在子汇编中全都不可见。

如果控制流经过块尾部,则会插入与在当前语句块中声明的局部变量数量相匹配的 pop 指令。无论何时引用局部变量,代码生成器都需要知道在当前栈的相对位置, 因此,需要跟踪当前所谓的栈高度。由于所有在语句块内声明的局部变量都会在语句块结束时被清楚,所以语句块前后的栈高度应该相同。如果情况并非如此,则会发出警告。

使用 switchfor 和函数应该可以编写复杂的代码,而无需手工调用 jumpjumpi。这将允许改进的形式化验证和优化更简单地分析控制流程。

此外,如果允许手动跳转,计算栈高度将会更加复杂。栈中所有局部变量的位置都需要明确知晓,否则在语句块结束时就无法自动获得局部变量的引用从而正确地清除它们。

例子:

我们将参考一个从 Solidity 到汇编指令的实例。考虑以下 Solidity 程序的运行时字节码:

pragma solidity ^0.4.16;

contract C {
  function f(uint x) public pure returns (uint y) {
    y = 1;
    for (uint i = 0; i < x; i++)
      y = 2 * y;
  }
}

将会生成如下汇编指令:

{
  mstore(0x40, 0x60) // 保存“空闲内存指针”
  // 函数选择器
  switch div(calldataload(0), exp(2, 226))
  case 0xb3de648b {
    let r := f(calldataload(4))
    let ret := $allocate(0x20)
    mstore(ret, r)
    return(ret, 0x20)
  }
  default { revert(0, 0) }
  // 内存分配器
  function $allocate(size) -> pos {
    pos := mload(0x40)
    mstore(0x40, add(pos, size))
  }
  // 合约函数
  function f(x) -> y {
    y := 1
    for { let i := 0 } lt(i, x) { i := add(i, 1) } {
      y := mul(2, y)
    }
  }
}
汇编语法

解析器任务如下:

  • 将字节流转换为符号流,丢弃 C ++ 风格的注释(对源代码引用存在特殊注释,我们这里不解释它)。
  • 根据下面的语法,将符号流转换为 AST。
  • 注册语句块中定义的标识符(注释到 AST 节点),并注明变量从哪个地方开始可以访问。

汇编词法分析器遵循由 Solidity 自己定义的规则。

空格用于分隔所有符号,它由空格字符、制表符和换行符组成。注释格式是常规的 JavaScript/C++ 风格,并被解释为空格。

Grammar:

AssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem =
    Identifier |
    AssemblyBlock |
    AssemblyExpression |
    AssemblyLocalDefinition |
    AssemblyAssignment |
    AssemblyStackAssignment |
    LabelDefinition |
    AssemblyIf |
    AssemblySwitch |
    AssemblyFunctionDefinition |
    AssemblyFor |
    'break' |
    'continue' |
    SubAssembly
AssemblyExpression = AssemblyCall | Identifier | AssemblyLiteral
AssemblyLiteral = NumberLiteral | StringLiteral | HexLiteral
Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
AssemblyCall = Identifier '(' ( AssemblyExpression ( ',' AssemblyExpression )* )? ')'
AssemblyLocalDefinition = 'let' IdentifierOrList ( ':=' AssemblyExpression )?
AssemblyAssignment = IdentifierOrList ':=' AssemblyExpression
IdentifierOrList = Identifier | '(' IdentifierList ')'
IdentifierList = Identifier ( ',' Identifier)*
AssemblyStackAssignment = '=:' Identifier
LabelDefinition = Identifier ':'
AssemblyIf = 'if' AssemblyExpression AssemblyBlock
AssemblySwitch = 'switch' AssemblyExpression AssemblyCase*
    ( 'default' AssemblyBlock )?
AssemblyCase = 'case' AssemblyExpression AssemblyBlock
AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
    ( '->' '(' IdentifierList ')' )? AssemblyBlock
AssemblyFor = 'for' ( AssemblyBlock | AssemblyExpression )
    AssemblyExpression ( AssemblyBlock | AssemblyExpression ) AssemblyBlock
SubAssembly = 'assembly' Identifier AssemblyBlock
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+

杂项

存储storage 中的状态变量储存结构

静态大小的变量(除 映射mapping 和动态数组之外的所有类型)都从位置 0 开始连续放置在 存储storage 中。如果可能的话,存储需求少于 32 字节的多个变量会被打包到一个 存储插槽storage slot 中,规则如下:

  • 存储插槽storage slot 的第一项会以低位对齐(即右对齐)的方式储存。
  • 基本类型仅使用存储它们所需的字节。
  • 如果 存储插槽storage slot 中的剩余空间不足以储存一个基本类型,那么它会被移入下一个 存储插槽storage slot
  • 结构(struct)和数组数据总是会占用一整个新插槽(但结构或数组中的各项,都会以这些规则进行打包)。

警告

使用小于 32 字节的元素时,你的合约的 gas 使用量可能高于使用 32 字节的元素时。这是因为 以太坊虚拟机Ethereum Virtual Machine(EVM) 每次会操作 32 个字节, 所以如果元素比 32 字节小,以太坊虚拟机Ethereum Virtual Machine(EVM) 必须使用更多的操作才能将其大小缩减到到所需的大小。

仅当你处理 存储插槽storage slot 中的值时候,使用缩减大小的参数才是有益的。因为编译器会将多个元素打包到一个 存储插槽storage slot 中, 从而将多个读或写合并到一次对存储的操作中。而在处理函数参数或 内存memory 中的值时,因为编译器不会打包这些值,所以没有什么益处。

最后,为了允许 以太坊虚拟机Ethereum Virtual Machine(EVM) 对此进行优化,请确保你对 存储storage 中的变量和 struct 成员的书写顺序允许它们被紧密地打包。 例如,按照 uint128,uint128,uint256 的顺序声明你的存储变量,而不是 uint128,uint256,uint128, 因为前者只占用两个 存储插槽storage slot,而后者将占用三个。

结构和数组中的元素都是顺序存储的,就像它们被明确给定的那样。

由于 映射mapping 和动态数组的大小是不可预知的,所以我们使用 Keccak-256 哈希计算来找到具体数值或数组数据的起始位置。 这些起始位置本身的数值总是会占满堆栈插槽。

映射mapping 或动态数组本身会根据上述规则来在某个位置 p 处占用一个(未填充的)存储中的插槽(或递归地将该规则应用到 映射mapping映射mapping 或数组的数组)。 对于动态数组,此插槽中会存储数组中元素的数量(字节数组和字符串在这里是一个例外,见下文)。对于 映射mapping ,该插槽未被使用(但它仍是需要的, 以使两个相同的 映射mapping 在彼此之后会使用不同的散列分布)。数组的数据会位于 keccak256(p)映射mapping 中的键 k 所对应的值会位于 keccak256(k . p), 其中 . 是连接符。如果该值又是一个非基本类型,则通过添加 keccak256(k . p) 作为偏移量来找到位置。

如果 bytesstring 的数据很短,那么它们的长度也会和数据一起存储到同一个插槽。具体地说:如果数据长度小于等于 31 字节, 则它存储在高位字节(左对齐),最低位字节存储 length * 2。如果数据长度超出 31 字节,则在主插槽存储 length * 2 + 1, 数据照常存储在 keccak256(slot) 中。

所以对于以下合约片段:

pragma solidity ^0.4.0;

contract C {
  struct s { uint a; uint b; }
  uint x;
  mapping(uint => mapping(uint => s)) data;
}

data[4][9].b 的位置将是 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1

内存memory 中的存储结构

Solidity 保留了 4 个 32 字节的插槽(slot):

  • 0x00 - 0x3f:用于保存方法(函数)哈希的临时空间
  • 0x40 - 0x5f:当前已分配的 内存memory 大小(又名,空闲 内存memory 指针)
  • 0x60 - 0x7f:0 值插槽

临时空间可以在语句之间使用(即在内联汇编之中)。0 值插槽则用来对动态内存数组进行初始化,且永远不会写入数据(因而可用的初始内存指针为 0x80)。

Solidity 总会把新对象保存在空闲 内存memory 指针的位置,所以这段内存实际上从来不会空闲(在未来可能会修改这个机制)。

警告

Solidity 中有一些操作需要大于 64 字节的临时内存区域,因此这种数据无法保存到临时空间里。它们将被放置在空闲内存指向的位置,但由于这种数据的生命周期较短,这个指针不会即时更新。这部分内存可能会被清零也可能不会。所以我们不应该期望这些所谓的空闲内存总会被清零。

尽管使用 msize 来到达非零内存区域是个好主意,然而非临时性地使用这样的指针,而不更新可用内存指针也会产生有害的结果。

调用数据存储结构

当从一个账户调用已部署的 Solidity 合约时,调用数据的格式被认为会遵循 ABI 说明。 根据 ABI 说明的规定,参数需要被整理为 32 字节的倍数。而内部函数调用会使用不同规则。

内部机制 - 清理变量

如果一个数值不足 256 位,那么在某些情况下,不足的位必须被清除。 Solidity 编译器设计用于在执行任何操作之前清除这些剩余位中可能会造成不利影响的潜在垃圾。 例如,因为 内存memory 中的内容可以用于计算散列或作为消息调用的数据发送,所以在向 内存memory 写入数值之前,需要清除剩余的位。 同样,在向 存储storage 中保存数据之前,剩余的位也需要清除,否则就会看到被混淆的数值。

另一方面,如果接下来的操作不会被影响,那我们就不用清除这些位的数据。例如,因为任何非零值都会被 JUMPI 指令视为 true, 所以在布尔数据用做 JUMPI 的条件之前,我们就不用清除它们。

除了以上设计原理之外,Solidity 编译器在把输入数据加载到堆栈时会对它们进行清除剩余位的处理。

不同的数据类型有不同的清除无效值的规则:

类型 合法数值 无效值会导致
n 个成员的 enum 0 到 n - 1 exception
bool 0 或 1 1
signed integers 以符号开头的 字(32字节) 目前会直接打包; 未来会抛出 exception
unsigned integers 高位补 0 目前会直接打包; 未来会抛出 exception

内部机制 - 优化器

Solidity 优化器是在汇编语言级别工作的,所以它可以并且也被其他语言所使用。它通过 JUMPJUMPDEST 语句将指令集序列分割为基础的代码块。在这些代码块内的指令集会被分析,并且对堆栈、内存或存储的每个修改都会被记录为表达式,这些表达式由一个指令和基本上是指向其他表达式的参数列表所组成。现在,主要的想法就是找到始终相等的表达式(在每个输入上)并将它们组合到一个表达式类中。优化器首先尝试在已知的表达式列表中查找每个新表达式。如果这不起作用,表达式会以 constant + constant = sum_of_constantsX * 1 = X 这样的规则进行简化。由于这是递归完成的,所以在我们知道第二个因子是一个更复杂的表达式,且此表达式总是等于 1 的情况下,也可以应用后一个规则。对存储和内存上某个具体位置的修改必须删除有关存储和内存位置的认知,这里边的区别并不为人所知:如果我们先在 x 位置写入,然后在 y 位置写入,且都是输入变量,则第二个可能会覆盖第一个,所以我们实际上并不知道在写入到 y 位置之后在 x 位置存储了什么。另一方面,如果对表达式 x - y 的简化,其结果为非零常数,那么我们知道我们可以保持关于 x 位置存储内容的认知。

在这个过程结束时,我们会知道最后哪些表达式必须在栈上,并且会得到一个修改内存和存储的列表。该信息与基本代码块一起存储并用来链接它们。此外,关于栈、存储和内存的配置信息会被转发到下一个代码块。如果我们知道所有 JUMPJUMPI 指令的目标,我们就可以构建一个完整的程序流程图。 如果只有一个我们不知道的目标(原则上可能发生,跳转目标可以基于输入来计算),我们必须消除关于代码块输入状态的所有信息,因为它可能是未知的 JUMP 目标。如果一个 JUMPI 的条件等于一个常量,它将被转换为无条件跳转。

作为最后一步,每个块中的代码都会被完全重新生成。然后会从代码块的结尾处在栈上的表达式开始创建依赖关系图,且不是该图组成部分的每个操作实质上都会被丢弃。现在,生成的代码将按照原始代码中的顺序对内存和存储进行修改(舍弃不需要的修改),最终,生成需要在栈中的当前位置保存的所有值。

这些步骤适用于每个基本代码块,如果代码块较小,则新生成的代码将用作替换。如果一个基本代码块在 JUMPI 处被分割,且在分析过程中被评估为一个常数,则会根据常量的值来替换 JUMPI,因此,类似于

var x = 7;
data[7] = 9;
if (data[x] != x + 2)
  return 2;
else
  return 1;

的代码也就被简化地编译为

data[7] = 9;
return 1;

即使原始代码中包含一个跳转。

源代码映射

作为 AST 输出的一部分,编译器提供 AST 中相应节点所代表的源代码范围。这可以用于多种用途,比如从用于报告错误的 AST 静态分析工具到可以突出显示局部变量及其用途的调试工具。

此外,编译器还可以生成从字节码到生成该指令的源代码范围的映射。对于在字节码级别上运行的静态分析工具以及在调试器中显示源代码中的当前位置或处理断点,这都是同样重要的。

这两种源映射都使用整数标识符来引用源文件。这些是通常称为 “sourceList” 的源文件列表的常规数组索引,它们是 combined-json 和 json / npm 编译器输出的一部分。

注解

在指令没有与任何特定的代码文件关联的情况下,源代码映射会将 -1 赋值给一个整数标识符。这会在字节码阶段发生,源于由编译器生成的内联汇编语句。

AST 内的源代码映射使用以下表示法:

s:l:f

其中,s 是源代码文件中范围起始处的字节偏移量,l 是源代码范围的长度(以字节为单位),f 是上述源代码索引。

针对字节码的源代码映射的编码方式更加复杂:它是由 ; 分隔的 s:l:f:j 列表。每个元素都对应一条指令,即不能使用字节偏移量,但必须使用指令偏移量(push 指令长于一个字节)。字段 slf 如上所述,j 可以是 io-,表示一个跳转指令是否进入一个函数、是否从一个函数返回或者是否是一个常规跳转的一部分,例如一个循环。

为了压缩这些源代码映射,特别是对字节码的映射,我们将使用以下规则:

  • 如果一个字段为空,则使用前一个元素中对应位置的值。
  • 如果缺少 :,则后续所有字段都被视为空。

这意味着以下的源代码映射是等价的:

1:2:1;1:9:1;2:1:2;2:1:2;2:1:2

1:2:1;:9;2:1:2;;

技巧和窍门

  • 可以使用 delete 来删除数组中的所有元素。
  • 对 struct 中的元素使用更短的数据类型,并对它们进行排序,以便将短数据类型组合在一起。这可以降低 gas 消耗,因为多个 SSTORE 操作可能会被合并成一个(SSTORE 消耗 5000 或 20000 的 gas,所以这应该是你想要优化的)。使用 gas 估算器(启用优化器)来检查!
  • 将你的状态变量设置为 public ——编译器会为你自动创建 getters
  • 如果你最终需要在函数开始位置检查很多输入条件或者状态变量的值,你可以尝试使用 函数
  • 如果你的合约有一个 send 函数,但你想要使用内置的 send 函数,你可以使用 address(contractVariable).send(amount)
  • 使用一个赋值语句就可以初始化 struct:x = MyStruct({a: 1, b: 2});

注解

如果存储结构具有“紧打包(tightly packed)”,可以用分开的赋值语句来初始化:x.a = 1; x.b = 2;。这样可以使优化器更容易地一次性更新存储,使赋值的开销更小。

速查表

操作符优先级

以下是按评估顺序列出的操作符优先级。

优先级 描述 操作符
1 后置自增和自减 ++, --
创建类型实例 new <typename>
数组元素 <array>[<index>]
访问成员 <object>.<member>
函数调用 <func>(<args...>)
小括号 (<statement>)
2 前置自增和自减 ++, --
一元运算的加和减 +, -
一元操作符 delete
逻辑非 !
按位非 ~
3 乘方 **
4 乘、除和模运算 *, /, %
5 算术加和减 +, -
6 移位操作符 <<, >>
7 按位与 &
8 按位异或 ^
9 按位或 |
10 非等操作符 <, >, <=, >=
11 等于操作符 ==, !=
12 逻辑与 &&
13 逻辑或 ||
14 三元操作符 <conditional> ? <if-true> : <if-false>
15 赋值操作符 =, |=, ^=, &=, <<=, >>=, +=, -=, *=, /=, %=
16 逗号 ,
全局变量
  • 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), ...)
  • block.blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块;而 blocks 从 0.4.22 版本开始已经不推荐使用,由 blockhash(uint blockNumber) 代替
  • block.coinbaseaddress):挖出当前区块的矿工的地址
  • block.difficultyuint):当前区块的难度值
  • block.gaslimituint):当前区块的 gas 上限
  • block.numberuint):当前区块的区块号
  • block.timestampuint):当前区块的时间戳
  • gasleft() returns (uint256):剩余的 gas
  • msg.databytes):完整的 calldata
  • msg.gasuint):剩余的 gas - 自 0.4.21 版本开始已经不推荐使用,由 gesleft() 代替
  • msg.senderaddress):消息发送方(当前调用)
  • msg.valueuint):随消息发送的 wei 的数量
  • nowuint):当前区块的时间戳(等价于 block.timestamp
  • tx.gaspriceuint):交易的 gas price
  • tx.originaddress):交易发送方(完整调用链上的原始发送方)
  • assert(bool condition):如果条件值为 false 则中止执行并回退所有状态变更(用做内部错误)
  • require(bool condition):如果条件值为 false 则中止执行并回退所有状态变更(用做异常输入或外部组件错误)
  • require(bool condition, string message):如果条件值为 false 则中止执行并回退所有状态变更(用做异常输入或外部组件错误),可以同时提供错误消息
  • revert():中止执行并回复所有状态变更
  • revert(string message):中止执行并回复所有状态变更,可以同时提供错误消息
  • blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希——仅可用于最新的 256 个区块
  • keccak256(...) returns (bytes32):计算 紧打包编码 的 Ethereum-SHA-3(Keccak-256)哈希
  • sha3(...) returns (bytes32):等价于 keccak256
  • sha256(...) returns (bytes32):计算 紧打包编码 的 SHA-256 哈希
  • ripemd160(...) returns (bytes20):计算 紧打包编码 的 RIPEMD-160 哈希
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):基于椭圆曲线签名找回与指定公钥关联的地址,发生错误的时候返回 0
  • addmod(uint x, uint y, uint k) returns (uint):计算 (x + y) % k 的值,其中加法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本开始会加入对 k != 0 的 assert(即会在此函数开头执行 assert(k != 0); 作为参数检查,译者注)。
  • mulmod(uint x, uint y, uint k) returns (uint):计算 (x * y) % k 的值,其中乘法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本开始会加入对 k != 0 的 assert(即会在此函数开头执行 assert(k != 0); 作为参数检查,译者注)。
  • this (类型为当前合约的变量):当前合约实例,可以准确地转换为 address
  • super:当前合约的上一级继承关系的合约
  • selfdestruct(address recipient):销毁当前合约,把余额发送到给定地址
  • suicide(address recipient):与 selfdestruct 等价,但已不推荐使用
  • <address>.balanceuint256): 地址类型 的余额,以 Wei 为单位
  • <address>.send(uint256 amount) returns (bool):向 地址类型 发送给定数量的 Wei,失败时返回 false
  • <address>.transfer(uint256 amount):向 地址类型 发送给定数量的 Wei,失败时会把错误抛出(throw)

注解

不要用 block.timestampnow 或者 blockhash 作为随机种子,除非你明确知道你在做什么。

时间戳和区块哈希都可以在一定程度上被矿工所影响。如果你用哈希值作为随机种子,那么例如挖矿团体中的坏人就可以使用给定的哈希来执行一个赌场功能,如果他们没赢钱,他们可以简单地换一个哈希再试。

当前区块的时间戳必须比前一个区块的时间戳大,但唯一可以确定的就是它会是权威链(主链或者主分支)上两个连续区块时间戳之间的一个数值。

注解

出于扩展性的原因,你无法取得所有区块的哈希。只有最新的 256 个区块的哈希可以拿到,其他的都将为 0。

函数可见性说明符
function myFunction() <visibility specifier> returns (bool) {
    return true;
}
  • public:内部、外部均可见(参考为存储/状态变量创建 getter 函数
  • private:仅在当前合约内可见
  • external:仅在外部可见(仅可修饰函数)——就是说,仅可用于消息调用(即使在合约内调用,也只能通过 this.func 的方式)
  • internal:仅在内部可见(也就是在当前 Solidity 源代码文件内均可见,不仅限于当前合约内,译者注)
修改器
  • pure 修饰函数时:不允许修改或访问状态——但目前并不是强制的。
  • view 修饰函数时:不允许修改状态——但目前不是强制的。
  • payable 修饰函数时:允许从调用中接收 以太币Ether
  • constant 修饰状态变量时:不允许赋值(除初始化以外),不会占据 存储插槽storage slot
  • constant 修饰函数时:与 view 等价。
  • anonymous 修饰事件时:不把事件签名作为 topic 存储。
  • indexed 修饰事件时:将参数作为 topic 存储。
保留字

以下是 Solidity 的保留字,未来可能会变为语法的一部分:

abstract, after, alias, apply, auto, case, catch, copyof, default, define, final, immutable, implements, in, inline, let, macro, match, mutable, null, of, override, partial, promise, reference, relocatable, sealed, sizeof, static, supports, switch, try, type, typedef, typeof, unchecked.

语法表
SourceUnit = (PragmaDirective | ImportDirective | ContractDefinition)*

// Pragma actually parses anything up to the trailing ';' to be fully forward-compatible.
PragmaDirective = 'pragma' Identifier ([^;]+) ';'

ImportDirective = 'import' StringLiteral ('as' Identifier)? ';'
        | 'import' ('*' | Identifier) ('as' Identifier)? 'from' StringLiteral ';'
        | 'import' '{' Identifier ('as' Identifier)? ( ',' Identifier ('as' Identifier)? )* '}' 'from' StringLiteral ';'

ContractDefinition = ( 'contract' | 'library' | 'interface' ) Identifier
                     ( 'is' InheritanceSpecifier (',' InheritanceSpecifier )* )?
                     '{' ContractPart* '}'

ContractPart = StateVariableDeclaration | UsingForDeclaration
             | StructDefinition | ModifierDefinition | FunctionDefinition | EventDefinition | EnumDefinition

InheritanceSpecifier = UserDefinedTypeName ( '(' Expression ( ',' Expression )* ')' )?

StateVariableDeclaration = TypeName ( 'public' | 'internal' | 'private' | 'constant' )* Identifier ('=' Expression)? ';'
UsingForDeclaration = 'using' Identifier 'for' ('*' | TypeName) ';'
StructDefinition = 'struct' Identifier '{'
                     ( VariableDeclaration ';' (VariableDeclaration ';')* ) '}'

ModifierDefinition = 'modifier' Identifier ParameterList? Block
ModifierInvocation = Identifier ( '(' ExpressionList? ')' )?

FunctionDefinition = 'function' Identifier? ParameterList
                     ( ModifierInvocation | StateMutability | 'external' | 'public' | 'internal' | 'private' )*
                     ( 'returns' ParameterList )? ( ';' | Block )
EventDefinition = 'event' Identifier EventParameterList 'anonymous'? ';'

EnumValue = Identifier
EnumDefinition = 'enum' Identifier '{' EnumValue? (',' EnumValue)* '}'

ParameterList = '(' ( Parameter (',' Parameter)* )? ')'
Parameter = TypeName StorageLocation? Identifier?

EventParameterList = '(' ( EventParameter (',' EventParameter )* )? ')'
EventParameter = TypeName 'indexed'? Identifier?

FunctionTypeParameterList = '(' ( FunctionTypeParameter (',' FunctionTypeParameter )* )? ')'
FunctionTypeParameter = TypeName StorageLocation?

// semantic restriction: mappings and structs (recursively) containing mappings
// are not allowed in argument lists
VariableDeclaration = TypeName StorageLocation? Identifier

TypeName = ElementaryTypeName
         | UserDefinedTypeName
         | Mapping
         | ArrayTypeName
         | FunctionTypeName

UserDefinedTypeName = Identifier ( '.' Identifier )*

Mapping = 'mapping' '(' ElementaryTypeName '=>' TypeName ')'
ArrayTypeName = TypeName '[' Expression? ']'
FunctionTypeName = 'function' FunctionTypeParameterList ( 'internal' | 'external' | StateMutability )*
                   ( 'returns' FunctionTypeParameterList )?
StorageLocation = 'memory' | 'storage' | 'calldata'
StateMutability = 'pure' | 'constant' | 'view' | 'payable'

Block = '{' Statement* '}'
Statement = IfStatement | WhileStatement | ForStatement | Block | InlineAssemblyStatement |
            ( DoWhileStatement | PlaceholderStatement | Continue | Break | Return |
              Throw | EmitStatement | SimpleStatement ) ';'

ExpressionStatement = Expression
IfStatement = 'if' '(' Expression ')' Statement ( 'else' Statement )?
WhileStatement = 'while' '(' Expression ')' Statement
PlaceholderStatement = '_'
SimpleStatement = VariableDefinition | ExpressionStatement
ForStatement = 'for' '(' (SimpleStatement)? ';' (Expression)? ';' (ExpressionStatement)? ')' Statement
InlineAssemblyStatement = 'assembly' StringLiteral? InlineAssemblyBlock
DoWhileStatement = 'do' Statement 'while' '(' Expression ')'
Continue = 'continue'
Break = 'break'
Return = 'return' Expression?
Throw = 'throw'
EmitStatement = 'emit' FunctionCall
VariableDefinition = ('var' IdentifierList | VariableDeclaration | '(' VariableDeclaration? (',' VariableDeclaration? )* ')' ) ( '=' Expression )?
IdentifierList = '(' ( Identifier? ',' )* Identifier? ')'

// Precedence by order (see github.com/ethereum/solidity/pull/732)
Expression
  = Expression ('++' | '--')
  | NewExpression
  | IndexAccess
  | MemberAccess
  | FunctionCall
  | '(' Expression ')'
  | ('!' | '~' | 'delete' | '++' | '--' | '+' | '-') Expression
  | Expression '**' Expression
  | Expression ('*' | '/' | '%') Expression
  | Expression ('+' | '-') Expression
  | Expression ('<<' | '>>') Expression
  | Expression '&' Expression
  | Expression '^' Expression
  | Expression '|' Expression
  | Expression ('<' | '>' | '<=' | '>=') Expression
  | Expression ('==' | '!=') Expression
  | Expression '&&' Expression
  | Expression '||' Expression
  | Expression '?' Expression ':' Expression
  | Expression ('=' | '|=' | '^=' | '&=' | '<<=' | '>>=' | '+=' | '-=' | '*=' | '/=' | '%=') Expression
  | PrimaryExpression

PrimaryExpression = BooleanLiteral
                  | NumberLiteral
                  | HexLiteral
                  | StringLiteral
                  | TupleExpression
                  | Identifier
                  | ElementaryTypeNameExpression

ExpressionList = Expression ( ',' Expression )*
NameValueList = Identifier ':' Expression ( ',' Identifier ':' Expression )*

FunctionCall = Expression '(' FunctionCallArguments ')'
FunctionCallArguments = '{' NameValueList? '}'
                      | ExpressionList?

NewExpression = 'new' TypeName
MemberAccess = Expression '.' Identifier
IndexAccess = Expression '[' Expression? ']'

BooleanLiteral = 'true' | 'false'
NumberLiteral = ( HexNumber | DecimalNumber ) (' ' NumberUnit)?
NumberUnit = 'wei' | 'szabo' | 'finney' | 'ether'
           | 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'years'
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9]*

HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+ ( '.' [0-9]* )? ( [eE] [0-9]+ )?

TupleExpression = '(' ( Expression? ( ',' Expression? )*  )? ')'
                | '[' ( Expression  ( ',' Expression  )*  )? ']'

ElementaryTypeNameExpression = ElementaryTypeName

ElementaryTypeName = 'address' | 'bool' | 'string' | 'var'
                   | Int | Uint | Byte | Fixed | Ufixed

Int = 'int' | 'int8' | 'int16' | 'int24' | 'int32' | 'int40' | 'int48' | 'int56' | 'int64' | 'int72' | 'int80' | 'int88' | 'int96' | 'int104' | 'int112' | 'int120' | 'int128' | 'int136' | 'int144' | 'int152' | 'int160' | 'int168' | 'int176' | 'int184' | 'int192' | 'int200' | 'int208' | 'int216' | 'int224' | 'int232' | 'int240' | 'int248' | 'int256'

Uint = 'uint' | 'uint8' | 'uint16' | 'uint24' | 'uint32' | 'uint40' | 'uint48' | 'uint56' | 'uint64' | 'uint72' | 'uint80' | 'uint88' | 'uint96' | 'uint104' | 'uint112' | 'uint120' | 'uint128' | 'uint136' | 'uint144' | 'uint152' | 'uint160' | 'uint168' | 'uint176' | 'uint184' | 'uint192' | 'uint200' | 'uint208' | 'uint216' | 'uint224' | 'uint232' | 'uint240' | 'uint248' | 'uint256'

Byte = 'byte' | 'bytes' | 'bytes1' | 'bytes2' | 'bytes3' | 'bytes4' | 'bytes5' | 'bytes6' | 'bytes7' | 'bytes8' | 'bytes9' | 'bytes10' | 'bytes11' | 'bytes12' | 'bytes13' | 'bytes14' | 'bytes15' | 'bytes16' | 'bytes17' | 'bytes18' | 'bytes19' | 'bytes20' | 'bytes21' | 'bytes22' | 'bytes23' | 'bytes24' | 'bytes25' | 'bytes26' | 'bytes27' | 'bytes28' | 'bytes29' | 'bytes30' | 'bytes31' | 'bytes32'

Fixed = 'fixed' | ( 'fixed' [0-9]+ 'x' [0-9]+ )

Ufixed = 'ufixed' | ( 'ufixed' [0-9]+ 'x' [0-9]+ )

InlineAssemblyBlock = '{' AssemblyItem* '}'

AssemblyItem = Identifier | FunctionalAssemblyExpression | InlineAssemblyBlock | AssemblyLocalBinding | AssemblyAssignment | AssemblyLabel | NumberLiteral | StringLiteral | HexLiteral
AssemblyLocalBinding = 'let' Identifier ':=' FunctionalAssemblyExpression
AssemblyAssignment = ( Identifier ':=' FunctionalAssemblyExpression ) | ( '=:' Identifier )
AssemblyLabel = Identifier ':'
FunctionalAssemblyExpression = Identifier '(' AssemblyItem? ( ',' AssemblyItem )* ')'

安全考量

尽管在通常情况下编写一个按照预期运行的软件很简单, 但想要确保没有人能够以出乎意料的方式使用它就困难多了。

在 Solidity 中,这一点尤为重要,因为智能合约可以用来处理通证,甚至有可能是更有价值的东西。 除此之外,智能合约的每一次执行都是公开的,而且源代码也通常是容易获得的。

当然,你总是需要考虑有多大的风险: 你可以将智能合约与公开的(当然也对恶意用户开放)、甚至是开源的网络服务相比较。 如果你只是在某个网络服务上存储你的购物清单,则可能不必太在意, 但如果你使用那个网络服务管理你的银行帐户, 那就需要特别当心了。

本节将列出一些陷阱和一般性的安全建议,但这绝对不全面。 另外,请时刻注意的是即使你的智能合约代码没有 bug, 但编译器或者平台本身可能存在 bug。 一个已知的编译器安全相关的 bug 列表可以在 已知bug列表 找到, 这个列表也可以用程序读取。 请注意其中有一个涵盖了 Solidity 编译器的代码生成器的 bug 悬赏项目。

我们的文档是开源的,请一如既往地帮助我们扩展这一节的内容(何况其中一些例子并不会造成损失)!

陷阱

私有信息和随机性

在智能合约中你所用的一切都是公开可见的,即便是局部变量和被标记成 private 的状态变量也是如此。

如果不想让矿工作弊的话,在智能合约中使用随机数会很棘手 (译者注:在智能合约中使用随机数很难保证节点不作弊, 这是因为智能合约中的随机数一般要依赖计算节点的本地时间得到, 而本地时间是可以被恶意节点伪造的,因此这种方法并不安全。 通行的做法是采用 链外off-chain 的第三方服务,比如 Oraclize 来获取随机数)。

重入

任何从合约 A 到合约 B 的交互以及任何从合约 A 到合约 B 的 以太币Ether 的转移,都会将控制权交给合约 B。 这使得合约 B 能够在交互结束前回调 A 中的代码。 举个例子,下面的代码中有一个 bug(这只是一个代码段,不是完整的合约):

pragma solidity ^0.4.0;

// 不要使用这个合约,其中包含一个 bug。
contract Fund {
    /// 合约中 |ether| 分成的映射。
    mapping(address => uint) shares;
    /// 提取你的分成。
    function withdraw() public {
        if (msg.sender.send(shares[msg.sender]))
            shares[msg.sender] = 0;
    }
}

这里的问题不是很严重,因为有限的 gas 也作为 send 的一部分,但仍然暴露了一个缺陷: 以太币Ether 的传输过程中总是可以包含代码执行,所以接收者可以是一个回调进入 withdraw 的合约。 这就会使其多次得到退款,从而将合约中的全部 以太币Ether 提取。 特别地,下面的合约将允许一个攻击者多次得到退款,因为它使用了 call ,默认发送所有剩余的 gas。

pragma solidity ^0.4.0;

// 不要使用这个合约,其中包含一个 bug。
contract Fund {
    /// 合约中 |ether| 分成的映射。
    mapping(address => uint) shares;
    /// 提取你的分成。
    function withdraw() public {
        if (msg.sender.call.value(shares[msg.sender])())
            shares[msg.sender] = 0;
    }
}

为了避免重入,你可以使用下面撰写的“检查-生效-交互”(Checks-Effects-Interactions)模式:

pragma solidity ^0.4.11;

contract Fund {
    /// 合约中 |ether| 分成的映射。
    mapping(address => uint) shares;
    /// 提取你的分成。
    function withdraw() public {
        var share = shares[msg.sender];
        shares[msg.sender] = 0;
        msg.sender.transfer(share);
    }
}

请注意重入不仅是 以太币Ether 传输的其中一个影响,还包括任何对另一个合约的函数调用。 更进一步说,你也不得不考虑多合约的情况。 一个被调用的合约可以修改你所依赖的另一个合约的状态。

gas 限制和循环

必须谨慎使用没有固定迭代次数的循环,例如依赖于 存储storage 值的循环: 由于区块 gas 有限,交易只能消耗一定数量的 gas。 无论是明确指出的还是正常运行过程中的,循环中的数次迭代操作所消耗的 gas 都有可能超出区块的 gas 限制,从而导致整个合约在某个时刻骤然停止。 这可能不适用于只被用来从区块链中读取数据的 view 函数。 尽管如此,这些函数仍然可能会被其它合约当作 链上on-chain 操作的一部分来调用,并使那些操作骤然停止。 请在合约代码的说明文档中明确说明这些情况。

发送和接收 以太币Ether

  • 目前无论是合约还是“外部账户”都不能阻止有人给它们发送 以太币Ether。 合约可以对一个正常的转账做出反应并拒绝它,但还有些方法可以不通过创建消息来发送 以太币Ether。 其中一种方法就是单纯地向合约地址“挖矿”,另一种方法就是使用 selfdestruct(x)
  • 如果一个合约收到了 以太币Ether (且没有函数被调用),就会执行 fallback 函数。 如果没有 fallback 函数,那么 以太币Ether 会被拒收(同时会抛出异常)。 在 fallback 函数执行过程中,合约只能依靠此时可用的“gas 津贴”(2300 gas)来执行。 这笔津贴并不足以用来完成任何方式的 存储storage 访问。 为了确保你的合约可以通过这种方式收到 以太币Ether,请你核对 fallback 函数所需的 gas 数量 (在 Remix 的“详细”章节会举例说明)。
  • 有一种方法可以通过使用 addr.call.value(x)() 向接收合约发送更多的 gas。 这本质上跟 addr.transfer(x) 是一样的, 只不过前者发送所有剩余的 gas,并且使得接收者有能力执行更加昂贵的操作 (它只会返回一个错误代码,而且也不会自动传播这个错误)。 这可能包括回调发送合约或者你想不到的其它状态改变的情况。 因此这种方法无论是给诚实用户还是恶意行为者都提供了极大的灵活性。
  • 如果你想要使用 address.transfer 发送 以太币Ether ,你需要注意以下几个细节:
    1. 如果接收者是一个合约,它会执行自己的 fallback 函数,从而可以回调发送 以太币Ether 的合约。
    2. 如果调用的深度超过 1024,发送 以太币Ether 也会失败。由于调用者对调用深度有完全的控制权,他们可以强制使这次发送失败; 请考虑这种可能性,或者使用 send 并且确保每次都核对它的返回值。 更好的方法是使用一种接收者可以取回 以太币Ether 的方式编写你的合约。
    3. 发送 以太币Ether 也可能因为接收方合约的执行所需的 gas 多于分配的 gas 数量而失败 (确切地说,是使用了 requireassertrevertthrow 或者因为这个操作过于昂贵) - “gas 不够用了”。 如果你使用 transfer 或者 send 的同时带有返回值检查,这就为接收者提供了在发送合约中阻断进程的方法。 再次说明,最佳实践是使用 “取回”模式而不是“发送”模式

调用栈深度

外部函数调用随时会失败,因为它们超过了调用栈的上限 1024。 在这种情况下,Solidity 会抛出一个异常。 恶意行为者也许能够在与你的合约交互之前强制将调用栈设置成一个比较高的值。

请注意,使用 .send() 时如果超出调用栈 并不会 抛出异常,而是会返回 false。 低级的函数比如 .call().callcode().delegatecall() 也都是这样的。

tx.origin

永远不要使用 tx.origin 做身份认证。假设你有一个如下的钱包合约:

pragma solidity ^0.4.11;

// 不要使用这个合约,其中包含一个 bug。
contract TxUserWallet {
    address owner;

    function TxUserWallet() public {
        owner = msg.sender;
    }

    function transferTo(address dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

现在有人欺骗你,将 以太币Ether 发送到了这个恶意钱包的地址:

pragma solidity ^0.4.11;

interface TxUserWallet {
    function transferTo(address dest, uint amount) public;
}

contract TxAttackWallet {
    address owner;

    function TxAttackWallet() public {
        owner = msg.sender;
    }

    function() public {
        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}

如果你的钱包通过核查 msg.sender 来验证发送方身份,你就会得到恶意钱包的地址,而不是所有者的地址。 但是通过核查 tx.origin ,得到的就会是启动交易的原始地址,它仍然会是所有者的地址。 恶意钱包会立即将你的资金抽出。

细枝末节

  • for (var i = 0; i < arrayName.length; i++) { ... } 中, i 的类型会变为 uint8 , 因为这是保存 0 值所需的最小类型。如果数组超过 255 个元素,则循环不会终止。
  • 不占用完整 32 字节的类型可能包含“脏高位”。这在当你访问 msg.data 的时候尤为重要 —— 它带来了延展性风险: 你既可以用原始字节 0xff000001 也可以用 0x00000001 作为参数来调用函数 f(uint8 x) 以构造交易。 这两个参数都会被正常提供给合约,并且 x 的值看起来都像是数字 1, 但 msg.data 会不一样,所以如果你无论怎么使用 keccak256(msg.data),你都会得到不同的结果。

推荐做法

认真对待警告

如果编译器警告了你什么事,你最好修改一下,即使你不认为这个特定的警告不会产生安全隐患,因为那也有可能埋藏着其他的问题。 我们给出的任何编译器警告,都可以通过轻微的修改来去掉。

同时也请尽早添加 pragma experimental "v0.5.0"; 来允许 0.5.0 版本的安全特性。 注意在这种情况下,experimental 并不意味着任何有风险的安全特性, 它只是可以允许一些在当前版本还不支持的 Solidity 特性,来提供向后的兼容。

限定 以太币Ether 的数量

限定 存储storage 在一个智能合约中 以太币Ether (或者其它通证)的数量。 如果你的源代码、编译器或者平台出现了 bug,可能会导致这些资产丢失。 如果你想控制你的损失,就要限定 以太币Ether 的数量。

保持合约简练且模块化

保持你的合约短小精炼且易于理解。 找出无关于其它合约或库的功能。 有关源码质量可以采用的一般建议: 限制局部变量的数量以及函数的长度等等。 将实现的函数文档化,这样别人看到代码的时候就可以理解你的意图,并判断代码是否按照正确的意图实现。

使用“检查-生效-交互”(Checks-Effects-Interactions)模式

大多数函数会首先做一些检查工作(例如谁调用了函数,参数是否在取值范围之内,它们是否发送了足够的 以太币Ether ,用户是否具有通证等等)。 这些检查工作应该首先被完成。

第二步,如果所有检查都通过了,应该接着进行会影响当前合约状态变量的那些处理。 与其它合约的交互应该是任何函数的最后一步。

早期合约延迟了一些效果的产生,为了等待外部函数调用以非错误状态返回。 由于上文所述的重入问题,这通常会导致严重的后果。

请注意,对已知合约的调用反过来也可能导致对未知合约的调用,所以最好是一直保持使用这个模式编写代码。

包含故障-安全(Fail-Safe)模式

尽管将系统完全去中心化可以省去许多中间环节,但包含某种故障-安全模式仍然是好的做法,尤其是对于新的代码来说:

你可以在你的智能合约中增加一个函数实现某种程度上的自检查,比如“ 以太币Ether 是否会泄露?”, “通证的总和是否与合约的余额相等?”等等。 请记住,你不能使用太多的 gas,所以可能需要通过 链外off-chain 计算来辅助。

如果自检查没有通过,合约就会自动切换到某种“故障安全”模式, 例如,关闭大部分功能,将控制权交给某个固定的可信第三方,或者将合约转换成一个简单的“退回我的钱”合约。

形式化验证

使用形式化验证可以执行自动化的数学证明,保证源代码符合特定的正式规范。 规范仍然是正式的(就像源代码一样),但通常要简单得多。

请注意形式化验证本身只能帮助你理解你做的(规范)和你怎么做(实际的实现)的之间的差别。 你仍然需要检查这个规范是否是想要的,而且没有漏掉由它产生的任何非计划内的效果。

使用编译器

使用命令行编译器

注解

这一节并不适用于 solcjs

solc 是 Solidity 源码库的构建目标之一,它是 Solidity 的命令行编译器。你可使用 solc --help 命令来查看它的所有选项的解释。该编译器可以生成各种输出,范围从简单的二进制文件、汇编文件到用于估计“gas”使用情况的抽象语法树(解析树)。如果你只想编译一个文件,你可以运行 solc --bin sourceFile.sol 来生成二进制文件。如果你想通过 solc 获得一些更高级的输出信息,可以通过 solc -o outputDirectory --bin --ast --asm sourceFile.sol 命令将所有的输出都保存到一个单独的文件夹中。

Before you deploy your contract, activate the optimizer while compiling using solc --optimize --bin sourceFile.sol. By default, the optimizer will optimize the contract for 200 runs. If you want to optimize for initial contract deployment and get the smallest output, set it to --runs=1. If you expect many transactions and don't care for higher deployment cost and output size, set --runs to a high number.

命令行编译器会自动从文件系统中读取并导入的文件,但同时,它也支持通过 prefix=path 选项将路径重定向。比如:

solc github.com/ethereum/dapp-bin/=/usr/local/lib/dapp-bin/ =/usr/local/lib/fallback file.sol

这实质上是告诉编译器去搜索 /usr/local/lib/dapp-bin 目录下的所有以 github.com/ethereum/dapp-bin/ 开头的文件,如果编译器找不到这样的文件,它会接着读取 /usr/local/lib/fallback 目录下的所有文件(空前缀意味着始终匹配)。solc 不会从位于重定向目标之外和显式指定的源文件所在目录之外的文件系统读取文件,所以,类似 import "/etc/passwd"; 这样的语句,编译器只会在你添加了 =/ 选项之后,才会尝试到根目录下加载 /etc/passwd 文件。

如果重定向路径下存在多个匹配,则选择具有最长公共前缀的那个匹配。

出于安全原因,编译器限制了它可以访问的目录。在命令行中指定的源文件的路径(及其子目录)和通过重定向定义的路径可用于 import 语句,其他的则会被拒绝。额外路径(及其子目录)可以通过 --allow-paths /sample/path,/another/sample/path 进行配置。

如果您的合约使用 libraries ,您会注意到在编译后的十六进制字节码中会包含形如 __LibraryName____ 的字符串。当您将 solc 作为链接器使用时,它会在下列情况中为你插入库的地址:要么在命令行中添加 --libraries "Math:0x12345678901234567890 Heap:0xabcdef0123456" 来为每个库提供地址,或者将这些字符串保存到一个文件中(每行一个库),并使用 --libraries fileName 参数。

如果在调用 solc 命令时使用了 --link 选项,则所有的输入文件会被解析为上面提到过的 __LibraryName____ 格式的未链接的二进制数据(十六进制编码),并且就地链接。(如果输入是从stdin读取的,则生成的数据会被写入stdout)。在这种情况下,除了 --libraries 外的其他选项(包括 -o )都会被忽略。

如果在调用 solc 命令时使用了 --standard-json 选项,它将会按JSON格式解析标准输入上的输入,并在标准输出上返回JSON格式的输出。

编译器输入输出JSON描述

下面展示的这些JSON格式是编译器API使用的,当然,在 solc 上也是可用的。有些字段是可选的(参见注释),并且它们可能会发生变化,但所有的变化都应该是后向兼容的。

编译器API需要JSON格式的输入,并以JSON格式输出编译结果。

注释是不允许的,这里仅用于解释目的。

输入说明

{
  // 必选: 源代码语言,比如“Solidity”,“serpent”,“lll”,“assembly”等
  language: "Solidity",
  // 必选
  sources:
  {
    // 这里的键值是源文件的“全局”名称,可以通过remappings引入其他文件(参考下文)
    "myFile.sol":
    {
      // 可选: 源文件的kaccak256哈希值,可用于校验通过URL加载的内容。
      "keccak256": "0x123...",
      // 必选(除非声明了 "content" 字段): 指向源文件的URL。
      // URL(s) 会按顺序加载,并且结果会通过keccak256哈希值进行检查(如果有keccak256的话)
      // 如果哈希值不匹配,或者没有URL返回成功,则抛出一个异常。
      "urls":
      [
        "bzzr://56ab...",
        "ipfs://Qma...",
        "file:///tmp/path/to/file.sol"
      ]
    },
    "mortal":
    {
      // 可选: 该文件的keccak256哈希值
      "keccak256": "0x234...",
      // 必选(除非声明了 "urls" 字段): 源文件的字面内容
      "content": "contract mortal is owned { function kill() { if (msg.sender == owner) selfdestruct(owner); } }"
    }
  },
  // 可选
  settings:
  {
    // 可选: 重定向参数的排序列表
    remappings: [ ":g/dir" ],
    // 可选: 优化器配置
    optimizer: {
      // 默认为 disabled
      enabled: true,
      // 基于你希望运行多少次代码来进行优化。
      // 较小的值可以使初始部署的费用得到更多优化,较大的值可以使高频率的使用得到优化。
      runs: 200
    },
    // 指定需编译的EVM的版本。会影响代码的生成和类型检查。可用的版本为:homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople
    evmVersion: "byzantium",
    // 可选: 元数据配置
    metadata: {
      // 只可使用字面内容,不可用URLs (默认设为 false)
      useLiteralContent: true
    },
    // 库的地址。如果这里没有把所有需要的库都给出,会导致生成输出数据不同的未链接对象
    libraries: {
      // 最外层的 key 是使用这些库的源文件的名字。
      // 如果使用了重定向, 在重定向之后,这些源文件应该能匹配全局路径
      // 如果源文件的名字为空,则所有的库为全局引用
      "myFile.sol": {
        "MyLib": "0x123123..."
      }
    }
    // 以下内容可以用于选择所需的输出。
    // 如果这个字段被忽略,那么编译器会加载并进行类型检查,但除了错误之外不会产生任何输出。
    // 第一级的key是文件名,第二级是合约名称,如果合约名为空,则针对文件本身(进行输出)。
    // 若使用通配符*,则表示所有合约。
    //
    // 可用的输出类型如下所示:
    //   abi - ABI
    //   ast - 所有源文件的AST
    //   legacyAST - 所有源文件的legacy AST
    //   devdoc - 开发者文档(natspec)
    //   userdoc - 用户文档(natspec)
    //   metadata - 元数据
    //   ir - 去除语法糖(desugaring)之前的新汇编格式
    //   evm.assembly - 去除语法糖(desugaring)之后的新汇编格式
    //   evm.legacyAssembly - JSON的旧样式汇编格式
    //   evm.bytecode.object - 字节码对象
    //   evm.bytecode.opcodes - 操作码列表
    //   evm.bytecode.sourceMap - 源码映射(用于调试)
    //   evm.bytecode.linkReferences - 链接引用(如果是未链接的对象)
    //   evm.deployedBytecode* - 部署的字节码(与evm.bytecode具有相同的选项)
    //   evm.methodIdentifiers - 函数哈希值列表
    //   evm.gasEstimates - 函数的gas预估量
    //   ewasm.wast - eWASM S-expressions 格式(不支持atm)
    //   ewasm.wasm - eWASM二进制格式(不支持atm)
    //
    // 请注意,如果使用 `evm` ,`evm.bytecode` ,`ewasm` 等选项,会选择其所有的子项作为输出。 另外,`*`可以用作通配符来请求所有内容。
    //
    outputSelection: {
      // 为每个合约生成元数据和字节码输出。
      "*": {
        "*": [ "metadata","evm.bytecode" ]
      },
      // 启用“def”文件中定义的“MyContract”合约的abi和opcodes输出。
      "def": {
        "MyContract": [ "abi","evm.bytecode.opcodes" ]
      },
      // 为每个合约生成源码映射输出
      "*": {
        "*": [ "evm.bytecode.sourceMap" ]
      },
      // 每个文件生成legacy AST输出
      "*": {
        "": [ "legacyAST" ]
      }
    }
  }
}

输出说明

{
  // 可选:如果没有遇到错误/警告,则不出现
  errors: [
    {
      // 可选:源文件中的位置
      sourceLocation: {
        file: "sourceFile.sol",
        start: 0,
        end: 100
      ],
      // 强制: 错误类型,例如 “TypeError”, “InternalCompilerError”, “Exception”等.
      // 可在文末查看完整的错误类型列表
      type: "TypeError",
      // 强制: 发生错误的组件,例如“general”,“ewasm”等
      component: "general",
      // 强制:错误的严重级别(“error”或“warning”)
      severity: "error",
      // 强制
      message: "Invalid keyword"
      // 可选: 带错误源位置的格式化消息
      formattedMessage: "sourceFile.sol:100: Invalid keyword"
    }
  ],
  // 这里包含了文件级别的输出。可以通过outputSelection来设置限制/过滤。
  sources: {
    "sourceFile.sol": {
      // 标识符(用于源码映射)
      id: 1,
      // AST对象
      ast: {},
      // legacy AST 对象
      legacyAST: {}
    }
  },
  // 这里包含了合约级别的输出。 可以通过outputSelection来设置限制/过滤。
  contracts: {
    "sourceFile.sol": {
      // 如果使用的语言没有合约名称,则该字段应该留空。
      "ContractName": {
        // 以太坊合约的应用二进制接口(ABI)。如果为空,则表示为空数组。
        // 请参阅 https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
        abi: [],
        // 请参阅元数据输出文档(序列化的JSON字符串)
        metadata: "{...}",
        // 用户文档(natspec)
        userdoc: {},
        // 开发人员文档(natspec)
        devdoc: {},
        // 中间表示形式 (string)
        ir: "",
        // EVM相关输出
        evm: {
          // 汇编 (string)
          assembly: "",
          // 旧风格的汇编 (object)
          legacyAssembly: {},
          // 字节码和相关细节
          bytecode: {
            // 十六进制字符串的字节码
            object: "00fe",
            // 操作码列表 (string)
            opcodes: "",
            // 源码映射的字符串。 请参阅源码映射的定义
            sourceMap: "",
            // 如果这里给出了信息,则表示这是一个未链接的对象
            linkReferences: {
              "libraryFile.sol": {
                // 字节码中的字节偏移;链接时,从指定的位置替换20个字节
                "Library1": [
                  { start: 0,length: 20 },
                  { start: 200,length: 20 }
                ]
              }
            }
          },
          // 与上面相同的布局
          deployedBytecode: { },
          // 函数哈希的列表
          methodIdentifiers: {
            "delegate(address)": "5c19a95c"
          },
          // 函数的gas预估量
          gasEstimates: {
            creation: {
              codeDepositCost: "420000",
              executionCost: "infinite",
              totalCost: "infinite"
            },
            external: {
              "delegate(address)": "25000"
            },
            internal: {
              "heavyLifting()": "infinite"
            }
          }
        },
        // eWASM相关的输出
        ewasm: {
          // S-expressions格式
          wast: "",
          // 二进制格式(十六进制字符串)
          wasm: ""
        }
      }
    }
  }
}
错误类型
  1. JSONError: JSON输入不符合所需格式,例如,输入不是JSON对象,不支持的语言等。
  2. IOError: IO和导入处理错误,例如,在提供的源里包含无法解析的URL或哈希值不匹配。
  3. ParserError: 源代码不符合语言规则。
  4. DocstringParsingError: 注释块中的NatSpec标签无法解析。
  5. SyntaxError: 语法错误,例如 continuefor 循环外部使用。
  6. DeclarationError: 无效的,无法解析的或冲突的标识符名称 比如 Identifier not found
  7. TypeError: 类型系统内的错误,例如无效类型转换,无效赋值等。
  8. UnimplementedFeatureError: 编译器当前不支持该功能,但预计将在未来的版本中支持。
  9. InternalCompilerError: 在编译器中触发的内部错误——应将此报告为一个issue。
  10. Exception: 编译期间的未知失败——应将此报告为一个issue。
  11. CompilerError: 编译器堆栈的无效使用——应将此报告为一个issue。
  12. FatalError: 未正确处理致命错误——应将此报告为一个issue。
  13. Warning: 警告,不会停止编译,但应尽可能处理。

合约的元数据

Solidity编译器自动生成JSON文件,即合约的元数据,其中包含了当前合约的相关信息。 它可以用于查询编译器版本,所使用的源代码,应用二进制接口Application Binary Interface(ABI)以太坊标准说明格式Ethereum Nature Specification Format(natspec) 文档,以便更安全地与合约进行交互并验证其源代码。

编译器会将元数据文件的 Swarm 哈希值附加到每个合约的字节码末尾(详情请参阅下文), 以便你可以以认证的方式获取该文件,而不必求助于中心化的数据提供者。

当然,你必须将元数据文件发布到 Swarm (或其他服务),以便其他人可以访问它。 该文件可以通过使用 solc --metadata 来生成,并被命名为 ContractName_meta.json 。 它将包含源代码的在 Swarm 上的引用,因此你必须上传所有源文件和元数据文件。

元数据文件具有以下格式。 下面的例子将以人类可读的方式呈现。 正确格式化的元数据应正确使用引号,将空白减少到最小,并对所有对象的键值进行排序以得到唯一的格式。 代码注释当然也是不允许的,这里仅用于解释目的。

{
  // 必选:元数据格式的版本
  version: "1",
  // 必选:源代码的编程语言,一般会选择规范的“子版本”
  language: "Solidity",
  // 必选:编译器的细节,内容视语言而定。
  compiler: {
    // 对 Solidity 来说是必须的:编译器的版本
    version: "0.4.6+commit.2dabbdf0.Emscripten.clang",
    // 可选: 生成此输出的编译器二进制文件的哈希值
    keccak256: "0x123..."
  },
  // 必选:编译的源文件/源单位,键值为文件名
  sources:
  {
    "myFile.sol": {
      // 必选:源文件的 keccak256 哈希值
      "keccak256": "0x123...",
      // 必选(除非定义了“content”,详见下文):
      // 已排序的源文件的URL,URL的协议可以是任意的,但建议使用 Swarm 的URL
      "urls": [ "bzzr://56ab..." ]
    },
    "mortal": {
      // 必选:源文件的 keccak256 哈希值
      "keccak256": "0x234...",
      // 必选(除非定义了“urls”): 源文件的字面内容
      "content": "contract mortal is owned { function kill() { if (msg.sender == owner) selfdestruct(owner); } }"
    }
  },
  // 必选:编译器的设置
  settings:
  {
    // 对 Solidity 来说是必须的: 已排序的重定向列表
    remappings: [ ":g/dir" ],
    // 可选: 优化器的设置( enabled 默认设为 false )
    optimizer: {
      enabled: true,
      runs: 500
    },
    // 对 Solidity 来说是必须的:用以生成该元数据的文件名和合约名或库名
    compilationTarget: {
      "myFile.sol": "MyContract"
    },
    // 对 Solidity 来说是必须的:所使用的库的地址
    libraries: {
      "MyLib": "0x123123..."
    }
  },
  // 必选:合约的生成信息
  output:
  {
    // 必选:合约的 ABI 定义
    abi: [ ... ],
    // 必选:合约的 NatSpec 用户文档
    userdoc: [ ... ],
    // 必选:合约的 NatSpec 开发者文档
    devdoc: [ ... ],
  }
}

注解

需注意,上面的 ABI 没有固定的顺序,随编译器的版本而不同。

注解

由于生成的合约的字节码包含元数据的哈希值,因此对元数据的任何更改都会导致字节码的更改。 此外,由于元数据包含所有使用的源代码的哈希值,所以任何源代码中的, 哪怕是一个空格的变化都将导致不同的元数据,并随后产生不同的字节代码。

元数据哈希字节码的编码

由于在将来我们可能会支持其他方式来获取元数据文件, 类似 {"bzzr0":<Swarm hash>} 的键值对,将会以 CBOR 编码来存储。 由于这种编码的起始位不容易找到,因此添加两个字节来表述其长度,以大端方式编码。 所以,当前版本的Solidity编译器,将以下内容添加到部署的字节码的末尾:

0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29

因此,为了获取数据,可以检查部署的字节码的末尾以匹配该模式,并使用 Swarm 哈希来获取元数据文件。

自动化接口生成和 以太坊标准说明格式Ethereum Nature Specification Format(natspec) 的使用方法

元数据以下列方式被使用:想要与合约交互的组件(例如,Mist)读取合约的字节码, 从中获取元数据文件的 Swarm 哈希,然后从 Swarm 获取该文件。该文件被解码为上面的 JSON 结构。

然后该组件可以使用ABI自动生成合约的基本用户接口。

此外,Mist可以使用 userdoc 在用户与合约进行交互时向用户显示确认消息。

有关 以太坊标准说明格式Ethereum Nature Specification Format(natspec) 的其他信息可以在 这里 找到。

源代码验证的使用方法

为了验证编译,可以通过元数据文件中的链接从 Swarm 中获取源代码。 获取到的源码,会根据元数据中指定的设置,被正确版本的编译器(应该为“官方”编译器之一)所处理。 处理得到的字节码会与创建交易的数据或者 CREATE 操作码使用的数据进行比较。 这会自动验证元数据,因为它的哈希值是字节码的一部分。 而额外的数据,则是与基于接口进行编码并展示给用户的构造输入数据相符的。

应用二进制接口Application Binary Interface(ABI) 说明

基本设计

以太坊Ethereum 生态系统中, 应用二进制接口Application Binary Interface(ABI) 是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。 数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。

我们假定合约函数的接口都是强类型的,且在编译时是可知的和静态的;不提供自我检查机制。我们假定在编译时,所有合约要调用的其他合约接口定义都是可用的。

这份手册并不针对那些动态合约接口或者仅在运行时才可获知的合约接口。如果这种场景变得很重要,你可以使用 以太坊Ethereum 生态系统中其他更合适的基础设施来处理它们。

函数选择器Function Selector

一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak(SHA-3)哈希的前 4 字节(高位在左的大端序)(译注:这里的“高位在左的大端序“,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左)。 这种签名被定义为基础原型的规范表达,基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。

注解

函数的返回类型并不是这个签名的一部分。在 Solidity 的函数重载 中,返回值并没有被考虑。这是为了使对函数调用的解析保持上下文无关。 然而 应用二进制接口Application Binary Interface(ABI) 的 JSON 描述中包含了即包含了输入也包含了输出。(参考 JSON ABI)。

参数编码

从第5字节开始是被编码的参数。这种编码也被用在其他地方,比如,返回值和事件的参数也会被用同样的方式进行编码,而用来指定函数的4个字节则不需要再进行编码。

类型

以下是基础类型:

  • uint<M>M 位的无符号整数,0 < M <= 256M % 8 == 0。例如:uint32uint8uint256
  • int<M>:以 2 的补码作为符号的 M 位整数,0 < M <= 256M % 8 == 0
  • address:除了字面上的意思和语言类型的区别以外,等价于 uint160。在计算和 函数选择器Function Selector 中,通常使用 address
  • uintintuint256int256 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 uint256int256
  • bool:等价于 uint8,取值限定为 0 或 1 。在计算和 函数选择器Function Selector 中,通常使用 bool
  • fixed<M>x<N>M 位的有符号的固定小数位的十进制数字 8 <= M <= 256M % 8 ==0、且 0 < N <= 80。其值 v 即是 v / (10 ** N)。(也就是说,这种类型是由 M 位的二进制数据所保存的,有 N 位小数的十进制数值。译者注。)
  • ufixed<M>x<N>:无符号的 fixed<M>x<N>
  • fixedufixedfixed128x18ufixed128x18 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 fixed128x18ufixed128x18
  • bytes<M>M 字节的二进制类型,0 < M <= 32
  • function:一个地址(20 字节)之后紧跟一个 函数选择器Function Selector (4 字节)。编码之后等价于 bytes24

以下是定长数组类型:

  • <type>[M]:有 M 个元素的定长数组,M >= 0,数组元素为给定类型。

以下是非定长类型:

  • bytes:动态大小的字节序列。
  • string:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。
  • <type>[]:元素为给定类型的变长数组。

可以将若干类型放到一对括号中,用逗号分隔开,以此来构成一个 元组tuple

  • (T1,T2,...,Tn):由 T1,...,Tnn >= 0 构成的 元组tuple

元组tuple 构成 元组tuple、用 元组tuple 构成数组等等也是可能的。另外也可以构成“零元组(zero-tuples)”,就是 n = 0 的情况。

注解

除了 元组tuple 以外,Solidity 支持以上所有类型的名称。ABI 元组tuple 是利用 Solidity 的 structs 编码得到的。

编码的形式化说明

我们现在来正式讲述编码,它具有如下属性,如果参数是嵌套的数组,这些属性非常有用:

属性:

1、读取的次数取决于参数数组结构中的最大深度;也就是说,要取得 a_i[k][l][r] 需要读取 4 次。在先前的ABI版本中,在最糟的情况下,读取的次数会随着动态参数的总数而线性地增长。

2、一个变量或数组元素的数据,不会被插入其他的数据,并且是可以再定位的;也就是说,它们只会使用相对的“地址”。

我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。

定义: 以下类型被称为“动态”:

  • bytes
  • string
  • 任意类型 T 的变长数组 T[]
  • 任意动态类型 T 的定长数组 T[k]k >= 0
  • 由动态的 Ti1 <= i <= k)构成的 元组tuple (T1,...,Tk)

所有其他类型都被称为“静态”。

定义: len(a) 是一个二进制字符串 a 的字节长度。len(a) 的类型被呈现为 uint256

我们把实际的编码 enc 定义为一个由ABI类型到二进制字符串的值的映射;因而,当且仅当 X 的类型是动态的,len(enc(X)) (即 X 经编码后的实际长度,译者注)才会依赖于 X 的值。

定义: 对任意ABI值 X,我们根据 X 的实际类型递归地定义 enc(X)

  • (T1,...,Tk) 对于 k >= 0 且任意类型 T1 ,..., Tk

    enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))

    这里,X = (X(1), ..., X(k)),并且 当 Ti 为静态类型时,headtail 被定义为

    head(X(i)) = enc(X(i)) and tail(X(i)) = "" (空字符串)

    否则,比如 Ti 是动态类型时,它们被定义为

    head(X(i)) = enc(len(head(X(1)) ... head(X(k-1)) tail(X(1)) ... tail(X(i-1)))) tail(X(i)) = enc(X(i))

    注意,在动态类型的情况下,由于 head 部分的长度仅取决于类型而非值,所以 head(X(i)) 是定义明确的。它的值是从 enc(X) 的开头算起的,tail(X(i)) 的起始位在 enc(X) 中的偏移量。

  • T[k] 对于任意 Tk

    enc(X) = enc((X[0], ..., X[k-1]))

    即是说,它就像是个由相同类型的 k 个元素组成的 元组tuple 那样被编码的。

  • T[]Xk 个元素(k 被呈现为类型 uint256):

    enc(X) = enc(k) enc([X[1], ..., X[k]])

    即是说,它就像是个由静态大小 k 的数组那样被编码的,且由元素的个数作为前缀。

  • 具有 k (呈现为类型 uint256)长度的 bytes

    enc(X) = enc(k) pad_right(X),即是说,字节数被编码为 uint256,紧跟着实际的 X 的字节码序列,再在前边(左边)补上可以使 len(enc(X)) 成为 32 的倍数的最少数量的 0 值字节数据。

  • string

    enc(X) = enc(enc_utf8(X)),即是说,X 被 utf-8 编码,且在后续编码中将这个值解释为 bytes 类型。注意,在随后的编码中使用的长度是其 utf-8 编码的字符串的字节数,而不是其字符数。

  • uint<M>enc(X) 是在 X 的大端序编码的前边(左边)补充若干 0 值字节以使其长度成为 32 字节。

  • address:与 uint160 的情况相同。

  • int<M>enc(X) 是在 X 的大端序的 2 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 字节;对于负数,添加值为 0xff (即 8 位全为 1,译者注)的字节数据,对于正数,添加 0 值(即 8 位全为 0,译者注)字节数据。

  • bool:与 uint8 的情况相同,1 用来表示 true0 表示 false

  • fixed<M>x<N>enc(X) 就是 enc(X * 10**N),其中 X * 10**N 可以理解为 int256

  • fixed:与 fixed128x18 的情况相同。

  • ufixed<M>x<N>enc(X) 就是 enc(X * 10**N),其中 X * 10**N 可以理解为 uint256

  • ufixed:与 ufixed128x18 的情况相同。

  • bytes<M>enc(X) 就是 X 的字节序列加上为使长度成为 32 字节而添加的若干 0 值字节。

注意,对于任意的 Xlen(enc(X)) 都是 32 的倍数。

函数选择器Function Selector 和参数编码

大体而言,一个以 a_1, ..., a_n 为参数的对 f 函数的调用,会被编码为

function_selector(f) enc((a_1, ..., a_n))

f 的返回值 v_1, ..., v_k 会被编码为

enc((v_1, ..., v_k))

也就是说,返回值会被组合为一个 元组tuple 进行编码。

例子

给定一个合约:

pragma solidity ^0.4.16;

contract Foo {
  function bar(bytes3[2]) public pure {}
  function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
  function sam(bytes, bool, uint[]) public pure {}
}

这样,对于我们的例子 Foo,如果我们想用 69true 做参数调用 baz,我们总共需要传送 68 字节,可以分解为:

  • 0xcdcd77c0:方法ID。这源自ASCII格式的 baz(uint32,bool) 签名的 Keccak 哈希的前 4 字节。
  • 0x0000000000000000000000000000000000000000000000000000000000000045:第一个参数,一个被用 0 值字节补充到 32 字节的 uint32 值 69
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数,一个被用 0 值字节补充到 32 字节的 boolean 值 true

合起来就是:

0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

它返回一个 bool。比如它返回 false,那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000,一个bool值。

如果我们想用 ["abc", "def"] 做参数调用 bar,我们总共需要传送68字节,可以分解为:

  • 0xfce353f6:方法ID。源自 bar(bytes3[2]) 的签名。
  • 0x6162630000000000000000000000000000000000000000000000000000000000:第一个参数的第一部分,一个 bytes3"abc" (左对齐)。
  • 0x6465660000000000000000000000000000000000000000000000000000000000:第一个参数的第二部分,一个 bytes3"def" (左对齐)。

合起来就是:

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

如果我们想用 "dave"true[1,2,3] 作为参数调用 sam,我们总共需要传送 292 字节,可以分解为:

  • 0xa5643bf2:方法ID。源自 sam(bytes,bool,uint256[]) 的签名。注意,uint 被替换为了它的权威代表 uint256
  • 0x0000000000000000000000000000000000000000000000000000000000000060:第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是 0x60
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数:boolean 的 true。
  • 0x00000000000000000000000000000000000000000000000000000000000000a0:第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是 0xa0
  • 0x0000000000000000000000000000000000000000000000000000000000000004:第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。
  • 0x6461766500000000000000000000000000000000000000000000000000000000:第一个参数的内容:"dave" 的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。
  • 0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第三个参数的第一个数组元素。
  • 0x0000000000000000000000000000000000000000000000000000000000000002:第三个参数的第二个数组元素。
  • 0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的第三个数组元素。

合起来就是:

0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003

动态类型的使用

用参数 (0x123, [0x456, 0x789], "1234567890", "Hello, world!") 进行对函数 f(uint,uint32[],bytes10,bytes) 的调用会通过以下方式进行编码:

取得 sha3("f(uint256,uint32[],bytes10,bytes)") 的前 4 字节,也就是 0x8be65246。 然后我们对所有 4 个参数的头部进行编码。对静态类型 uint256bytes10 是可以直接传过去的值;对于动态类型 uint32[]bytes,我们使用的字节数偏移量是它们的数据区域的起始位置,由需编码的值的开始位置算起(也就是说,不计算包含了函数签名的前 4 字节),这就是:

  • 0x00000000000000000000000000000000000000000000000000000000000001230x123 补充到 32 字节)
  • 0x0000000000000000000000000000000000000000000000000000000000000080 (第二个参数的数据部分起始位置的偏移量,4*32 字节,正好是头部的大小)
  • 0x3132333435363738393000000000000000000000000000000000000000000000"1234567890" 从右边补充到 32 字节)
  • 0x00000000000000000000000000000000000000000000000000000000000000e0 (第四个参数的数据部分起始位置的偏移量 = 第一个动态参数的数据部分起始位置的偏移量 + 第一个动态参数的数据部分的长度 = 4*32 + 3*32,参考后文)

在此之后,跟着第一个动态参数的数据部分 [0x456, 0x789]

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (数组元素个数,2)
  • 0x0000000000000000000000000000000000000000000000000000000000000456 (第一个数组元素)
  • 0x0000000000000000000000000000000000000000000000000000000000000789 (第二个数组元素)

最后,我们将第二个动态参数的数据部分 "Hello, world!" 进行编码:

  • 0x000000000000000000000000000000000000000000000000000000000000000d (元素个数,在这里是字节数:13)
  • 0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000"Hello, world!" 从右边补充到 32 字节)

最后,合并到一起的编码就是(为了清晰,在 函数选择器Function Selector 和每 32 字节之后加了换行):

0x8be65246
  0000000000000000000000000000000000000000000000000000000000000123
  0000000000000000000000000000000000000000000000000000000000000080
  3132333435363738393000000000000000000000000000000000000000000000
  00000000000000000000000000000000000000000000000000000000000000e0
  0000000000000000000000000000000000000000000000000000000000000002
  0000000000000000000000000000000000000000000000000000000000000456
  0000000000000000000000000000000000000000000000000000000000000789
  000000000000000000000000000000000000000000000000000000000000000d
  48656c6c6f2c20776f726c642100000000000000000000000000000000000000

让我们使用相同的原理来对一个签名为 g(uint[][],string[]),参数值为 ([[1, 2], [3]], ["one", "two", "three"]) 的函数来进行编码;但从最原子的部分开始:

首先我们将第一个根数组 [[1, 2], [3]] 的第一个嵌入的动态数组 [1, 2] 的长度和数据进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个数组中的元素数量 2;元素本身是 12)
  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第一个元素)
  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第二个元素)

然后我们将第一个根数组 [[1, 2], [3]] 的第二个潜入的动态数组 [3] 的长度和数据进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第二个数组中的元素数量 1;元素数据是 3)
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第一个元素)

然后我们需要找到动态数组 [1, 2][3] 的偏移量。要计算这个偏移量,我们可以来看一下第一个根数组 [[1, 2], [3]] 编码后的具体数据:

0 - a                                                                - [1, 2] 的偏移量
1 - b                                                                - [3] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的计数
3 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
4 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的计数
6 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码

偏移量 a 指向数组 [1, 2] 内容的开始位置,即第 2 行的开始(64 字节);所以 a = 0x0000000000000000000000000000000000000000000000000000000000000040

偏移量 b 指向数组 [3] 内容的开始位置,即第 5 行的开始(160 字节);所以 b = 0x00000000000000000000000000000000000000000000000000000000000000a0

然后我们对第二个根数组的嵌入字符串进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (单词 "one" 中的字符个数)
  • 0x6f6e650000000000000000000000000000000000000000000000000000000000 (单词 "one" 的 utf8 编码)
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (单词 "two" 中的字符个数)
  • 0x74776f0000000000000000000000000000000000000000000000000000000000 (单词 "two" 的 utf8 编码)
  • 0x0000000000000000000000000000000000000000000000000000000000000005 (单词 "three" 中的字符个数)
  • 0x7468726565000000000000000000000000000000000000000000000000000000 (单词 "three" 的 utf8 编码)

作为与第一个根数组的并列,因为字符串也属于动态元素,我们也需要找到它们的偏移量 c, de

0 - c                                                                - "one" 的偏移量
1 - d                                                                - "two" 的偏移量
2 - e                                                                - "three" 的偏移量
3 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
8 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码

偏移量 c 指向字符串 "one" 内容的开始位置,即第 3 行的开始(96 字节);所以 c = 0x0000000000000000000000000000000000000000000000000000000000000060

偏移量 d 指向字符串 "two" 内容的开始位置,即第 5 行的开始(160 字节);所以 d = 0x00000000000000000000000000000000000000000000000000000000000000a0

偏移量 e 指向字符串 "three" 内容的开始位置,即第 7 行的开始(224 字节);所以 e = 0x00000000000000000000000000000000000000000000000000000000000000e0

注意,根数组的嵌入元素的编码并不互相依赖,且具有对于函数签名 g(string[],uint[][]) 所相同的编码。

然后我们对第一个根数组的长度进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个根数组的元素数量 2;这些元素本身是 [1, 2][3])

而后我们对第二个根数组的长度进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第二个根数组的元素数量 3;这些字符串本身是 "one""two""three")

最后,我们找到根动态数组元素 [[1, 2], [3]]["one", "two", "three"] 的偏移量 fg。汇编数据的正确顺序如下:

0x2289b18c                                                            - 函数签名
 0 - f                                                                - [[1, 2], [3]] 的偏移量
 1 - g                                                                - ["one", "two", "three"] 的偏移量
 2 - 0000000000000000000000000000000000000000000000000000000000000002 - [[1, 2], [3]] 的元素计数
 3 - 0000000000000000000000000000000000000000000000000000000000000040 - [1, 2] 的偏移量
 4 - 00000000000000000000000000000000000000000000000000000000000000a0 - [3] 的偏移量
 5 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的元素计数
 6 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
 7 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
 8 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的元素计数
 9 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
10 - 0000000000000000000000000000000000000000000000000000000000000003 - ["one", "two", "three"] 的元素计数
11 - 0000000000000000000000000000000000000000000000000000000000000060 - "one" 的偏移量
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - "two" 的偏移量
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - "three" 的偏移量
14 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
16 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
18 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
19 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码

偏移量 f 指向数组 [[1, 2], [3]] 内容的开始位置,即第 2 行的开始(64 字节);所以 f = 0x0000000000000000000000000000000000000000000000000000000000000040

偏移量 g 指向数组 ["one", "two", "three"] 内容的开始位置,即第 10 行的开始(320 字节);所以 g = 0x0000000000000000000000000000000000000000000000000000000000000140

事件

事件,是 以太坊Ethereum 的日志/事件监视协议的一个抽象。日志项提供了合约的地址、一系列的主题(最高 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能(与接口定义一起),事件沿用了既存的 ABI 函数。

给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有 3 个,被用来与事件签名的 Keccak 哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。

这样,一个使用 ABI 的日志项就可以描述为:

  • address:合约地址(由 以太坊Ethereum 真正提供);
  • topics[0]keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")canonical_type_of 是一个可以返回给定参数的权威类型的函数,例如,对 uint indexed foo 它会返回 uint256)。如果事件被声明为 anonymous,那么 topics[0] 不会被生成;
  • topics[n]EVENT_INDEXED_ARGS[n - 1]EVENT_INDEXED_ARGS 是已索引的 EVENT_ARGS);
  • dataabi_serialise(EVENT_NON_INDEXED_ARGS)EVENT_NON_INDEXED_ARGS 是未索引的 EVENT_ARGSabi_serialise 是一个用来从某个函数返回一系列类型值的ABI序列化函数,就像上文所讲的那样)。

对于所有定长的Solidity类型,EVENT_INDEXED_ARGS 数组会直接包含32字节的编码值。然而,对于 动态长度的类型 ,包含 stringbytes 和数组, EVENT_INDEXED_ARGS 会包含编码值的 Keccak 哈希 而不是直接包含编码值。这样就允许应用程序更有效地查询动态长度类型的值(通过把编码值的哈希设定为主题), 但也使应用程序不能对它们还没查询过的已索引的值进行解码。对于动态长度的类型,应用程序开发者面临在对预先设定的值(如果参数已被索引)的快速检索和对任意数据的清晰处理(需要参数不被索引)之间的权衡。 开发者们可以通过定义两个参数(一个已索引、一个未索引)保存同一个值的方式来解决这种权衡,从而既获得高效的检索又能清晰地处理任意数据。

JSON

合约接口的JSON格式是由一个函数和/或事件描述的数组所给定的。一个函数的描述是一个有如下字段的JSON对象:

  • type"function""constructor""fallback"未命名的 "缺省" 函数
  • name:函数名称;
  • inputs:对象数组,每个数组对象会包含:
    • name:参数名称;
    • type:参数的权威类型(详见下文)
    • components:供 元组tuple 类型使用(详见下文)
  • outputs:一个类似于 inputs 的对象数组,如果函数无返回值时可以被省略;
  • payable:如果函数接受 以太币Ether ,为 true;缺省为 false
  • stateMutability:为下列值之一:pure指定为不读取区块链状态),view指定为不修改区块链状态),nonpayablepayable (与上文 payable 一样)。
  • constant:如果函数被指定为 pureview 则为 true

type 可以被省略,缺省为 "function"

Constructor 和 fallback 函数没有 nameoutputs。Fallback 函数也没有 inputs

向 non-payable(即不接受 以太币Ether )的函数发送非零值的 以太币Ether 会导致其丢失。不要这么做。

一个事件描述是一个有极其相似字段的 JSON 对象:

  • type:总是 "event"
  • name:事件名称;
  • inputs:对象数组,每个数组对象会包含:
    • name:参数名称;
    • type:参数的权威类型(相见下文);
    • components:供 元组tuple 类型使用(详见下文);
    • indexed:如果此字段是日志的一个主题,则为 true;否则为 false
  • anonymous:如果事件被声明为 anonymous,则为 true

例如,

pragma solidity ^0.4.0;

contract Test {
  function Test() public { b = 0x12345678901234567890123456789012; }
  event Event(uint indexed a, bytes32 b);
  event Event2(uint indexed a, bytes32 b);
  function foo(uint a) public { Event(a, b); }
  bytes32 b;
}

可由如下 JSON 来表示:

[{
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
}]

处理 元组tuple 类型

尽管名称被有意地不作为 ABI 编码的一部分,但将它们包含进JSON来显示给最终用户是非常合理的。其结构会按下列方式进行嵌套:

一个拥有 nametype 和潜在的 components 成员的对象描述了某种类型的变量。 直至到达一个 元组tuple 类型且到那点的存储在 type 属性中的字符串以 tuple 为前缀,也就是说,在 tuple 之后紧跟一个 [] 或有整数 k[k],才能确定一个 元组tuple元组tuple 的组件元素会被存储在成员 components 中,它是一个数组类型,且与顶级对象具有同样的结构,只是在这里不允许已索引的(indexed)数组元素。

作为例子,代码

pragma solidity ^0.4.19;
pragma experimental ABIEncoderV2;

contract Test {
  struct S { uint a; uint[] b; T[] c; }
  struct T { uint x; uint y; }
  function f(S s, T t, uint a) public { }
  function g() public returns (S s, T t, uint a) {}
}

可由如下 JSON 来表示:

[
  {
    "name": "f",
    "type": "function",
    "inputs": [
      {
        "name": "s",
        "type": "tuple",
        "components": [
          {
            "name": "a",
            "type": "uint256"
          },
          {
            "name": "b",
            "type": "uint256[]"
          },
          {
            "name": "c",
            "type": "tuple[]",
            "components": [
              {
                "name": "x",
                "type": "uint256"
              },
              {
                "name": "y",
                "type": "uint256"
              }
            ]
          }
        ]
      },
      {
        "name": "t",
        "type": "tuple",
        "components": [
          {
            "name": "x",
            "type": "uint256"
          },
          {
            "name": "y",
            "type": "uint256"
          }
        ]
      },
      {
        "name": "a",
        "type": "uint256"
      }
    ],
    "outputs": []
  }
]

非标准打包模式

Solidity 支持一种非标准打包模式:

  • 函数选择器 不进行编码,
  • 长度低于 32 字节的类型,既不会进行补 0 操作,也不会进行符号扩展,以及
  • 动态类型会直接进行编码,并且不包含长度信息。

例如,对 int1, bytes1, uint16, string 用数值 -1, 0x42, 0x2424, "Hello, world!" 进行编码将生成如下结果

0xff42242448656c6c6f2c20776f726c6421
  ^^                                 int1(-1)
    ^^                               bytes1(0x42)
      ^^^^                           uint16(0x2424)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field

更具体地说,每个静态大小的类型都尽可能多地按它们的数值范围使用了字节数,而动态大小的类型,像 stringbytesuint[],在编码时没有包含其长度信息。 这意味着一旦有两个动态长度的元素,编码就会变得有歧义了。

Yul

Yul (先前被也被称为 JULIA 或 IULIA)是一种可以编译到各种不同后端的中间语言( 以太坊虚拟机Ethereum Virtual Machine(EVM) 1.0,以太坊虚拟机Ethereum Virtual Machine(EVM) 1.5,而 eWASM 也在计划中)。 正因为如此,它被设计成为这三种平台的可用的共同标准。 它已经可以用于 Solidity 内部的“内联汇编”,并且未来版本的 Solidity 编译器甚至会将 Yul 用作中间语言。 为 Yul 构建高级的优化器阶段也将会很容易。

注解

请注意,用于“内联汇编”的书写风格是不带类型的(所有的都是 u256),内置函数与 以太坊虚拟机Ethereum Virtual Machine(EVM) 操作码相同。 有关详细信息,请参阅内联汇编文档。

Yul 的核心组件是函数,代码块,变量,字面量,for 循环,if 条件语句,switch 条件语句,表达式和变量赋值。

Yul 是强类型的,变量和字面量都需要通过前缀符号来指明类型。支持的类型有:boolu8s8u32s32u64s64u128s128u256s256

Yul 本身甚至不提供操作符。如果目标平台是 以太坊虚拟机Ethereum Virtual Machine(EVM),则操作码将作为内置函数提供,但如果后端平台发生了变化,则可以重新实现它们。 有关强制性的内置函数的列表,请参阅下面的章节。

以下示例程序假定 以太坊虚拟机Ethereum Virtual Machine(EVM) 操作码 muldivmo 是原生支持或可以作为函数用以计算指数的。

{
    function power(base:u256, exponent:u256) -> result:u256
    {
        switch exponent
        case 0:u256 { result := 1:u256 }
        case 1:u256 { result := base }
        default:
        {
            result := power(mul(base, base), div(exponent, 2:u256))
            switch mod(exponent, 2:u256)
                case 1:u256 { result := mul(base, result) }
        }
    }
}

也可用 for 循环代替递归来实现相同的功能。这里,我们需要 以太坊虚拟机Ethereum Virtual Machine(EVM) 操作码 lt (小于)和 add 可用。

{
    function power(base:u256, exponent:u256) -> result:u256
    {
        result := 1:u256
        for { let i := 0:u256 } lt(i, exponent) { i := add(i, 1:u256) }
        {
            result := mul(result, base)
        }
    }
}

Yul 语言说明

本章介绍 Yul 代码。Yul 代码通常放置在一个 Yul 对象中,它将在下一节中介绍。

语法:

代码块 = '{' 语句* '}'
语句 =
    代码块 |
    函数定义 |
    变量声明 |
    赋值 |
    表达式 |
    Switch |
    For 循环 |
    循环中断
函数定义 =
    'function' 标识符 '(' 带类型的标识符列表? ')'
    ( '->' 带类型的标识符列表 )? 代码块
变量声明 =
    'let' 带类型的标识符列表 ( ':=' 表达式 )?
赋值 =
    标识符列表 ':=' 表达式
表达式 =
    函数调用 | 标识符 | 字面量
If 条件语句 =
    'if' 表达式 代码块
Switch 条件语句 =
    'switch' 表达式 Case* ( 'default' 代码块 )?
Case =
    'case' 字面量 代码块
For 循环 =
    'for' 代码块 表达式 代码块 代码块
循环中断 =
    'break' | 'continue'
函数调用 =
    标识符 '(' ( 表达式 ( ',' 表达式 )* )? ')'
标识符 = [a-zA-Z_$] [a-zA-Z_0-9]*
标识符列表 = 标识符 ( ',' 标识符)*
类型名 = 标识符 | 内置的类型名
内置的类型名 = 'bool' | [us] ( '8' | '32' | '64' | '128' | '256' )
带类型的标识符列表 = 标识符 ':' 类型名 ( ',' 标识符 ':' 类型名 )*
字面量 =
    (数字字面量 | 字符串字面量 | 十六进制字面量 | True字面量 | False字面量) ':' 类型名
数字字面量 = 十六进制数字 | 十进制数字
十六进制字面量 = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
字符串字面量 = '"' ([^"\r\n\\] | '\\' .)* '"'
True字面量 = 'true'
False字面量 = 'false'
十六进制数字 = '0x' [0-9a-fA-F]+
十进制数字 = [0-9]+

语法层面的限制

Switches 必须至少有一个 case(包括 default )。 如果表达式的所有可能值都被覆盖了,那么不应该允许使用 default (即带 bool 表达式的 switch 语句同时具有 true case 和 false case 的情况下不应再有 default 语句)。

每个表达式都求值为零个或多个值。 标识符和字面量求值为一个值,函数调用求值为所调用函数的返回值。

在变量声明和赋值中,右侧表达式(如果存在)求值后,必须得出与左侧变量数量相等的值。 这是唯一允许求值出多个值的表达式。

那种同时又是语句的表达式(即在代码块的层次)求值结果必须只有零个值。

在其他所有情况中,表达式求值后必须仅有一个值。

continuebreak 语句只能用在循环体中,并且必须与循环处于同一个函数中(或者两者都必须在顶层)。

for 循环的条件部分的求值结果只能为一个值。

字面量不可以大于它们本身的类型。已定义的最大类型宽度为 256 比特。

作用域规则

Yul 中的作用域是与块(除了函数和 for 循环,如下所述)和所有引入新的标识符到作用域中的声明 ( FunctionDefinitionVariableDeclaration )紧密绑定的。

标识符在将其定义的块中可见(包括所有子节点和子块)。 作为例外,for 循环的 “init” 部分中(第一个块)定义的标识符在 for 循环的所有其他部分(但不在循环之外)中都是可见的。 在 for 循环的其他部分声明的标识符遵守常规的作用域语法规则。 函数的参数和返回参数在函数体中可见,并且它们的名称不能相同。

变量只能在声明后引用。 尤其是,变量不能在它们自己的变量声明的右边被引用。 函数可以在声明之前被引用(如果它们是可见的)。

Shadowing 是不被允许的,即是说,你不能在同名标识符已经可见的情况下又定义该标识符,即使它是不可访问的。

在函数内,不可能访问声明在函数外的变量。

形式规范

我们通过在 AST 的各个节点上提供重载的求值函数 E 来正式指定 Yul。 任何函数都可能有副作用,所以 E 接受两个状态对象和 AST 节点作为它的参数,并返回两个新的状态对象和数量可变的其他值。

这两个状态对象是全局状态对象(在 以太坊虚拟机Ethereum Virtual Machine(EVM) 的上下文中是 内存memory存储storage 和区块链的状态)和本地状态对象(局部变量的状态,即 以太坊虚拟机Ethereum Virtual Machine(EVM) 中堆栈的某个段)。 如果 AST 节点是一个语句,E 将返回两个状态对象和一个用于 break 和 continue 语句的 “mode”。 如果 AST 节点是表达式,则 E 返回两个状态对象,并返回与表达式求值结果相同数量的值。

在这份高层次的描述中,并没有对全局状态的确切本质进行说明。 本地状态 L 是标识符 i 到值 v 的映射,表示为 L[i] = v。 对于标识符 v, 我们用 $v 作为标识符的名字。

我们将为 AST 节点使用解构符号。

E(G, L, <{St1, ..., Stn}>: Block) =
    let G1, L1, mode = E(G, L, St1, ..., Stn)
    let L2 be a restriction of L1 to the identifiers of L
    G1, L2, mode
E(G, L, St1, ..., Stn: Statement) =
    if n is zero:
        G, L, regular
    else:
        let G1, L1, mode = E(G, L, St1)
        if mode is regular then
            E(G1, L1, St2, ..., Stn)
        otherwise
            G1, L1, mode
E(G, L, FunctionDefinition) =
    G, L, regular
E(G, L, <let var1, ..., varn := rhs>: VariableDeclaration) =
    E(G, L, <var1, ..., varn := rhs>: Assignment)
E(G, L, <let var1, ..., varn>: VariableDeclaration) =
    let L1 be a copy of L where L1[$vari] = 0 for i = 1, ..., n
    G, L1, regular
E(G, L, <var1, ..., varn := rhs>: Assignment) =
    let G1, L1, v1, ..., vn = E(G, L, rhs)
    let L2 be a copy of L1 where L2[$vari] = vi for i = 1, ..., n
    G, L2, regular
E(G, L, <for { i1, ..., in } condition post body>: ForLoop) =
    if n >= 1:
        let G1, L1, mode = E(G, L, i1, ..., in)
        // 由于语法限制,mode 必须是规则的
        let G2, L2, mode = E(G1, L1, for {} condition post body)
        // 由于语法限制,mode 必须是规则的
        let L3 be the restriction of L2 to only variables of L
        G2, L3, regular
    else:
        let G1, L1, v = E(G, L, condition)
        if v is false:
            G1, L1, regular
        else:
            let G2, L2, mode = E(G1, L, body)
            if mode is break:
                G2, L2, regular
            else:
                G3, L3, mode = E(G2, L2, post)
                E(G3, L3, for {} condition post body)
E(G, L, break: BreakContinue) =
    G, L, break
E(G, L, continue: BreakContinue) =
    G, L, continue
E(G, L, <if condition body>: If) =
    let G0, L0, v = E(G, L, condition)
    if v is true:
        E(G0, L0, body)
    else:
        G0, L0, regular
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn>: Switch) =
    E(G, L, switch condition case l1:t1 st1 ... case ln:tn stn default {})
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn default st'>: Switch) =
    let G0, L0, v = E(G, L, condition)
    // i = 1 .. n
    // 对字面量求值,上下文无关
    let _, _, v1 = E(G0, L0, l1)
    ...
    let _, _, vn = E(G0, L0, ln)
    if there exists smallest i such that vi = v:
        E(G0, L0, sti)
    else:
        E(G0, L0, st')

E(G, L, <name>: Identifier) =
    G, L, L[$name]
E(G, L, <fname(arg1, ..., argn)>: FunctionCall) =
    G1, L1, vn = E(G, L, argn)
    ...
    G(n-1), L(n-1), v2 = E(G(n-2), L(n-2), arg2)
    Gn, Ln, v1 = E(G(n-1), L(n-1), arg1)
    Let <function fname (param1, ..., paramn) -> ret1, ..., retm block>
    be the function of name $fname visible at the point of the call.
    Let L' be a new local state such that
    L'[$parami] = vi and L'[$reti] = 0 for all i.
    Let G'', L'', mode = E(Gn, L', block)
    G'', Ln, L''[$ret1], ..., L''[$retm]
E(G, L, l: HexLiteral) = G, L, hexString(l),
    where hexString decodes l from hex and left-aligns it into 32 bytes
E(G, L, l: StringLiteral) = G, L, utf8EncodeLeftAligned(l),
    where utf8EncodeLeftAligned performs a utf8 encoding of l
    and aligns it left into 32 bytes
E(G, L, n: HexNumber) = G, L, hex(n)
    where hex is the hexadecimal decoding function
E(G, L, n: DecimalNumber) = G, L, dec(n),
    where dec is the decimal decoding function

类型转换函数

Yul 不支持隐式类型转换,因此存在提供显式转换的函数。 在将较大类型转换为较短类型时,如果发生溢出,则可能会发生运行时异常。

下列类型的“截取式”转换是允许的:
  • bool
  • u32
  • u64
  • u256
  • s256

这里的每种类型的转换函数都有一个格式为 <input_type>to<output_type>(x:<input_type>) -> y:<output_type> 的原型, 比如 u32tobool(x:u32) -> y:boolu256tou32(x:u256) -> y:u32s256tou256(x:s256) -> y:u256

注解

u32tobool(x:u32) -> y:bool 可以由 y := not(iszerou256(x)) 实现,并且 booltou32(x:bool) -> y:u32 可以由 switch x case true:bool { y := 1:u32 } case false:bool { y := 0:u32 } 实现

低级函数

以下函数必须可用:

逻辑操作
not(x:bool) -> z:bool 逻辑非
and(x:bool, y:bool) -> z:bool 逻辑与
or(x:bool, y:bool) -> z:bool 逻辑或
xor(x:bool, y:bool) -> z:bool 异或
算术操作
addu256(x:u256, y:u256) -> z:u256 x + y
subu256(x:u256, y:u256) -> z:u256 x - y
mulu256(x:u256, y:u256) -> z:u256 x * y
divu256(x:u256, y:u256) -> z:u256 x / y
divs256(x:s256, y:s256) -> z:s256 x / y, 有符号数用补码形式
modu256(x:u256, y:u256) -> z:u256 x % y
mods256(x:s256, y:s256) -> z:s256 x % y, 有符号数用补码形式
signextendu256(i:u256, x:u256) -> z:u256 从第 (i*8+7) 位开始进行符号扩展,从最低符号位开始计算
expu256(x:u256, y:u256) -> z:u256 x 的 y 次方
addmodu256(x:u256, y:u256, m:u256) -> z:u256 任意精度的数学模运算 (x + y) % m
mulmodu256(x:u256, y:u256, m:u256) -> z:u256 任意精度的数学模运算 (x * y) % m
ltu256(x:u256, y:u256) -> z:bool 若 x < y 为 true, 否则为 false
gtu256(x:u256, y:u256) -> z:bool 若 x > y 为 true, 否则为 false
sltu256(x:s256, y:s256) -> z:bool 若 x < y 为 true, 否则为 false 有符号数用补码形式
sgtu256(x:s256, y:s256) -> z:bool 若 x > y 为 true, 否则为 false 有符号数用补码形式
equ256(x:u256, y:u256) -> z:bool 若 x == y 为 true, 否则为 false
iszerou256(x:u256) -> z:bool 若 x == 0 为 true, 否则为 false
notu256(x:u256) -> z:u256 ~x, 对 x 按位非
andu256(x:u256, y:u256) -> z:u256 x 和 y 按位与
oru256(x:u256, y:u256) -> z:u256 x 和 y 按位或
xoru256(x:u256, y:u256) -> z:u256 x 和 y 按位异或
shlu256(x:u256, y:u256) -> z:u256 将 x 逻辑左移 y 位
shru256(x:u256, y:u256) -> z:u256 将 x 逻辑右移 y 位
saru256(x:u256, y:u256) -> z:u256 将 x 算术右移 y 位
byte(n:u256, x:u256) -> v:u256 x 的第 n 字节,这里的索引位置是从 0 开始的; 能否用 and256(shr256(n, x), 0xff) 来替换它, 并使它在 EVM 后端之外被优化呢?
内存和存储
mload(p:u256) -> v:u256 mem[p..(p+32))
mstore(p:u256, v:u256) mem[p..(p+32)) := v
mstore8(p:u256, v:u256) mem[p] := v & 0xff - 仅修改单个字节
sload(p:u256) -> v:u256 storage[p]
sstore(p:u256, v:u256) storage[p] := v
msize() -> size:u256 内存的大小, 即已访问过的内存的最大下标, 因为内存扩展的限制(只能按字进行扩展) 返回值永远都是 32 字节的倍数
执行控制
create(v:u256, p:u256, s:u256) 以 mem[p..(p+s)) 上的代码创建一个新合约,发送 v 个 wei,并返回一个新的地址
call(g:u256, a:u256, v:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 调用地址 a 上的合约,以 mem[in..(in+insize)) 作为输入 一并发送 g gas 和 v wei ,以 mem[out..(out+outsize)) 作为输出空间。若错误,返回 0 (比如,gas 用光 成功,返回 1
callcode(g:u256, a:u256, v:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 相当于 call 但仅仅使用地址 a 上的代码, 而留在当前合约的上下文当中
delegatecall(g:u256, a:u256, in:u256, insize:u256, out:u256, outsize:u256) -> r:u256 相当于 callcode, 但同时保留 callercallvalue
abort() 终止 (相当于EVM上的非法指令)
return(p:u256, s:u256) 终止执行,返回 mem[p..(p+s)) 上的数据
revert(p:u256, s:u256) 终止执行,恢复状态变更,返回 mem[p..(p+s)) 上的数据
selfdestruct(a:u256) 终止执行,销毁当前合约,并且将余额发送到地址 a
log0(p:u256, s:u256) 用 mem[p..(p+s)] 上的数据产生日志,但没有 topic
log1(p:u256, s:u256, t1:u256) 用 mem[p..(p+s)] 上的数据和 topic t1 产生日志
log2(p:u256, s:u256, t1:u256, t2:u256) 用 mem[p..(p+s)] 上的数据和 topic t1,t2 产生日志
log3(p:u256, s:u256, t1:u256, t2:u256, t3:u256) 用 mem[p..(p+s)] 上的数据和 topic t1,t2,t3 产生日志
log4(p:u256, s:u256, t1:u256, t2:u256, t3:u256, t4:u256) 用 mem[p..(p+s)] 上的数据和 topic t1,t2,t3,t4 产生日志
状态查询
blockcoinbase() -> address:u256 当前的矿工
blockdifficulty() -> difficulty:u256 当前区块的难度
blockgaslimit() -> limit:u256 当前区块的区块 gas 限制
blockhash(b:u256) -> hash:u256 区块号为 b 的区块的哈希, 仅可用于最近的 256 个区块,不包含当前区块
blocknumber() -> block:u256 当前区块号
blocktimestamp() -> timestamp:u256 自 epoch 开始的,当前块的时间戳,以秒为单位
txorigin() -> address:u256 交易的发送方
txgasprice() -> price:u256 交易中的 gas 价格
gasleft() -> gas:u256 还可用于执行的 gas
balance(a:u256) -> v:u256 地址 a 上的 wei 余额
this() -> address:u256 当前合约/执行上下文的地址
caller() -> address:u256 调用的发送方 (不包含委托调用)
callvalue() -> v:u256 与当前调用一起发送的 wei
calldataload(p:u256) -> v:u256 从 position p 开始的 calldata (32 字节)
calldatasize() -> v:u256 以字节为单位的 calldata 的大小
calldatacopy(t:u256, f:u256, s:u256) 从位置为 f 的 calldata 中,拷贝 s 字节到内存位置 t
codesize() -> size:u256 当前合约/执行上下文的代码大小
codecopy(t:u256, f:u256, s:u256) 从 code 位置 f 拷贝 s 字节到内存位置 t
extcodesize(a:u256) -> size:u256 地址 a 上的代码大小
extcodecopy(a:u256, t:u256, f:u256, s:u256) 相当于 codecopy(t, f, s),但从地址 a 获取代码
其他
discard(unused:bool) 丢弃值
discardu256(unused:u256) 丢弃值
splitu256tou64(x:u256) -> (x1:u64, x2:u64,
x3:u64, x4:u64)
将一个 u256 拆分为四个 u64
combineu64tou256(x1:u64, x2:u64, x3:u64,
x4:u64) -> (x:u256)
将四个 u64 组合为一个 u256
keccak256(p:u256, s:u256) -> v:u256 keccak(mem[p...(p+s)))

后端

后端或目标负责将 Yul 翻译到特定字节码。 每个后端都可以暴露以后端名称为前缀的函数。 我们为两个建议的后端保留 evm_ewasm_ 前缀。

后端: EVM

目标 以太坊虚拟机Ethereum Virtual Machine(EVM) 将具有所有用 evm_ 前缀暴露的 以太坊虚拟机Ethereum Virtual Machine(EVM) 底层操作码。

后端: "EVM 1.5"

TBD

后端: eWASM

TBD

Yul 对象说明

语法:

顶层对象 = 'object' '{' 代码? ( 对象 | 数据 )* '}'
对象 = 'object' 字符串字面量 '{' 代码? ( 对象 | 数据 )* '}'
代码 = 'code' 代码块
数据 = 'data' 字符串字面量 十六进制字面量
十六进制字面量 = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
字符串字面量 = '"' ([^"\r\n\\] | '\\' .)* '"'

在上面,代码块 指的是前一章中解释的 Yul 代码语法中的 代码块

Yul 对象示例如下:

..code:

// 代码由单个对象组成。 单个 “code” 节点是对象的代码。
// 每个(其他)命名的对象或数据部分都被序列化
// 并可供特殊内置函数:datacopy / dataoffset / datasize 用于访问
object {
    code {
        let size = datasize("runtime")
        let offset = allocate(size)
        // 这里,对于 eWASM 变为一个内存到内存的拷贝,对于 EVM 则相当于 codecopy
        datacopy(dataoffset("runtime"), offset, size)
        // 这是一个构造函数,并且运行时代码会被返回
        return(offset, size)
    }

    data "Table2" hex"4123"

    object "runtime" {
        code {
            // 运行时代码

            let size = datasize("Contract2")
            let offset = allocate(size)
            // 这里,对于 eWASM 变为一个内存到内存的拷贝,对于 EVM 则相当于 codecopy
            datacopy(dataoffset("Contract2"), offset, size)
            // 构造函数参数是一个数字 0x1234
            mstore(add(offset, size), 0x1234)
            create(offset, add(size, 32))
        }

        // 内嵌对象。使用场景是,外层是一个工厂合约,而 Contract2 将是由工厂生成的代码
        object "Contract2" {
            code {
                // 代码在这 ...
            }

            object "runtime" {
                code {
                    // 代码在这 ...
                }
             }

             data "Table1" hex"4123"
        }
    }
}

风格指南

概述

本指南旨在约定 solidity 代码的编码规范。本指南是不断变化演进的,旧的、过时的编码规范会被淘汰, 而新的、有用的规范会被添加进来。

许多项目会实施他们自己的编码风格指南。如遇冲突,应优先使用具体项目的风格指南。

本风格指南中的结构和许多建议是取自 python 的 pep8 style guide

本指南并 不是 以指导正确或最佳的 solidity 编码方式为目的。本指南的目的是保持代码的 一致性 。 来自 python 的参考文档 pep8 。 很好地阐述了这个概念。

风格指南是关于一致性的。重要的是与此风格指南保持一致。但项目中的一致性更重要。一个模块或功能内的一致性是最重要的。 但最重要的是:知道什么时候不一致 —— 有时风格指南不适用。如有疑问,请自行判断。看看其他例子,并决定什么看起来最好。并应毫不犹豫地询问他人!

代码结构

缩进

每个缩进级别使用4个空格。

制表符或空格

空格是首选的缩进方法。

应该避免混合使用制表符和空格。

空行

在 solidity 源码中合约声明之间留出两个空行。

正确写法:

contract A {
    ...
}


contract B {
    ...
}


contract C {
    ...
}

错误写法:

contract A {
    ...
}
contract B {
    ...
}

contract C {
    ...
}

在一个合约中的函数声明之间留有一个空行。

在相关联的各组单行语句之间可以省略空行。(例如抽象合约的 stub 函数)。

正确写法:

contract A {
    function spam() public;
    function ham() public;
}


contract B is A {
    function spam() public {
        ...
    }

    function ham() public {
        ...
    }
}

错误写法:

contract A {
    function spam() public {
        ...
    }
    function ham() public {
        ...
    }
}

代码行的最大长度

基于 PEP 8 recommendation ,将代码行的字符长度控制在 79(或 99)字符来帮助读者阅读代码。

折行时应该遵从以下指引:

  1. 第一个参数不应该紧跟在左括号后边
  2. 用一个、且只用一个缩进
  3. 每个函数应该单起一行
  4. 结束符号 ); 应该单独放在最后一行

函数调用

Yes:

thisFunctionCallIsReallyLong(
    longArgument1,
    longArgument2,
    longArgument3
);

No:

thisFunctionCallIsReallyLong(longArgument1,
                              longArgument2,
                              longArgument3
);

thisFunctionCallIsReallyLong(longArgument1,
    longArgument2,
    longArgument3
);

thisFunctionCallIsReallyLong(
    longArgument1, longArgument2,
    longArgument3
);

thisFunctionCallIsReallyLong(
longArgument1,
longArgument2,
longArgument3
);

thisFunctionCallIsReallyLong(
    longArgument1,
    longArgument2,
    longArgument3);

赋值语句

Yes:

thisIsALongNestedMapping[being][set][to_some_value] = someFunction(
    argument1,
    argument2,
    argument3,
    argument4
);

No:

thisIsALongNestedMapping[being][set][to_some_value] = someFunction(argument1,
                                                                   argument2,
                                                                   argument3,
                                                                   argument4);

事件定义和事件发生

Yes:

event LongAndLotsOfArgs(
    adress sender,
    adress recipient,
    uint256 publicKey,
    uint256 amount,
    bytes32[] options
);

LongAndLotsOfArgs(
    sender,
    recipient,
    publicKey,
    amount,
    options
);

No:

event LongAndLotsOfArgs(adress sender,
                        adress recipient,
                        uint256 publicKey,
                        uint256 amount,
                        bytes32[] options);

LongAndLotsOfArgs(sender,
                  recipient,
                  publicKey,
                  amount,
                  options);

源文件编码格式

首选 UTF-8 或 ASCII 编码。

Imports 规范

Import 语句应始终放在文件的顶部。

正确写法:

import "owned";


contract A {
    ...
}


contract B is owned {
    ...
}

错误写法:

contract A {
    ...
}


import "owned";


contract B is owned {
    ...
}

函数顺序

排序有助于读者识别他们可以调用哪些函数,并更容易地找到构造函数和 fallback 函数的定义。

函数应根据其可见性和顺序进行分组:

  • 构造函数
  • fallback 函数(如果存在)
  • 外部函数
  • 公共函数
  • 内部函数和变量
  • 私有函数和变量

在一个分组中,把 viewpure 函数放在最后。

正确写法:

contract A {
    function A() public {
        ...
    }

    function() public {
        ...
    }

    // External functions
    // ...

    // External functions that are view
    // ...

    // External functions that are pure
    // ...

    // Public functions
    // ...

    // Internal functions
    // ...

    // Private functions
    // ...
}

错误写法:

contract A {

    // External functions
    // ...

    // Private functions
    // ...

    // Public functions
    // ...

    function A() public {
        ...
    }

    function() public {
        ...
    }

    // Internal functions
    // ...
}

表达式中的空格

在以下情况下避免无关的空格:

除单行函数声明外,紧接着小括号,中括号或者大括号的内容应该避免使用空格。

正确写法:

spam(ham[1], Coin({name: "ham"}));

错误写法:

spam( ham[ 1 ], Coin( { name: "ham" } ) );

除外:

function singleLine() public { spam(); }

紧接在逗号,分号之前:

正确写法:

function spam(uint i, Coin coin) public;

错误写法:

function spam(uint i , Coin coin) public ;

赋值或其他操作符两边多于一个的空格:

正确写法:

x = 1;
y = 2;
long_variable = 3;

错误写法:

x             = 1;
y             = 2;
long_variable = 3;

fallback 函数中不要包含空格:

正确写法:

function() public {
    ...
}

错误写法:

function () public {
    ...
}

控制结构

用大括号表示一个合约,库、函数和结构。 应该:

  • 开括号与声明应在同一行。
  • 闭括号在与之前函数声明对应的开括号保持同一缩进级别上另起一行。
  • 开括号前应该有一个空格。

正确写法:

contract Coin {
    struct Bank {
        address owner;
        uint balance;
    }
}

错误写法:

contract Coin
{
    struct Bank {
        address owner;
        uint balance;
    }
}

对于控制结构 ifelsewhilefor 的实施建议与以上相同。

另外,诸如 ifelsewhilefor 这类的控制结构和条件表达式的块之间应该有一个单独的空格, 同样的,条件表达式的块和开括号之间也应该有一个空格。

正确写法:

if (...) {
    ...
}

for (...) {
    ...
}

错误写法:

if (...)
{
    ...
}

while(...){
}

for (...) {
    ...;}

对于控制结构, 如果 其主体内容只包含一行,则可以省略括号。

正确写法:

if (x < 10)
    x += 1;

错误写法:

if (x < 10)
    someArray.push(Coin({
        name: 'spam',
        value: 42
    }));

对于具有 elseelse if 子句的 if 块, else 应该是与 if 的闭大括号放在同一行上。 这一规则区别于 其他块状结构。

正确写法:

if (x < 3) {
    x += 1;
} else if (x > 7) {
    x -= 1;
} else {
    x = 5;
}


if (x < 3)
    x += 1;
else
    x -= 1;

错误写法:

if (x < 3) {
    x += 1;
}
else {
    x -= 1;
}

函数声明

对于简短的函数声明,建议函数体的开括号与函数声明保持在同一行。

闭大括号应该与函数声明的缩进级别相同。

开大括号之前应该有一个空格。

正确写法:

function increment(uint x) public pure returns (uint) {
    return x + 1;
}

function increment(uint x) public pure onlyowner returns (uint) {
    return x + 1;
}

错误写法:

function increment(uint x) public pure returns (uint)
{
    return x + 1;
}

function increment(uint x) public pure returns (uint){
    return x + 1;
}

function increment(uint x) public pure returns (uint) {
    return x + 1;
    }

function increment(uint x) public pure returns (uint) {
    return x + 1;}

你应该严格地标示所有函数的可见性,包括构造函数。

Yes:

function explicitlyPublic(uint val) public {
    doSomething();
}

No:

function implicitlyPublic(uint val) {
    doSomething();
}

函数的可见性修饰符应该出现在任何自定义修饰符之前。

正确写法:

function kill() public onlyowner {
    selfdestruct(owner);
}

错误写法:

function kill() onlyowner public {
    selfdestruct(owner);
}

对于长函数声明,建议将每个参数独立一行并与函数体保持相同的缩进级别。闭括号和开括号也应该 独立一行并保持与函数声明相同的缩进级别。

正确写法:

function thisFunctionHasLotsOfArguments(
    address a,
    address b,
    address c,
    address d,
    address e,
    address f
)
    public
{
    doSomething();
}

错误写法:

function thisFunctionHasLotsOfArguments(address a, address b, address c,
    address d, address e, address f) public {
    doSomething();
}

function thisFunctionHasLotsOfArguments(address a,
                                        address b,
                                        address c,
                                        address d,
                                        address e,
                                        address f) public {
    doSomething();
}

function thisFunctionHasLotsOfArguments(
    address a,
    address b,
    address c,
    address d,
    address e,
    address f) public {
    doSomething();
}

如果一个长函数声明有修饰符,那么每个修饰符应该下沉到独立的一行。

正确写法:

function thisFunctionNameIsReallyLong(address x, address y, address z)
    public
    onlyowner
    priced
    returns (address)
{
    doSomething();
}

function thisFunctionNameIsReallyLong(
    address x,
    address y,
    address z,
)
    public
    onlyowner
    priced
    returns (address)
{
    doSomething();
}

错误写法:

function thisFunctionNameIsReallyLong(address x, address y, address z)
                                      public
                                      onlyowner
                                      priced
                                      returns (address) {
    doSomething();
}

function thisFunctionNameIsReallyLong(address x, address y, address z)
    public onlyowner priced returns (address)
{
    doSomething();
}

function thisFunctionNameIsReallyLong(address x, address y, address z)
    public
    onlyowner
    priced
    returns (address) {
    doSomething();
}

多行输出参数和返回值语句应该遵从 代码行的最大长度 一节的说明。

Yes:

function thisFunctionNameIsReallyLong(
    address a,
    address b,
    address c
)
    public
    returns (
        address someAddressName,
        uint256 LongArgument,
        uint256 Argument
    )
{
    doSomething()

    return (
        veryLongReturnArg1,
        veryLongReturnArg2,
        veryLongReturnArg3
    );
}

No:

function thisFunctionNameIsReallyLong(
    address a,
    address b,
    address c
)
    public
    returns (address someAddressName,
             uint256 LongArgument,
             uint256 Argument)
{
    doSomething()

    return (veryLongReturnArg1,
            veryLongReturnArg1,
            veryLongReturnArg1);
}

对于继承合约中需要参数的构造函数,如果函数声明很长或难以阅读,建议将基础构造函数像多个修饰符的风格那样 每个下沉到一个新行上书写。

正确写法:

contract A is B, C, D {
    function A(uint param1, uint param2, uint param3, uint param4, uint param5)
        B(param1)
        C(param2, param3)
        D(param4)
        public
    {
        // do something with param5
    }
}

错误写法:

contract A is B, C, D {
    function A(uint param1, uint param2, uint param3, uint param4, uint param5)
    B(param1)
    C(param2, param3)
    D(param4)
    public
    {
        // do something with param5
    }
}

contract A is B, C, D {
    function A(uint param1, uint param2, uint param3, uint param4, uint param5)
        B(param1)
        C(param2, param3)
        D(param4)
        public {
        // do something with param5
    }
}

当用单个语句声明简短函数时,允许在一行中完成。

允许:

function shortFunction() public { doSomething(); }

这些函数声明的准则旨在提高可读性。 因为本指南不会涵盖所有内容,作者应该自行作出最佳判断。

映射

待定

变量声明

数组变量的声明在变量类型和括号之间不应该有空格。

正确写法:

uint[] x;

错误写法:

uint [] x;

其他建议

  • 字符串应该用双引号而不是单引号。

正确写法:

str = "foo";
str = "Hamlet says, 'To be or not to be...'";

错误写法:

str = 'bar';
str = '"Be yourself; everyone else is already taken." -Oscar Wilde';
  • 操作符两边应该各有一个空格。

正确写法:

x = 3;
x = 100 / 10;
x += 3 + 4;
x |= y && z;

错误写法:

x=3;
x = 100/10;
x += 3+4;
x |= y&&z;
  • 为了表示优先级,高优先级操作符两边可以省略空格。这样可以提高复杂语句的可读性。你应该在操作符两边总是使用相同的空格数:

正确写法:

x = 2**3 + 5;
x = 2*y + 3*z;
x = (a+b) * (a-b);

错误写法:

x = 2** 3 + 5;
x = y+z;
x +=1;

命名规范

当完全采纳和使用命名规范时会产生强大的作用。 当使用不同的规范时,则不会立即获取代码中传达的重要 信息。

这里给出的命名建议旨在提高可读性,因此它们不是规则,而是透过名称来尝试和帮助传达最多的信息。

最后,基于代码库中的一致性,本文档中的任何规范总是可以被(代码库中的规范)取代。

命名方式

为了避免混淆,下面的名字用来指明不同的命名方式。

  • b (单个小写字母)
  • B (单个大写字母)
  • lowercase (小写)
  • lower_case_with_underscores (小写和下划线)
  • UPPERCASE (大写)
  • UPPER_CASE_WITH_UNDERSCORES (大写和下划线)
  • CapitalizedWords (驼峰式,首字母大写)
  • mixedCase (混合式,与驼峰式的区别在于首字母小写!)
  • Capitalized_Words_With_Underscores (首字母大写和下划线)
..注意:: 当在驼峰式命名中使用缩写时,应该将缩写中的所有字母都大写。 因此 HTTPServerError 比 HttpServerError 好。
当在混合式命名中使用缩写时,除了第一个缩写中的字母小写(如果它是整个名称的开头的话)以外,其他缩写中的字母均大写。 因此 xmlHTTPRequest 比 XMLHTTPRequest 更好。

应避免的名称

  • l - el的小写方式
  • O - oh的大写方式
  • I - eye的大写方式

切勿将任何这些用于单个字母的变量名称。 他们经常难以与数字 1 和 0 区分开。

合约和库名称

合约和库名称应该使用驼峰式风格。比如:SimpleTokenSmartBankCertificateHashRepositoryPlayer

结构体名称

结构体名称应该使用驼峰式风格。比如:MyCoinPositionPositionXY

事件名称

事件名称应该使用驼峰式风格。比如:DepositTransferApprovalBeforeTransferAfterTransfer

函数名称

函数名称不同于结构,应该使用混合式命名风格。比如:getBalancetransferverifyOwneraddMemberchangeOwner

函数参数命名

函数参数命名应该使用混合式命名风格。比如:initialSupplyaccountrecipientAddresssenderAddressnewOwner。 在编写操作自定义结构的库函数时,这个结构体应该作为函数的第一个参数,并且应该始终命名为 self

局部变量和状态变量名称

使用混合式命名风格。比如:totalSupplyremainingSupplybalancesOfcreatorAddressisPreSaletokenExchangeRate

常量命名

常量应该全都使用大写字母书写,并用下划线分割单词。比如:MAX_BLOCKSTOKEN_NAMETOKEN_TICKERCONTRACT_VERSION

修饰符命名

使用混合式命名风格。比如:onlyByonlyAfteronlyDuringThePreSale

枚举变量命名

在声明简单类型时,枚举应该使用驼峰式风格。比如:TokenGroupFrameHashStyleCharacterLocation

避免命名冲突

  • single_trailing_underscore_

当所起名称与内建或保留关键字相冲突时,建议照此惯例在名称后边添加下划线。

一般建议

待定

通用模式

从合约中提款

在某个操作之后发送资金的推荐方式是使用取回(withdrawal)模式。尽管在某个操作之后,最直接地发送以太币方法是一个 send 调用, 但这并不推荐;因为这会引入一个潜在的安全风险。你可能需要参考 安全考量 来获取更多信息。

这里是一个在合约中使用取回模式的示例,它目标是通过向合约发送最多的钱来成为“最富有的人”, 其灵感来自 King of the Ether

在下边的合约中,如果你的“最富有”位置被其他人取代,你可以收到取代你成为“最富有”的人发送到合约的资金。

pragma solidity ^0.4.11;

contract WithdrawalContract {
    address public richest;
    uint public mostSent;

    mapping (address => uint) pendingWithdrawals;

    function WithdrawalContract() public payable {
        richest = msg.sender;
        mostSent = msg.value;
    }

    function becomeRichest() public payable returns (bool) {
        if (msg.value > mostSent) {
            pendingWithdrawals[richest] += msg.value;
            richest = msg.sender;
            mostSent = msg.value;
            return true;
        } else {
            return false;
        }
    }

    function withdraw() public {
        uint amount = pendingWithdrawals[msg.sender];
        // 记住,在发送资金之前将待发金额清零
        // 来防止重入(re-entrancy)攻击
        pendingWithdrawals[msg.sender] = 0;
        msg.sender.transfer(amount);
    }
}

下面是一个相反的直接使用发送模式的例子:

pragma solidity ^0.4.11;

contract SendContract {
    address public richest;
    uint public mostSent;

    function SendContract() public payable {
        richest = msg.sender;
        mostSent = msg.value;
    }

    function becomeRichest() public payable returns (bool) {
        if (msg.value > mostSent) {
            // 这一行会导致问题(详见下文)
            richest.transfer(msg.value);
            richest = msg.sender;
            mostSent = msg.value;
            return true;
        } else {
            return false;
        }
    }
}

注意,在这个例子里,攻击者可以给这个合约设下陷阱,使其进入不可用状态,比如通过使一个 fallback 函数会失败的合约成为 richest (可以在 fallback 函数中调用 revert() 或者直接在 fallback 函数中使用超过 2300 gas 来使其执行失败)。这样,当这个合约调用 transfer 来给“下过毒”的合约 发送资金时,调用会失败,从而导致 becomeRichest 函数失败,这个合约也就被永远卡住了。

如果在合约中像第一个例子那样使用“取回(withdraw)”模式,那么攻击者只能使他/她自己的“取回”失败,并不会导致整个合约无法运作。

限制访问

限制访问是合约的一个通用模式。注意,你不可能限制任何人或机器读取你的交易内容或合约状态。 你可以通过加密使这种访问变得困难一些,但如果你想让你的合约读取这些数据,那么其他人也将可以做到。

你可以限制 其他合约 读取你的合约状态。 这(其他合约不能读取你的合约状态)是默认的,除非你将合约状态变量声明为 public

此外,你可以对谁可以修改你的合约状态或调用你的合约函数加以限制,这是本节要介绍的内容。

通过使用“函数 修饰器modifier”,可以使这些限制变得非常明确。

pragma solidity ^0.4.22;

contract AccessRestriction {
    // 这些将在构造阶段被赋值
    // 其中,`msg.sender` 是
    // 创建这个合约的账户。
    address public owner = msg.sender;
    uint public creationTime = now;

    // 修饰器可以用来更改
    // 一个函数的函数体。
    // 如果使用这个修饰器,
    // 它会预置一个检查,仅允许
    // 来自特定地址的
    // 函数调用。
    modifier onlyBy(address _account)
    {
        require(
            msg.sender == _account,
            "Sender not authorized."
        );
        // 不要忘记写 `_;`!
        // 它会被实际使用这个修饰器的
        // 函数体所替代。
        _;
    }

    // 使 `_newOwner` 成为这个合约的
    // 新所有者。
    function changeOwner(address _newOwner)
        public
        onlyBy(owner)
    {
        owner = _newOwner;
    }

    modifier onlyAfter(uint _time) {
        require(
            now >= _time,
            "Function called too early."
        );
        _;
    }

    // 抹掉所有者信息。
    // 仅允许在合约创建成功 6 周以后
    // 的时间被调用。
    function disown()
        public
        onlyBy(owner)
        onlyAfter(creationTime + 6 weeks)
    {
        delete owner;
    }

    // 这个修饰器要求对函数调用
    // 绑定一定的费用。
    // 如果调用方发送了过多的费用,
    // 他/她会得到退款,但需要先执行函数体。
    // 这在 0.4.0 版本以前的 Solidity 中很危险,
    // 因为很可能会跳过 `_;` 之后的代码。
    modifier costs(uint _amount) {
        require(
            msg.value >= _amount,
            "Not enough Ether provided."
        );
        _;
        if (msg.value > _amount)
            msg.sender.send(msg.value - _amount);
    }

    function forceOwnerChange(address _newOwner)
        public
        payable
        costs(200 ether)
    {
        owner = _newOwner;
        // 这只是示例条件
        if (uint(owner) & 0 == 1)
            // 这无法在 0.4.0 版本之前的
            // Solidity 上进行退还。
            return;
        // 退还多付的费用
    }
}

一个更专用地限制函数调用的方法将在下一个例子中介绍。

状态机

合约通常会像状态机那样运作,这意味着它们有特定的 阶段,使它们有不同的表现或者仅允许特定的不同函数被调用。 一个函数调用通常会结束一个阶段,并将合约转换到下一个阶段(特别是如果一个合约是以 交互 来建模的时候)。 通过达到特定的 时间 点来达到某些阶段也是很常见的。

一个典型的例子是盲拍(blind auction)合约,它起始于“接受盲目出价”, 然后转换到“公示出价”,最后结束于“确定拍卖结果”。

函数 修饰器modifier 可以用在这种情况下来对状态进行建模,并确保合约被正常的使用。

示例

在下边的示例中, 修饰器modifier atStage 确保了函数仅在特定的阶段才可以被调用。

根据时间来进行的自动阶段转换,是由 修饰器modifier timeTransitions 来处理的, 它应该用在所有函数上。

注解

修饰器modifier 的顺序非常重要。 如果 atStage 和 timedTransitions 要一起使用, 请确保在 timedTransitions 之后声明 atStage, 以便新的状态可以 首先被反映到账户中。

最后, 修饰器modifier transitionNext 能够用来在函数执行结束时自动转换到下一个阶段。

注解

修饰器modifier 可以被忽略。 以下特性仅在 0.4.0 版本之前的 Solidity 中有效: 由于 修饰器modifier 是通过简单的替换代码 而不是使用函数调用来提供的, 如果函数本身使用了 return,那么 transitionNext 修饰器modifier 的代码是可以被忽略的。 如果你希望这么做, 请确保你在这些函数中手工调用了 nextStage。 从 0.4.0 版本开始,即使函数明确地 return 了, 修饰器modifier 的代码也会执行。

pragma solidity ^0.4.22;

contract StateMachine {
    enum Stages {
        AcceptingBlindedBids,
        RevealBids,
        AnotherStage,
        AreWeDoneYet,
        Finished
    }

    // 这是当前阶段。
    Stages public stage = Stages.AcceptingBlindedBids;

    uint public creationTime = now;

    modifier atStage(Stages _stage) {
        require(
            stage == _stage,
            "Function cannot be called at this time."
        );
        _;
    }

    function nextStage() internal {
        stage = Stages(uint(stage) + 1);
    }

    // 执行基于时间的阶段转换。
    // 请确保首先声明这个修饰器,
    // 否则新阶段不会被带入账户。
    modifier timedTransitions() {
        if (stage == Stages.AcceptingBlindedBids &&
                    now >= creationTime + 10 days)
            nextStage();
        if (stage == Stages.RevealBids &&
                now >= creationTime + 12 days)
            nextStage();
        // 由交易触发的其他阶段转换
        _;
    }

    // 这里的修饰器顺序非常重要!
    function bid()
        public
        payable
        timedTransitions
        atStage(Stages.AcceptingBlindedBids)
    {
        // 我们不会在这里实现实际功能(因为这仅是个代码示例,译者注)
    }

    function reveal()
        public
        timedTransitions
        atStage(Stages.RevealBids)
    {
    }

    // 这个修饰器在函数执行结束之后
    // 使合约进入下一个阶段。
    modifier transitionNext()
    {
        _;
        nextStage();
    }

    function g()
        public
        timedTransitions
        atStage(Stages.AnotherStage)
        transitionNext
    {
    }

    function h()
        public
        timedTransitions
        atStage(Stages.AreWeDoneYet)
        transitionNext
    {
    }

    function i()
        public
        timedTransitions
        atStage(Stages.Finished)
    {
    }
}

已知bug列表

在下面,你可以找到一个 JSON 格式的列表,上面列出了 Solidity 编译器上一些已知的安全相关的 bug。 该文件被放置于 Github 仓库 。 该列表可以追溯到 0.3.0 版本,只在此版本之前存在的 bug 没有被列入。

这里,还有另外一个 bugs_by_version.json 文件。 该文件可用于查询特定的某个编译器版本会受哪些 bug 影响。

合约的源文件检查工具以及其他与合约交互的工具,需基于以下规则查阅上述 bug 列表文件:

  • 如果合约是用每日构建版本的编译器编译,而不是发布版本的编译器,那就有点可疑了。上述bug列表不跟踪未发布或每日构建版本的编译器。
  • 如果一个合约并不是由它被创建时点的最新版本编译器所编译的,那么这也是值得怀疑的。对于由其他合约创建的合约,您必须沿着创建链追溯最初交易,并使用该交易的日期作为创建日期。
  • 高度可疑的情况是,如果一份合约由一个包含已知 bug 的编译器编译,但在合约创建时,已修复了相应 bug 的新版编译器已经发布了。

下面这份包含已知 bug 的 JSON 文件实际上是一个对象数组,每个对象对应一个 bug,并包含以下的 keys :

name
赋予该 bug 的唯一的名字
summary
对该 bug 的简要描述
description
对该 bug 的详细描述
link
包含更多详尽信息的链接,可选
introduced
第一个包含该 bug 的编译器的发布版本,可选
fixed
第一个不再包含该 bug 的编译器的发布版本
publish
该 bug 被公开的日期,可选
severity
bug 的严重性: very low, low, medium, high。综合考虑了在合约测试中的可发现性、发生的可能性和被利用后的潜在损害。
conditions
触发该 bug 所需满足的条件。当前,这是一个包含了 optimizer 布尔值的对象,这意味着只有打开优化器选项时,才会触发该 bug。 如果没有给出任何条件,则意味着此 bug 始终存在。
[
    {
        "name": "OneOfTwoConstructorsSkipped",
        "summary": "If a contract has both a new-style constructor (using the constructor keyword) and an old-style constructor (a function with the same name as the contract) at the same time, one of them will be ignored.",
        "description": "If a contract has both a new-style constructor (using the constructor keyword) and an old-style constructor (a function with the same name as the contract) at the same time, one of them will be ignored. There will be a compiler warning about the old-style constructor, so contracts only using new-style constructors are fine.",
        "introduced": "0.4.22",
        "fixed": "0.4.23",
        "severity": "very low"
    },
    {
        "name": "ZeroFunctionSelector",
        "summary": "It is possible to craft the name of a function such that it is executed instead of the fallback function in very specific circumstances.",
        "description": "If a function has a selector consisting only of zeros, is payable and part of a contract that does not have a fallback function and at most five external functions in total, this function is called instead of the fallback function if Ether is sent to the contract without data.",
        "fixed": "0.4.18",
        "severity": "very low"
    },
    {
        "name": "DelegateCallReturnValue",
        "summary": "The low-level .delegatecall() does not return the execution outcome, but converts the value returned by the functioned called to a boolean instead.",
        "description": "The return value of the low-level .delegatecall() function is taken from a position in memory, where the call data or the return data resides. This value is interpreted as a boolean and put onto the stack. This means if the called function returns at least 32 zero bytes, .delegatecall() returns false even if the call was successuful.",
        "introduced": "0.3.0",
        "fixed": "0.4.15",
        "severity": "low"
    },
    {
        "name": "ECRecoverMalformedInput",
        "summary": "The ecrecover() builtin can return garbage for malformed input.",
        "description": "The ecrecover precompile does not properly signal failure for malformed input (especially in the 'v' argument) and thus the Solidity function can return data that was previously present in the return area in memory.",
        "fixed": "0.4.14",
        "severity": "medium"
    },
    {
        "name": "SkipEmptyStringLiteral",
        "summary": "If \"\" is used in a function call, the following function arguments will not be correctly passed to the function.",
        "description": "If the empty string literal \"\" is used as an argument in a function call, it is skipped by the encoder. This has the effect that the encoding of all arguments following this is shifted left by 32 bytes and thus the function call data is corrupted.",
        "fixed": "0.4.12",
        "severity": "low"
    },
    {
        "name": "ConstantOptimizerSubtraction",
        "summary": "In some situations, the optimizer replaces certain numbers in the code with routines that compute different numbers.",
        "description": "The optimizer tries to represent any number in the bytecode by routines that compute them with less gas. For some special numbers, an incorrect routine is generated. This could allow an attacker to e.g. trick victims about a specific amount of ether, or function calls to call different functions (or none at all).",
        "link": "https://blog.ethereum.org/2017/05/03/solidity-optimizer-bug/",
        "fixed": "0.4.11",
        "severity": "low",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "name": "IdentityPrecompileReturnIgnored",
        "summary": "Failure of the identity precompile was ignored.",
        "description": "Calls to the identity contract, which is used for copying memory, ignored its return value. On the public chain, calls to the identity precompile can be made in a way that they never fail, but this might be different on private chains.",
        "severity": "low",
        "fixed": "0.4.7"
    },
    {
        "name": "OptimizerStateKnowledgeNotResetForJumpdest",
        "summary": "The optimizer did not properly reset its internal state at jump destinations, which could lead to data corruption.",
        "description": "The optimizer performs symbolic execution at certain stages. At jump destinations, multiple code paths join and thus it has to compute a common state from the incoming edges. Computing this common state was simplified to just use the empty state, but this implementation was not done properly. This bug can cause data corruption.",
        "severity": "medium",
        "introduced": "0.4.5",
        "fixed": "0.4.6",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "name": "HighOrderByteCleanStorage",
        "summary": "For short types, the high order bytes were not cleaned properly and could overwrite existing data.",
        "description": "Types shorter than 32 bytes are packed together into the same 32 byte storage slot, but storage writes always write 32 bytes. For some types, the higher order bytes were not cleaned properly, which made it sometimes possible to overwrite a variable in storage when writing to another one.",
        "link": "https://blog.ethereum.org/2016/11/01/security-alert-solidity-variables-can-overwritten-storage/",
        "severity": "high",
        "introduced": "0.1.6",
        "fixed": "0.4.4"
    },
    {
        "name": "OptimizerStaleKnowledgeAboutSHA3",
        "summary": "The optimizer did not properly reset its knowledge about SHA3 operations resulting in some hashes (also used for storage variable positions) not being calculated correctly.",
        "description": "The optimizer performs symbolic execution in order to save re-evaluating expressions whose value is already known. This knowledge was not properly reset across control flow paths and thus the optimizer sometimes thought that the result of a SHA3 operation is already present on the stack. This could result in data corruption by accessing the wrong storage slot.",
        "severity": "medium",
        "fixed": "0.4.3",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "name": "LibrariesNotCallableFromPayableFunctions",
        "summary": "Library functions threw an exception when called from a call that received Ether.",
        "description": "Library functions are protected against sending them Ether through a call. Since the DELEGATECALL opcode forwards the information about how much Ether was sent with a call, the library function incorrectly assumed that Ether was sent to the library and threw an exception.",
        "severity": "low",
        "introduced": "0.4.0",
        "fixed": "0.4.2"
    },
    {
        "name": "SendFailsForZeroEther",
        "summary": "The send function did not provide enough gas to the recipient if no Ether was sent with it.",
        "description": "The recipient of an Ether transfer automatically receives a certain amount of gas from the EVM to handle the transfer. In the case of a zero-transfer, this gas is not provided which causes the recipient to throw an exception.",
        "severity": "low",
        "fixed": "0.4.0"
    },
    {
        "name": "DynamicAllocationInfiniteLoop",
        "summary": "Dynamic allocation of an empty memory array caused an infinite loop and thus an exception.",
        "description": "Memory arrays can be created provided a length. If this length is zero, code was generated that did not terminate and thus consumed all gas.",
        "severity": "low",
        "fixed": "0.3.6"
    },
    {
        "name": "OptimizerClearStateOnCodePathJoin",
        "summary": "The optimizer did not properly reset its internal state at jump destinations, which could lead to data corruption.",
        "description": "The optimizer performs symbolic execution at certain stages. At jump destinations, multiple code paths join and thus it has to compute a common state from the incoming edges. Computing this common state was not done correctly. This bug can cause data corruption, but it is probably quite hard to use for targeted attacks.",
        "severity": "low",
        "fixed": "0.3.6",
        "conditions": {
            "optimizer": true
        }
    },
    {
        "name": "CleanBytesHigherOrderBits",
        "summary": "The higher order bits of short bytesNN types were not cleaned before comparison.",
        "description": "Two variables of type bytesNN were considered different if their higher order bits, which are not part of the actual value, were different. An attacker might use this to reach seemingly unreachable code paths by providing incorrectly formatted input data.",
        "severity": "medium/high",
        "fixed": "0.3.3"
    },
    {
        "name": "ArrayAccessCleanHigherOrderBits",
        "summary": "Access to array elements for arrays of types with less than 32 bytes did not correctly clean the higher order bits, causing corruption in other array elements.",
        "description": "Multiple elements of an array of values that are shorter than 17 bytes are packed into the same storage slot. Writing to a single element of such an array did not properly clean the higher order bytes and thus could lead to data corruption.",
        "severity": "medium/high",
        "fixed": "0.3.1"
    },
    {
        "name": "AncientCompiler",
        "summary": "This compiler version is ancient and might contain several undocumented or undiscovered bugs.",
        "description": "The list of bugs is only kept for compiler versions starting from 0.3.0, so older versions might contain undocumented bugs.",
        "severity": "high",
        "fixed": "0.3.0"
    }
]

贡献方式

对于大家的帮助,我们一如既往地感激。

你可以试着 从源代码编译 开始,以熟悉 Solidity 的组件和编译流程。这对精通 Solidity 上智能合约的编写也有帮助。

我们特别需要以下方面的帮助:

怎样报告问题

请用 GitHub issues tracker 来报告问题。汇报问题时,请提供下列细节:

  • 你所使用的 Solidity 版本
  • 源码(如果可以的话)
  • 你在哪个平台上运行代码
  • 如何重现该问题
  • 该问题的结果是什么
  • 预期行为是什么样的

将造成问题的源码缩减到最少,总是很有帮助的,并且有时候甚至能澄清误解。

Pull Request 的工作流

为了进行贡献,请 fork 一个 develop 分支并在那里进行修改。除了你 做了什么 之外,你还需要在 commit 信息中说明,你 为什么 做这些修改(除非只是个微小的改动)。

在进行了 fork 之后,如果你还需要从 develop 分支 pull 任何变更的话(例如,为了解决潜在的合并冲突),请避免使用 git merge ,而是 git rebase 你的分支。

此外,如果你在编写一个新功能,请确保你编写了合适的 Boost 测试案例,并将他们放在了 test/ 下。

但是,如果你在进行一个更大的变更,请先与 Solidity Development Gitter channel 进行商量(与上文提到的那个功能不同,这个变更侧重于编译器和编程语言开发,而不是编程语言的使用)。

新的特性和 bug 修复会被添加到 Changelog.md 文件中:使用的时候请遵循上述方式。

最后,请确保你遵守了这个项目的 编码风格 。还有,虽然我们采用了持续集成测试,但是在提交 pull request 之前,请测试你的代码并确保它能在本地进行编译。

感谢你的帮助!

运行编译器测试

Solidity 有不同类型的测试,他们包含在应用 soltest 中。其中一些需要 cpp-ethereum 客户端运行在测试模式下,另一些需要安装 libz3

soltest 会从保存在 ./test/libsolidity/syntaxTests 中的测试合约中获取所期待的结果。为了使 soltest 可以找到这些测试,可以使用 --testpath 命令行参数来指定测试根目录,例如 ./build/test/soltest -- --testpath ./test

若要禁用 z3 测试,可使用 ./build/test/soltest -- --no-smt --testpath ./test ,若要执行不需要 cpp-ethereum 的测试子集,则用 ./build/test/soltest -- --no-ipc --testpath ./test

对于其他测试,你都需要安装 cpp-ethereum ,并在测试模式下运行它:eth --test -d /tmp/testeth

之后再执行实际的测试文件:./build/test/soltest -- --ipcpath /tmp/testeth/geth.ipc --testpath ./test

可以用过滤器来执行一组测试子集:soltest -t TestSuite/TestName -- --ipcpath /tmp/testeth/geth.ipc --testpath ./test,其中 TestName 可以是通配符 *

另外, scripts/test.sh 里有一个测试脚本可执行所有测试,并自动运行 cpp-ethereum,如果它在 scripts 路径中的话(但不会去下载它)。

Travis CI 甚至会执行一些额外的测试(包括 solc-js 和对第三方 Solidity 框架的测试),这些测试需要去编译 Emscripten 目标代码。

编写和运行语法测试

就像前文提到的,语法测试存储在单独的合约里。这些文件必须包含注解,为相关的测试标注预想的结果。测试工具将编译并基于给定的预想结果进行检查。

例如:./test/libsolidity/syntaxTests/double_stateVariable_declaration.sol

contract test {
    uint256 variable;
    uint128 variable;
}
// ----
// DeclarationError: Identifier already declared.

一个语法测试必须在合约代码之后包含跟在分隔符 ---- 之后的测试代码。上边例子中额外的注释则用来描述预想的编译错误或警告。如果合约不会出现编译错误或警告,这部分可以为空。

在上边的例子里,状态变量 variable 被声明了两次,这是不允许的。这会导致一个 DeclarationError 来告知标识符已经被声明过了。

用来进行那些测试的工具叫做 isoltest,可以在 ./test/tools/ 下找到。它是一个交互工具,允许你使用你喜欢的文本编辑器编辑失败的合约。让我们把第二个 variable 的声明去掉来使测试失败:

contract test {
    uint256 variable;
}
// ----
// DeclarationError: Identifier already declared.

再次运行 ./test/isoltest 就会得到一个失败的测试:

syntaxTests/double_stateVariable_declaration.sol: FAIL
    Contract:
        contract test {
            uint256 variable;
        }

    Expected result:
        DeclarationError: Identifier already declared.
    Obtained result:
        Success

这里,在获得了结果之后打印了预想的结果,但也提供了编辑/更新/跳过当前合约或直接退出的办法,isoltest 提供了下列测试失败选项:

  • edit:isoltest 会尝试打开先前用 isoltest --editor /path/to/editor 所指定的编辑器。如果没设定路径,则会产生一个运行时错误。如果指定了编辑器,这将打开编辑器并允许你修改合约代码。
  • update:更新测试中的合约。这将会移除包含了不匹配异常的注解,或者增加缺失的预想结果。然后测试会重新开始。
  • skip:跳过当前测试的执行。
  • quit:退出 isoltest

在上边的情况自动更新合约会把它变为:

contract test {
    uint256 variable;
}
// ----

并重新运行测试。它将会通过:

Re-running test case...
syntaxTests/double_stateVariable_declaration.sol: OK

注解

请为合约文件取个名字,它应该是可以自我解释正在测试什么的那种名字,例如 double_variable_declaration.sol。不要在一个文件中放多个合约,isoltest 目前无法分别识别它们。

通过 AFL 运行 Fuzzer

Fuzzing 是一种测试技术,它可以通过运行多少不等的随机输入来找出异常的执行状态(片段故障、异常等等)。现代的 fuzzer 已经可以很聪明地在输入中进行直接的查询。 我们有一个专门的程序叫做 solfuzzer,它可以将源代码作为输入,当发生一个内部编译错误、片段故障或者类似的错误时失败,但当代码包含错误的时候则不会失败。 通过这种方法,fuzzing 工具可以找到那些编译级别的内部错误。

我们主要使用 AFL 来进行 fuzzing 测试。你需要手工下载和构建 AFL。然后用 AFL 作为编译器来构建 Solidity(或直接构建 solfuzzer):

cd build
# if needed
make clean
cmake .. -DCMAKE_C_COMPILER=path/to/afl-gcc -DCMAKE_CXX_COMPILER=path/to/afl-g++
make solfuzzer

然后,你需要一个源文件例子。这将使 fuzzer 可以更容易地找到错误。你可以从语法测试目录下拷贝一些文件或者从文档中提取一些测试文件或其他测试:

mkdir /tmp/test_cases
cd /tmp/test_cases
# extract from tests:
path/to/solidity/scripts/isolate_tests.py path/to/solidity/test/libsolidity/SolidityEndToEndTest.cpp
# extract from documentation:
path/to/solidity/scripts/isolate_tests.py path/to/solidity/docs docs

AFL 的文档指出,账册(初始的输入文件)不应该太大。每个文件本身不应该超过 1 kB,并且每个功能最多只能有一个输入文件;所以最好从少量的输入文件开始。 此外还有一个叫做 afl-cmin 的工具,可以将输入文件整理为可以具有近似行为的二进制代码。

现在运行 fuzzer(-m 参数将使用的内存大小扩展为 60 MB):

afl-fuzz -m 60 -i /tmp/test_cases -o /tmp/fuzzer_reports -- /path/to/solfuzzer

fuzzer 会将导致失败的源文件创建在 /tmp/fuzzer_reports 中。通常它会找到产生相似错误的类似的源文件。 你可以使用 scripts/uniqueErrors.sh 工具来过滤重复的错误。

Whiskers 模板系统

Whiskers 是一个类似于 Mustache 的模板系统。编译器在各种各样的地方使用 Whiskers 来增强可读性,从而提高代码的可维护性和可验证性。

它的语法与 Mustache 有很大差别:模板标记 {{}} 被替换成了 <> ,以便增强语法分析,避免与 内联汇编 的冲突(符号 <> 在内联汇编中是无效的,而 {} 则被用来限定块)。另一个局限是,列表只会被解析一层,而不是递归解析。未来可能会改变这一个限制。

下面是一个粗略的说明:

任何出现 <name> 的地方都会被所提供的变量 name 的字符串值所替换,既不会进行任何转义也不会迭代替换。可以通过 <#name>...</name> 来限定一个区域。该区域中的内容将进行多次拼接,每次拼接会使用相应变量集中的值替换区域中的 <inner> 项,模板系统中提供了多少组变量集,就会进行多少次拼接。顶层变量也可以在这样的区域的内部使用。

译者注:对于区域<#name>...</name>的释义,译者参考自:https://github.com/janl/mustache.js#sections

常见问题

这份清单最早是由 fivedogit 收集整理的。

基本问题

可以在特定的区块上进行操作吗?(比如发布一个合约或执行一笔交易)

鉴于交易数据的写入是由矿工决定的而不是由提交者决定的,谁也无法保证交易一定会发生在下一个或未来某一个特定的区块上。这个结论适用于函数调用/交易以及合约的创建。

如果你希望你的合约被定时调用,可以使用:alarm clock

什么是交易的“有效载荷(payload)”?

就是随交易一起发送的字节码“数据”。

存在反编译器吗?

除了 Porosity 有点接近之外,Solidity 没有严格意义上的反编译器。由于诸如变量名、注释、代码格式等会在编译过程中丢失,所以完全反编译回源代码是没有可能的。

很多区块链浏览器都能将字节码分解为一系列操作码。

如果区块链上的合约会被第三方使用,那么最好将源代码一起进行发布。

创建一个可以被中止并退款的合约

首先,需要提醒一下:中止合约听起来是一个好主意,把垃圾打扫干净是个好习惯,但如上所述,合约是不会被真正清理干净的。甚至,被发送至已移除合约的以太币,会从此丢失。

如果想让合约不再可用,建议的做法是修改合约内部状态来使其 失效 ,让所有函数调用都变为无效返回。这样就无法使用这份合约了,而且发送过去的以太币也会被自动退回。

现在正式回答这个问题:在构造函数中,将 creator 赋值为 msg.sender ,并保存。然后调用 selfdestruct(creator); 来中止程序并进行退款。

例子

需要注意的是,如果你已经在合约顶部做了引用 import "mortal" 并且声明了 contract SomeContract is mortal { ... ,然后再在已存在此合约的编译器中进行编译(包含 Remix),那么 kill() 就会自动执行。当一份合约被声明为 mortal 时,你可以仿照我的例子,使用 contractname.kill.sendTransaction({from:eth.coinbase}) 来中止它。

调用 Solidity 方法可以返回一个数组或字符串(string)吗?

可以。参考 array_receiver_and_returner.sol

但是,在 Solidity内部 调用一个函数并返回变长数据(例如 uint[] 这种变长数组)时,往往会出现问题。这是 以太坊虚拟机Ethereum Virtual Machine(EVM) 自身的限制,我们已经计划在下一次协议升级时解决这个问题。

将变长数据作为外部交易或调用的一部分返回是没问题的。

数组可以使用 in-line 的方式(指在声明变量的同一个语句中)来初始化吗?比如: string[] myarray = ["a", "b"];

可以。然而需要注意的是,这方法现在只能用于定长 内存memory 数组。你甚至可以在返回语句中用 in-line 的方式新建一个 内存memory 数组。听起来很酷,对吧!

例子:

pragma solidity ^0.4.16;

contract C {
    function f() public pure returns (uint8[5]) {
        string[4] memory adaArr = ["This", "is", "an", "array"];
        return ([1, 2, 3, 4, 5]);
    }
}

合约的函数可以返回结构(struct)吗?

可以,但只适用于内部(internal)函数调用。

我从一个返回的枚举类型(enum)中,使用 web3.js 只得到了整数值。我该如何获取具名数值?

虽然 Solidity 支持枚举类型,但 ABI(应用程序二进制接口)并不支持。当前阶段你需要自己去做映射,将来我们可能会提供一些帮助。

可以使用 in-line 的方式来初始化状态变量吗?

可以,所有类型都可以(甚至包括结构)。然而需要注意的是,在数组使用这个方法的时候需要将其定义为静态 内存memory 数组。

例子:

pragma solidity ^0.4.0;

contract C {
    struct S {
        uint a;
        uint b;
    }

    S public x = S(1, 2);
    string name = "Ada";
    string[4] adaArr = ["This", "is", "an", "array"];
}

contract D {
    C c = new C();
}

结构(structs)如何使用?

参考 struct_and_for_loop_tester.sol

循环(for loops)如何使用?

和 JavaScript 非常相像。但有一点需要注意:

如果你使用 for (var i = 0; i < a.length; i ++) { a[i] = i; } ,那么 i 的数据类型将会是 uint8,需要从 0 开始计数。也就是说,如果 a 有超过 255 个元素,那么循环就无法中止,因为 i 最大只能变为 255

最好使用 for (uint i = 0; i < a.length...

参考 struct_and_for_loop_tester.sol

有没有一些简单的操作字符串的例子(substringindexOfcharAt 等)?

这里有一些字符串相关的功能性函数 stringUtils.sol ,并且会在将来作扩展。另外,Arachnid 有写过 solidity-stringutils

当前,如果你想修改一个字符串(甚至你只是想获取其长度),首先都必须将其转化为一个 bytes

pragma solidity ^0.4.0;

contract C {
    string s;

    function append(byte c) public {
        bytes(s).push(c);
    }

    function set(uint i, byte c) public {
        bytes(s)[i] = c;
    }
}

我能拼接两个字符串吗?

目前只能通过手工实现。

为什么大家都选择将合约实例化成一个变量(ContractB b;),然后去执行变量的函数(b.doSomething();),而不是直接调用这个 低级函数low-level function .call()

如果你调用实际的成员函数,编译器会提示诸如参数类型不匹配的问题,如果函数不存在或者不可见,他也会自动帮你打包参数。

参考 ping.solpong.sol

没被使用的 gas 会被自动退回吗?

是的,马上会退回。也就是说,作为交易的一部分,在交易完成的同时完成退款。

当返回一个值的时候,比如说 uint 类型的值, 可以返回一个 undefined 或者类似 null 的值吗?

这不可能,因为所有的数据类型已经覆盖了全部的取值范围。

替代方案是可以在错误时抛出(throw),这同样能复原整个交易,当你遇到意外情况时不失为一个好的选择。

如果你不想抛出,也可以返回一对(a pair)值

pragma solidity >0.4.23 <0.5.0;

contract C {
    uint[] counters;

    function getCounter(uint index)
        public
        view
        returns (uint counter, bool error) {
            if (index >= counters.length)
                return (0, true);
            else
                return (counters[index], false);
    }

    function checkCounter(uint index) public view {
        (uint counter, bool error) = getCounter(index);
        if (error) {
            // ...
        } else {
            // ...
        }
    }
}

注释会被包含在已部署的合约里吗,而且会增加部署的 gas 吗?

不会,所有执行时非必须的内容都会在编译的时候被移除。 其中就包括注释、变量名和类型名。

如果在调用合约的函数时一起发送了以太币,将会发生什么?

就像在创建合约时发送以太币一样,会累加到合约的余额总数上。 你只可以将以太币一起发送至拥有 payable 修饰符的函数,不然会抛出异常。

合约对合约的交易可以获得交易回执吗?

不能,合约对合约的函数调用并不会创建前者自己的交易,你必须要去查看全部的交易。这也是为什么很多区块浏览器无法正确显示合约对合约发送的以太币。

关键字 memory 是什么?是用来做什么的?

以太坊虚拟机Ethereum Virtual Machine(EVM) 拥有三类存储区域。

第一类是 存储storage,贮存了合约声明中所有的变量。 虚拟机会为每份合约分别划出一片独立的 存储storage 区域,并在函数相互调用时持久存在,所以其使用开销非常大。

第二类是 内存memory,用于暂存数据。其中存储的内容会在函数被调用(包括外部函数)时擦除,所以其使用开销相对较小。

第三类是栈,用于存放小型的局部变量。使用几乎是免费的,但容量有限。

对绝大部分数据类型来说,由于每次被使用时都会被复制,所以你无法指定将其存储在哪里。

在数据类型中,对所谓存储地点比较重视的是结构和数组。 如果你在函数调用中传递了这类变量,假设它们的数据可以被贮存在 存储storage内存memory 中,那么它们将不会被复制。也就是说,当你在被调用函数中修改了它们的内容,这些修改对调用者也是可见的。

不同数据类型的变量会有各自默认的存储地点:

  • 状态变量总是会贮存在 存储storage
  • 函数参数默认存放在 内存memory
  • 结构、数组或映射类型的局部变量,默认会放在 存储storage
  • 除结构、数组及映射类型之外的局部变量,会储存在栈中

例子:

pragma solidity ^0.4.0;

contract C {
    uint[] data1;
    uint[] data2;

    function appendOne() public {
        append(data1);
    }

    function appendTwo() public {
        append(data2);
    }

    function append(uint[] storage d) internal {
        d.push(1);
    }
}

函数 append 能一起作用于 data1data2,并且修改是永久保存的。如果你移除了 storage 关键字,函数的参数会默认存储于 memory。这带来的影响是,在 append(data1)append(data2) 被调用的时候,一份全新的状态变量的拷贝会在 内存memory 中被创建,append 操作的会是这份拷贝(也不支持 .push ——但这又是另一个话题了)。针对这份全新的拷贝的修改,不会反过来影响 data1data2

一个常见误区就是声明了一个局部变量,就认为它会创建在 内存memory 中,其实它会被创建在 存储storage 中:

/// 这份合约包含一处错误

pragma solidity ^0.4.0;

contract C {
    uint someVariable;
    uint[] data;

    function f() public {
        uint[] x;
        x.push(2);
        data = x;
    }
}

局部变量 x 的数据类型是 uint[] storage,但由于 存储storage 不是动态分配的,它需要在使用前通过状态变量赋值。所以 x 本身不会被分配 存储storage 的空间,取而代之的是,它只是作为 存储storage 中已有变量的别名。

实际上会发生的是,编译器将 x 解析为一个 存储storage 指针,并默认将指针指向 存储插槽storage slot 0 。这就造成 someVariable (贮存在 存储插槽storage slot 0)会被 x.push(2) 更改。(在本例中,两个合约变量 someVariable 和 data 会被预先分配到两个 存储插槽storage slot 中,即 存储插槽storage slot 0存储插槽storage slot 1 。上面的程序会使局部变量 x 变成指向保存了变量 someVariable 的 存储插槽storage slot 0 的指针。译者注。)

正确的方法如下:

pragma solidity ^0.4.0;

contract C {
    uint someVariable;
    uint[] data;

    function f() public {
        uint[] x = data;
        x.push(2);
    }
}

高级问题

怎样才能在合约中获取一个随机数?(实施一份自动回款的博彩合约)

做好随机这件事情,往往是一个加密项目最关键的部分,大部分的失败都来自于使用了低劣的随机数发生器。

如果你不考虑安全性,可以做一个类似于 coin flipper 的东西,反之,最好调用一份可以提供随机性的合约,比如 RANDAO

从另一份合约中的 non-constant 函数获取返回值

关键点是调用者(合约)需要了解将被调用的函数。

参考 ping.solpong.sol

让合约在首次被挖出时就开始做些事情

使用构造函数。在构造函数中写的任何内容都会在首次被挖出时执行。

参考 replicator.sol

怎样才能创建二维数组?

参考 2D_array.sol

需要注意的是,用 uint8 类型的数据填满一个 10x10 的方阵,再加上合约创建,总共需要花费超过 800,000 的 gas。如果是 17x17 需要 2,000,000 的 gas。然而交易的 gas 上限是 314 万。。。好吧,其实你也玩不了太大的花样。

注意,“创建”数组纯粹是免费的,成本在于填充数组。

还需注意,优化 存储storage 访问可以大大降低 gas 的花费,因为一个 存储插槽storage slot 可以存放下 32 个 uint8 类型的值。但这类优化目前也存在一些问题:在跨循环的时候不起作用;以及在边界检查时候会出问题。当然,在未来这种情况会得到改观。

当我们复制一个结构(struct)时, 结构 (struct)中定义的映射会被怎么处理?

这是一个非常有意思的问题。假设我们有一份合约,里面的字段设置如下:

struct User {
    mapping(string => string) comments;
}

function somefunction public {
   User user1;
   user1.comments["Hello"] = "World";
   User user2 = user1;
}

在这种情况下,由于缺失“被映射的键列表”,被复制至 userList 的结构中的映射会被忽视。因此,系统无法找出什么值可以被复制过去。

我应该如何初始化一份只包含指定数量 wei 的合约?

目前实现方式不是太优雅,当然暂时也没有更好的方法。 就拿 合约A 调用一个 合约B 的新实例来说,new B 周围必须要加括号,不然 B.value 会被认作是 B 的一个成员函数,叫做 value。 你必须确保两份合约都知道对方的存在,并且 合约B 拥有 payable 构造函数。

就是这个例子:

pragma solidity ^0.4.0;

contract B {
    function B() public payable {}
}

contract A {
    address child;

    function test() public {
        child = (new B).value(10)(); //construct a new B with 10 wei
    }
}

合约的函数可以接收二维数组吗?

二维数组还无法使用于外部调用和动态数组——你只能使用一维的动态数组。

bytes32string 有什么关系吗?为什么 bytes32 somevar = "stringliteral"; 可以生效,还有保存下来的那个 32 字节的 16 进制数值有什么含义吗?

数据类型 bytes32 可以存放 32 个(原始)字节。在给变量分配值的过程中 bytes32 samevar = "stringliteral";, 字符串已经被逐字翻译成了原始字节。如果你去检查 somevar ,会发现一个 32 字节的 16 进制数值,这就是用 16 进制表示的 字符串的文字

数据类型 bytes 与此类似,只是它的长度可以改变。

最终来看,假设 bytes 储存的是字符串的 UTF-8 编码,那么它和 string 基本是等同的。由于 string 存储storage 的是 UTF-8 编码格式的数据,所以计算字符串中字符数量的成本是很高的(某些字符的编码甚至大于一个字节)。因此,系统还不支持 string s; s.length ,甚至不能通过索引访问 s[2] 。但如果你想访问字符串的下级字节编码,可以使用 bytes(s).lengthbytes(s)[2],它们分别会返回字符串在 UTF-8 编码下的字节数量(不是字符数量)以及字符串 UTF-8 编码的第二个字节(不是字符)。

一份合约可以传递一个数组(固定长度)或者一个字符串或者一个 bytes (不定长度)给另一份合约吗?

当然可以。但如果不小心跨越了 内存memory / 存储storage 的边界,一份独立的拷贝就会被创建出来:

pragma solidity ^0.4.16;

contract C {
    uint[20] x;

    function f() public {
        g(x);
        h(x);
    }

    function g(uint[20] y) internal pure {
        y[2] = 3;
    }

    function h(uint[20] storage y) internal {
        y[3] = 4;
    }
}

由于会在 内存memory 中对 存储storage 的值创建一份独立的拷贝(默认存储在 内存memory 中),所以对 g(x) 的调用其实并不会对 x 产生影响。另一方面,由于传递的只是引用而不是一个拷贝, h(x) 得以成功地修改了 x

有些时候,当我想用类似这样的表达式: arrayname.length = 7; 来修改数组长度,却会得到一个编译错误 Value must be an lvalue。这是为什么?

你可以使用 arrayname.length = <some new length>; 来调整 存储storage 中的动态数组(也就是在合约级别声明的数组)的长度。如果你得到一个 lvalue 错误,那么你有可能做错了以下两件事中的一件或全部。

  1. 你在尝试修改长度的数组可能是保存在 内存memory 中的,或者
  2. 你可能在尝试修改一个非动态数组的长度。
// 这将无法编译通过

pragma solidity ^0.4.18;

contract C {
    int8[] dynamicStorageArray;
    int8[5] fixedStorageArray;

    function f() {
        int8[] memory memArr;        // 第一种情况
        memArr.length++;             // 非法

        int8[5] storage storageArr = fixedStorageArray;   // 第二种情况
        storageArr.length++;                             // 非法

        int8[] storage storageArr2 = dynamicStorageArray;
        storageArr2.length++;                     // 非法


    }
}

重要提醒: 在 Solidity 中,数组维数的声明方向是和在 C 或 Java 中的声明方向相反的,但访问方式相同。

举个例子,int8[][5] somearray; 是5个 int8 格式的动态数组。

这么做的原因是,T[5] 总是能被识别为5个 T 的数组,哪怕 T 本身就是一个数组(而在 C 或 Java 是不一样的)。

Solidity 的函数可以返回一个字符串数组吗(string[])?

暂时还不可以,因为这要求两个维度都是动态数组(string 本身就是一种动态数组)。

如果你发起了一次获取数组的调用,有可能获得整个数组吗?还是说另外需要写一个辅助函数来实现?

一个数组类型的公共状态变量会有一个自动的获取函数 getter function , 这个函数只会返回单个元素。如果你想获取完整的数组,那么只能再手工写一个函数来实现。

如果某个账户只存储了值但没有任何代码,将会发生什么?例子: http://test.ether.camp/account/5f740b3a43fbb99724ce93a879805f4dc89178b5

构造函数做的最后一件事情是返回合约的代码。这件事消耗的 gas 取决于代码的长度,其中有种可能的情况是提供的 gas 不够。这是唯一的一种情况下,出现了 “out of gas” 异常却不会去复原改变了的状态,这个改变在这里就是对状态变量的初始化。

https://github.com/ethereum/wiki/wiki/Subtleties

当 CREATE 操作的某个阶段被成功执行,如果这个操作返回 x,那么 5 * len(x) 的 gas 在合约被创建前会从剩余 gas 中被扣除。如果剩余的 gas 少于 5 * len(x),那么就不进行 gas 扣除,而是把创建的合约代码改变成空字符串,但这时候并不认为是发生了异常——不会发生复原。

在定制 通证token 的合约中,下面这些奇怪的校验是做什么的?

require((balanceOf[_to] + _value) >= balanceOf[_to]);

在Solidity(以及大多数其他机器相关的编程语言)中的整型都会被限定在一定范围内。 比如 uint256 ,就是从 02**256 - 1 。如果针对这些数字进行操作的结果不在这个范围内,那么就会被截断。这些截断会带来 严重的后果 ,所以像上面这样的代码需要考虑避免此类攻击。

更多问题?

如果你有其他问题,或者你的问题在这里找不到答案,请在此联系我们 gitter 或者提交一个 issue