天天看点

以太坊:深入理解Solidity-合约

合约

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

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

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

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

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

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

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

<code>external</code> :

外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 <code>f</code>不能从内部调用(即 <code>f</code> 不起作用,但 <code>this.f()</code> 可以)。 当收到大量数据的时候,外部函数有时候会更有效率。

<code>public</code> :

public 函数是合约接口的一部分,可以在内部或通过消息调用。对于公共状态变量, 会自动生成一个 getter 函数(见下面)。

<code>internal</code> :

这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 <code>this</code> 调用。

<code>private</code> :

private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

注解

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

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

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

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

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

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

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

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

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

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

警告

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

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

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

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

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

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

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

As in JavaScript, functions may take parameters as input. Unlike in JavaScript and C, functions may also return an arbitrary number of values as output.

函数参数

Function parameters are declared the same way as variables, and the name of unused parameters can be omitted.

For example, if you want your contract to accept one kind of external call with two integers, you would use something like:

Function parameters can be used as any other local variable and they can also be assigned to.

An external function cannot accept a multi-dimensional array as an input parameter. This functionality is possible if you enable the new experimental <code>ABIEncoderV2</code> feature by adding <code>pragma experimental ABIEncoderV2;</code> to your source file.

An internal function can accept a multi-dimensional array without enabling the feature.

返回值

Function return variables are declared with the same syntax after the <code>returns</code> keyword.

For example, suppose you want to return two results: the sum and the product of two integers passed as function parameters, then you use something like:

The names of return variables can be omitted. Return variables can be used as any other local variable and they are initialized with their default value and have that value unless explicitly set.

You can either explicitly assign to return variables and then leave the function using <code>return;</code>, or you can provide return values (either a single or multiple ones) directly with the <code>return</code> statement:

This form is equivalent to first assigning values to the return variables and then using <code>return;</code> to leave the function.

You cannot return some types from non-internal functions, notably multi-dimensional dynamic arrays and structs. If you enable the new experimental <code>ABIEncoderV2</code> feature by adding <code>pragma experimental ABIEncoderV2;</code> to your source file then more types are available, but <code>mapping</code>types are still limited to inside a single contract and you cannot transfer them.

返回多个值

When a function has multiple return types, the statement <code>return (v0, v1, ..., vn)</code> can be used to return multiple values. The number of components must be the same as the number of return types.

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

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

修改状态变量。

产生事件。

创建其它合约。

使用 <code>selfdestruct</code>。

通过调用发送以太币。

调用任何没有标记为 <code>view</code> 或者 <code>pure</code> 的函数。

使用低级调用。

使用包含特定操作码的内联汇编。

<code>constant</code> 是 <code>view</code> 的别名。

Getter 方法被标记为 <code>view</code>。

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

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

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

读取状态变量。

访问 <code>this.balance</code> 或者 <code>&lt;address&gt;.balance</code>。

访问 <code>block</code>,<code>tx</code>, <code>msg</code> 中任意成员 (除 <code>msg.sig</code> 和 <code>msg.data</code> 之外)。

调用任何未标记为 <code>pure</code> 的函数。

使用包含某些操作码的内联汇编。

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

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

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

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

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

写入存储

创建合约

调用消耗大量 gas 的外部函数

发送以太币

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

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

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

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

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

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

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

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

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

重载解析和参数匹配

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Javascript 文档

Web3.js 0.2x 中文文档

事件使用例程

如何在 js 中访问它们

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

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

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

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

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

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

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

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

A 构造器函数 is an optional function declared with the <code>constructor</code> keyword which is executed upon contract creation, and where you can run contract initialisation code.

Before the constructor code is executed, state variables are initialised to their specified value if you initialise them inline, or zero if you do not.

After the constructor has run, the final code of the contract is deployed to the blockchain. The deployment of the code costs additional gas linear to the length of the code. This code includes all functions that are part of the public interface and all functions that are reachable from there through function calls. It does not include the constructor code or internal functions that are only called from the constructor.

Constructor functions can be either <code>public</code> or <code>internal</code>. If there is no constructor, the contract will assume the default constructor, which is equivalent to <code>constructor() public {}</code>. For example:

A constructor set as <code>internal</code> causes the contract to be marked as abstract.

Prior to version 0.4.22, constructors were defined as functions with the same name as the contract. This syntax was deprecated and is not allowed anymore in version 0.5.0.

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

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

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

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

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

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

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

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

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

Note that a function without implementation is different from a Function Type even though their syntax looks very similar.

Example of function without implementation (a function declaration):

Example of a Function Type (a variable declaration, where the variable is of type <code>function</code>):

Abstract contracts decouple the definition of a contract from its implementation providing better extensibility and self-documentation and facilitating patterns like the Template method and removing code duplication. Abstract contracts are useful in the same way that defining methods in an interface is useful. It is a way for the designer of the abstract contract to say “any child of mine must implement this method”.

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

无法继承其他合约或接口。

无法定义构造函数。

无法定义变量。

无法定义结构体

无法定义枚举。

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

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

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

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

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

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

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

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

调用 <code>Set.contains</code>,<code>Set.insert</code> 和 <code>Set.remove</code> 都被编译为外部调用( <code>DELEGATECALL</code> )。 如果使用库,请注意实际执行的是外部函数调用。 <code>msg.sender</code>, <code>msg.value</code> 和 <code>this</code> 在调用中将保留它们的值, (在 Homestead 之前,因为使用了 <code>CALLCODE</code>,改变了 <code>msg.sender</code> 和 <code>msg.value</code>)。

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

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

与合约相比,库的限制:

没有状态变量

不能够继承或被继承

不能接收以太币

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

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

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

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

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

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

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

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

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

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

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

继续阅读