Solidity contract智能合约概览
Contracts in Solidity are similar to classes in object-oriented languages. They contain persistent data in state variables, and functions that can modify these variables. Calling a function on a different contract (instance) will perform an EVM function call and thus switch the context such that state variables in the calling contract are inaccessible. A contract and its functions need to be called for anything to happen. There is no “cron” concept in Ethereum to call a function at a particular event automatically. 在solidity中Contract类似于在面向对象语言中的classes概念. 合约包含状态变量,用于存储数据, 和函数,用于修改这些变量。 不同的合约实例调用一个函数,会执行一个EVM的函数调用, 如此,切换上下文环境后,状态变量在调用合约的过程中不能互相访问.合约和合约的函数需要被调用,才能运行。在以太坊中,存在cron的概念, 定时任务自动调用的方式.
Visibility and Getters
State Variable Visibility
public
internal
private
Function Visibility
external
External functions are part of the contract interface, which means they can be called from other contracts and via transactions. An external function f
cannot be called internally (i.e. f()
does not work, but this.f()
works).
public
Public functions are part of the contract interface and can be either called internally or via message calls.
internal
private
Function Modifiers
Modifiers can be used to change the behavior of functions in a declarative way. For example, you can use a modifier to automatically check a condition prior to executing the function.
Constant and Immutable State Variables
State variables can be declared as constant
or immutable
. In both cases, the variables cannot be modified after the contract has been constructed. For constant
variables, the value has to be fixed at compile-time, while for immutable
, it can still be assigned at construction time.
constant在编译的时候,已经固化了变量值不可改变。 immutable是合约创建之后才固化下来,在合约的构造函数里面仍然可以修改.
Functions
Functions can be declared view
in which case they promise not to modify the state.
Functions can be declared pure
in which case they promise not to read from or modify the state.
Fallback Function
A contract can have at most one fallback
function, declared using either fallback () external [payable]
or fallback (bytes calldata input) external [payable] returns (bytes memory output)
(both without the function
keyword). This function must have external
visibility. A fallback function can be virtual, can override and can have modifiers.
一个只能合约最多有一个fallback函数,声明fallback () external [payable] 或者fallback (bytes calldata input) external [payable] returns (bytes memory output)
, 二者都没有function关键字. 回调函数必须是external 可视属性. 一个回调函数可以是虚拟的, 可被重写, 可以有修饰符modifiers去修饰。
contract TestPayable { uint x; uint y; // This function is called for all messages sent to // this contract, except plain Ether transfers // (there is no other function except the receive function). // Any call with non-empty calldata to this contract will execute // the fallback function (even if Ether is sent along with the call). // 任意非空calldata的call调用,都会触发这个回调函数. // call内部有参数,则调用fallback, 没有参数, 则调用receive. fallback() external payable { x = 1; y = msg.value; } // This function is called for plain Ether transfers, i.e. // for every call with empty calldata. // 每一个空的calldata的调用,会调用这个receive方法. receive() external payable { x = 2; y = msg.value; } }
function callTestPayable(TestPayable test) public returns (bool) { (bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()")); require(success); // results in test.x becoming == 1 and test.y becoming 0. (success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()")); require(success); // results in test.x becoming == 1 and test.y becoming 1. // If someone sends Ether to that contract, the receive function in TestPayable will be called. // Since that function writes to storage, it takes more gas than is available with a // simple ``send`` or ``transfer``. Because of that, we have to use a low-level call. (success,) = address(test).call{value: 2 ether}(""); require(success); // results in test.x becoming == 2 and test.y becoming 2 ether. return true; }
Inheritance
Solidity supports multiple inheritance including polymorphism. solidity 支持多继承和多模态.
Polymorphism means that a function call (internal and external) always executes the function of the same name (and parameter types) in the most derived contract in the inheritance hierarchy. This has to be explicitly enabled on each function in the hierarchy using the virtual and override keywords. See Function Overriding for more details.
多态性意味着函数调用(内部和外部)总是在继承层次结构中派生最多的契约中执行相同名称(和参数类型)的函数。必须使用virtual和override关键字在层次结构中的每个函数上显式启用此功能。有关更多详细信息,请参阅函数重写。
When a contract inherits from other contracts, only a single contract is created on the blockchain, and the code from all the base contracts is compiled into the created contract. This means that all internal calls to functions of base contracts also just use internal function calls (super.f(..)
will use JUMP and not a message call). 当一个合约从其他多个合约继承的时候, 只有一个合约会在链上创建,所有的基类合约代码会被编译为新建的合约中。意味着所有对基类合约的函数的内部调用将使用内部函数调用。(super.f(..) 将使用JUMP, 而不是消息调用).
多继承例子:
contract PriceFeed is Owned, Destructible, Named("GoldFeed") { function updateInfo(uint newInfo) public { if (msg.sender == owner) info = newInfo; } // Here, we only specify `override` and not `virtual`. // This means that contracts deriving from `PriceFeed` // cannot change the behavior of `destroy` anymore. function destroy() public override(Destructible, Named) { Named.destroy(); } function get() public view returns(uint r) { return info; } uint info; }
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
}
contract Destructible is owned {
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override { /* do cleanup 1 */ super.destroy(); }
}
contract Base2 is Destructible {
function destroy() public virtual override { /* do cleanup 2 */ super.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { super.destroy(); }
}
If Base2
calls a function of super
, it does not simply call this function on one of its base contracts. Rather, it calls this function on the next base contract in the final inheritance graph, so it will call Base1.destroy()
(note that the final inheritance sequence is – starting with the most derived contract: Final, Base2, Base1, Destructible, owned).
Base2 调用super,不是简单的调用基类中的某一个函数. 而是调用最终继承树旁边的基类的方法,因此就是调用Base1.destory(). 注意: 最终继承序列是:Final ->Base2 -> Base1 -> Destructible->owned.
Function Overriding 函数重写
The overriding function may only change the visibility of the overridden function from external
to public
. 复写函数可能只可以改变被复写函数的可视性. 比如从external到public.
The mutability may be changed to a more strict one following the order: nonpayable
can be overridden by view
and pure
. view
can be overridden by pure
. payable
is an exception and cannot be changed to any other mutability.
可变性可以按照以下顺序更改为更严格的可变性:nonpayable可以被view,pure重写, view可以被pure重写. payable是一个列外, 不可能被任何可变性重写.
Arguments for Base Constructors
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base {
uint x;
constructor(uint _x) { x = _x; }
}
// Either directly specify in the inheritance list...
contract Derived1 is Base(7) {
constructor() {}
}
// or through a "modifier" of the derived constructor.
contract Derived2 is Base {
constructor(uint _y) Base(_y * _y) {}
}
Multiple Inheritance and Linearization (多继承和线性化,从右向左, python是从左向右.)
Another simplifying way to explain this is that when a function is called that is defined multiple times in different contracts, the given bases are searched from right to left (left to right in Python) in a depth-first manner, stopping at the first match. If a base contract has already been searched, it is skipped. 另一个简单的方法去解释这个是: 多继承中,当调用一个定义在多个不同合约中相同的函数时,对基类函数的搜索顺序是以深度优先方式 从右向左,(在python中是从左向右)。 如果一个合约已经被搜索到, 搜索会中断跳出.
注意构造函数调用顺序:
// SP DX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Base1 { constructor() {} } contract Base2 { constructor() {} } // Constructors are executed in the following order: // 1 - Base1 // 2 - Base2 // 3 - Derived1 contract Derived1 is Base1, Base2 { constructor() Base1() Base2() {} } // Constructors are executed in the following order: // 1 - Base2 // 2 - Base1 // 3 - Derived2 contract Derived2 is Base2, Base1 { constructor() Base2() Base1() {} } // Constructors are still executed in the following order: // 1 - Base2 // 2 - Base1 // 3 - Derived3 contract Derived3 is Base2, Base1 { constructor() Base1() Base2() {} }
Abstract Contracts 抽象合约
Contracts need to be marked as abstract when at least one of their functions is not implemented.
合约必须被标注为abstract, 当至少一个函数没有被实现.
Contracts may be marked as abstract even though all functions are implemented.
合约可能会被标注为abstract,及时所有的函数都已经实现了.
Such abstract contracts can not be instantiated directly. This is also true, if an abstract contract itself does implement all defined functions. 抽象合约不能被实例化. 即使是抽象合约实现了所有的定义函数,也不能被直接实例化.
If a contract inherits from an abstract contract and does not implement all non-implemented functions by overriding, it needs to be marked as abstract as well.
如果一个合约继承一个抽象合约,并且没有实现全部抽象函数,则这个合约也得被标注为abstract.
抽象合约不能用一个抽象函数去重写一个已经实现了的函数.
Interfaces (接口)
-
They cannot inherit from other contracts, but they can inherit from other interfaces. 不能继承其他合约,只能继承接口.
-
All declared functions must be external. 所有的函数声明,必须外部可视.
-
They cannot declare a constructor. 不能声明构造函数.
-
They cannot declare state variables. 不能声明状态变量.
All functions declared in interfaces are implicitly
virtual
, which means that they can be overridden. This does not automatically mean that an overriding function can be overridden again - this is only possible if the overriding function is markedvirtual
.所有在接口中定义的函数隐式的被virtual修饰, 意味着它们都可以被重写。这并不是意味自动的, 重写函数可以被自动的被重写,必须要有virtual去修饰。
Libraries
Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the DELEGATECALL
(CALLCODE
until Homestead) feature of the EVM. Libraries 和contracts 非常相似, 它们被部署在一个特殊的地址上,它们的代码通过EVM的特性delegatecall 或callcode来调用重用。 liberies的执行环境是调用合约的执行环境。
Library functions can only be called directly 只有在libereries函数是view或pure的时候,才能直接调用。 不允许修改状态的lib函数,直接被调用, 必须通过delegatecall调用。
Libraries can be seen as implicit base contracts of the contracts that use them. They will not be explicitly visible in the inheritance hierarchy, but calls to library functions look just like calls to functions of explicit base contracts (using qualified access like L.f()
). Of course, calls to internal functions use the internal calling convention, which means that all internal types can be passed and types stored in memory will be passed by reference and not copied. To realize this in the EVM, code of internal library functions and all functions called from therein will at compile time be included in the calling contract, and a regular JUMP
call will be used instead of a DELEGATECALL
.
Libraries可以被看成是隐式的基类继承合约,但lib不会出现在继承层级中, 但调用lib的函数,跟调用基类合约的函数是一样的(正确的访问应该是 L.f()). 当然,调用内部函数,使用调用内部函数的惯例。convention 惯例. 意味所有被传递的内部类型都是存储在memory中, 可以被引用传递,而不是值拷贝。 EVM为了实现这个功能,lib的内部函数代码和在那里被调用的参数将会在编译期间被包含在调用合约里面(calling contract), 一个常规的JUMP 会取代DELEGATECALL.
As the compiler does not know the address where the library will be deployed, the compiled hex code will contain placeholders of the form __$30bbc0abd4d6364515865950d3e0d10953$__
.
因为编译器不知道library将会部署到那个地址上,所有编译16进制代码会包含一个占位符号:__$30bbc0abd4d6364515865950d3e0d10953$__ (是
libraries/bigint.sol:BigInt的hash格式.)
Such bytecode is incomplete and should not be deployed. Placeholders need to be replaced with actual addresses. You can do that by either passing them to the compiler when the library is being compiled or by using the linker to update an already compiled binary. See Library Linking for information on how to use the commandline compiler for linking. 如此字节代码是不完整的,不应该被部署。 占位符需要用真实的地址替换掉。 你可以把真实地址传递给编译器当library正在被编译的过程中, 或者使用linker更新一个已经编译好的字节码和真实地址建立联系.
In comparison to contracts, libraries are restricted in the following ways:
-
they cannot have state variables 不能拥有状态变量.
-
they cannot inherit nor be inherited 不能继承或被继承。
-
they cannot receive Ether 不能接受ETH
-
they cannot be destroyed 不能被销毁.
(These might be lifted at a later point.) 这些限制在未来中可能会被lifted.
Function Signatures and Selectors in Libraries
Similarly to the contract ABI, the selector consists of the first four bytes of the Keccak256-hash of the signature. 和contract ABI相似, selector 由 函数签名后的Keccak256-hash的前4个字节组成.
Call Protection For Libraries
As mentioned in the introduction, if a library’s code is executed using a CALL
instead of a DELEGATECALL
or CALLCODE
, it will revert unless a view
or pure
function is called.
只有view和pure库函数,可以用call调用,其他的都不可以,否则会被revert.
DelegateCall和callcode, 可以调用所有的库函数.
The EVM does not provide a direct way for a contract to detect whether it was called using CALL
or not, but a contract can use the ADDRESS
opcode to find out “where” it is currently running. The generated code compares this address to the address used at construction time to determine the mode of calling.
EVM没有为合约提供一个直接的方法去识别合约是被用call调用,还是不是. 但是合约可以用ADDRESS 操作码去识别它当前在哪里运行。生成的代码会去比较this.address和构造函数里面的地址去识别合约运行模式。
More specifically, the runtime code of a library always starts with a push instruction, which is a zero of 20 bytes at compilation time. When the deploy code runs, this constant is replaced in memory by the current address and this modified code is stored in the contract. At runtime, this causes the deploy time address to be the first constant to be pushed onto the stack and the dispatcher code compares the current address against this constant for any non-view and non-pure function.
This means that the actual code stored on chain for a library is different from the code reported by the compiler as deployedBytecode
.
更具体的说,库运行代码总是带着一个推送结构(20个字节结构,在编译期间,都为0)一起启动, 当开始部署时候, 这个20字节常量constant 会被当前地址所替换 并且 这个修改后的代码被存储在合约里面。在运行期间,部署期间的地址成为优先常量会被push到stack里面, 调度员代码会比较当前地址和常量constant地址为每一个非view和非pure的方法.
这意味着存储在库链上的实际代码与编译器报告为deployedBytecode的代码不同。
Using For
The directive using A for B;
can be used to attach library functions (from the library A
) to any type (B
) in the context of a contract. These functions will receive the object they are called on as their first parameter (like the self
variable in Python).
直接使用using A for B, 可以把库A的函数全部赋到B合约上. 这些库函数将会把B作为他们的第一个参数.
The effect of using A for *;
is that the functions from the library A
are attached to any type.
using A for *, 把库A的函数依附到所有类型上.
The using A for B;
directive is active only within the current contract, including within all of its functions, and has no effect outside of the contract in which it is used. The directive may only be used inside a contract, not inside any of its functions.
using A for B 指令只会在合约内有效,包括合约内的所有函数有效, 对合约外部没有影响。这个指令只能写在合约里面,不能写在函数里面。
Note that all external library calls are actual EVM function calls. This means that if you pass memory or value types, a copy will be performed, even of the self
variable. The only situation where no copy will be performed is when storage reference variables are used or when internal library functions are called.
请注意,所有外部库调用都是实际的EVM函数调用。这意味着,如果传递内存或值类型,将执行复制,甚至是自变量的复制。唯一不执行复制的情况是使用存储引用变量或调用内部库函数时。
调用库外部函数,除了storage类型,都是值复制。 调用库内部函数都是值引用。
Inline Assembly
Inline assembly is a way to access the Ethereum Virtual Machine at a low level. 内联汇编是一种以底层的方式去访问Etherum Virtual Machine的方式.This bypasses several important safety features and checks of Solidity. 这回绕过很重要的solidity的安全特性和安全检查。You should only use it for tasks that need it, and only if you are confident with using it. 你只能在任务真实需要的时候使用它,并且你有把握去使用它。
An inline assembly block is marked by assembly { ... }