Solidity-17-Contracts
Contracts
Solidity 中的契约类似于面向对象语言中的类。
它们在状态变量中包含持久数据,以及可以修改这些变量的函数。
在不同的合约(实例)上调用函数将执行 EVM 函数调用,从而切换上下文,使得调用合约中的状态变量不可访问。
任何事情发生都需要调用合约及其函数。
以太坊中没有“cron”概念来自动调用特定事件的函数。
创建合同
合约可以通过以太坊交易“从外部”创建,也可以从 Solidity 合约内部创建。
IDE(例如 Remix)使用 UI 元素使创建过程无缝。
在以太坊上以编程方式创建合约的一种方法是通过 JavaScript API web3.js。它有一个名为 web3.eth.Contract 的函数来促进合约的创建。
当一个合约被创建时,它的构造函数(一个使用 constructor 关键字声明的函数)被执行一次。
构造函数是可选的。只允许一个构造函数,这意味着不支持重载。
构造函数执行后,合约的最终代码存储在区块链上。此代码包括所有公共和外部函数以及可通过函数调用从那里访问的所有函数。部署的代码不包括构造函数代码或仅从构造函数调用的内部函数。
在内部,构造函数参数在合约本身的代码之后通过 ABI 编码传递,但如果您使用 web3.js,则不必关心这一点。
如果一个合约想要创建另一个合约,创建者必须知道创建的合约的源代码(和二进制文件)。
这意味着循环创建依赖是不可能的。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 =0.4.16 =0.4.16 =0.4.16 =0.4.0 =0.4.16 =0.4.0 uint) map;
uint[3] c;
uint[] d;
bytes e;
}
mapping (uint => mapping(bool => Data[])) public data;
}
它生成以下形式的函数。
结构中的映射和数组(字节数组除外)被省略了,因为没有很好的方法来选择单个结构成员或为映射提供键:
function data(uint arg1, bool arg2, uint arg3)
public
returns (uint a, bytes3 b, bytes memory e)
{
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
e = data[arg1][arg2][arg3].e;
}
Function Modifiers 功能修饰符
修饰符可用于以声明方式更改函数的行为。
例如,您可以使用修饰符在执行函数之前自动检查条件。
修饰符是合约的可继承属性,可以被派生合约覆盖,但前提是它们被标记为虚拟。
有关详细信息,请参阅修改器覆盖。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 = price) {
_;
}
}
}
contract Register is priced, destructible {
mapping (address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) { price = initialPrice; }
// It is important to also provide the
// `payable` keyword here, otherwise the function will
// automatically reject all Ether sent to it.
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,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
/// This function is protected by a mutex, which means that
/// reentrant calls from within `msg.sender.call` cannot call `f` again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
如果你想访问合约 C 中定义的修饰符 m,你可以使用 C.m 来引用它而无需虚拟查找。
只能使用当前合约或其基础合约中定义的修饰符。
修饰符也可以在库中定义,但它们的使用仅限于同一库的函数。
通过在以空格分隔的列表中指定多个修饰符来将多个修饰符应用于函数,并按显示的顺序进行评估。
修饰符不能隐式访问或更改它们修改的函数的参数和返回值。
它们的值只能在调用时显式传递给它们。
从修饰符或函数体显式返回仅保留当前修饰符或函数体。
分配返回变量,控制流在前面修饰符中的 _
之后继续。
- WARN
在早期版本的 Solidity 中,具有修饰符的函数中的 return 语句表现不同。
带有 return 的修饰符的显式返回; 不影响函数返回的值。 然而,修饰符可以选择根本不执行函数体,在这种情况下,返回变量被设置为它们的默认值,就像函数有一个空的函数体一样。
_
符号可以多次出现在修饰符中。 每次出现都替换为函数体。
修饰符参数允许使用任意表达式,在这种情况下,从函数中可见的所有符号在修饰符中都是可见的。 修饰符中引入的符号在函数中不可见(因为它们可能会因覆盖而改变)。
常量和不可变状态变量
状态变量可以声明为常量或不可变。
在这两种情况下,变量在合约构建后都不能修改。
对于常量变量,值必须在编译时固定,而对于不可变变量,它仍然可以在构造时赋值。
也可以在文件级别定义常量变量。
编译器不会为这些变量保留存储槽,每次出现都会被相应的值替换。
与常规状态变量相比,常量和不可变变量的 gas 成本要低得多。
对于常量变量,分配给它的表达式被复制到所有访问它的地方,并且每次都重新计算。
这允许局部优化。不可变变量在构造时被评估一次,它们的值被复制到代码中访问它们的所有位置。
对于这些值,保留 32 个字节,即使它们可以容纳更少的字节。因此,常量值有时可能比不可变值便宜。
目前并非所有常量和不可变类型都已实现。唯一支持的类型是字符串(仅用于常量)和值类型。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.4;
uint constant X = 32**22 + 8;
contract C {
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals;
uint immutable maxBalance;
address immutable owner = msg.sender;
constructor(uint decimals_, address ref) {
decimals = decimals_;
// Assignments to immutables can even access the environment.
maxBalance = ref.balance;
}
function isBalanceTooHigh(address other) public view returns (bool) {
return other.balance > maxBalance;
}
}
常数 Constant
对于常量变量,该值在编译时必须是一个常量,并且必须在声明变量的地方赋值。
任何访问存储、区块链数据(例如 block.timestamp、address(this).balance 或 block.number)或执行数据(msg.value 或 gasleft())或调用外部合约的表达式都是不允许的。
允许可能对内存分配产生副作用的表达式,但不允许对其他内存对象产生副作用的表达式。
允许使用内置函数 keccak256、sha256、ripemd160、ecrecover、addmod 和 mulmod(尽管除了 keccak256,它们确实调用了外部合约)。
允许对内存分配器产生副作用的原因是应该可以构造复杂的对象,例如 查找表。
此功能尚未完全可用。
不可变 Immutable
声明为不可变的变量比声明为常量的限制要少一些:不可变变量可以在合约的构造函数中或在它们声明时被分配一个任意值。它们只能分配一次,从那时起,即使在施工期间也可以读取。
编译器生成的合约创建代码将在返回之前修改合约的运行时代码,将所有对不可变对象的引用替换为分配给它们的值。如果您将编译器生成的运行时代码与实际存储在区块链中的运行时代码进行比较,这一点很重要。
- 笔记
在声明时分配的不可变对象仅在合约的构造函数执行后才被视为已初始化。这意味着您不能使用依赖于另一个不可变对象的值内联初始化不可变对象。但是,您可以在合约的构造函数中执行此操作。
这是防止对状态变量初始化和构造函数执行顺序的不同解释的保护措施,尤其是在继承方面。
函數 Functions
可以在合约内部和外部定义函数。
合约之外的函数,也称为“自由函数”,总是具有隐含的内部可见性。
它们的代码包含在调用它们的所有合约中,类似于内部库函数。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 = 10);
found = true;
}
}
- 笔记
在合约之外定义的功能仍然总是在合约的上下文中执行。
他们仍然可以访问变量 this,可以调用其他合约,向它们发送以太币并销毁调用它们的合约等等。
与合约内定义的函数的主要区别在于,自由函数不能直接访问不在其范围内的存储变量和函数。
函数参数和返回变量
函数将类型化参数作为输入,并且与许多其他语言不同,它还可以返回任意数量的值作为输出。
Function Parameters
函数参数的声明方式与变量相同,未使用的参数名称可以省略。
例如,如果您希望您的合约接受一种带有两个整数的外部调用,您可以使用如下内容:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 =0.4.16 =0.4.16 =0.5.0 .balance`。
访问 block、tx、msg 的任何成员(msg.sig 和 msg.data 除外)。
调用任何未标记为纯的函数。
使用包含某些操作码的内联汇编。
```js
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 =0.6.0 =0.6.2 =0.4.16 =0.4.16 =0.4.16 =0.4.21 uint) balance;
function transfer(address to, uint256 amount) public {
if (amount > balance[msg.sender])
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
错误不能被重载或覆盖,而是被继承。只要范围不同,就可以在多个地方定义相同的错误。错误实例只能使用 revert 语句创建。
该错误创建的数据随后通过还原操作传递给调用者,以返回到链外组件或在 try/catch 语句中捕获它。请注意,错误只能在来自外部调用时被捕获,在内部调用或同一函数内部发生的还原无法被捕获。
如果不提供任何参数,则错误只需要四个字节的数据,您可以使用上面的 NatSpec 进一步解释错误背后的原因,它没有存储在链上。这使得它同时成为一个非常便宜和方便的错误报告功能。
更具体地说,错误实例以与对同名和类型的函数的函数调用相同的方式进行 ABI 编码,然后将其用作还原操作码中的返回数据。这意味着数据包含一个 4 字节选择器,后跟 ABI 编码数据。选择器由错误类型签名的 keccak256-hash 的前四个字节组成。
- 笔记
合同可能会因同名的不同错误或什至在调用者无法区分的不同位置定义的错误而恢复。对于外部,即 ABI,只有错误的名称是相关的,而不是定义它的合同或文件。
语句 require(condition, "description");如果可以定义错误 Error(string),则相当于 if (!condition) revert Error("description")。但是请注意,Error 是一种内置类型,不能在用户提供的代码中定义。
类似地,失败的断言或类似情况将恢复为内置类型 Panic(uint256) 的错误。
- 笔记
错误数据应该只用于给出失败的指示,而不是作为控制流的手段。原因是内部调用的还原数据默认通过外部调用链传播回来。这意味着内部调用可以“伪造”恢复看起来可能来自调用它的合约的数据。
继承
Solidity 支持多重继承,包括多态性。
多态性意味着函数调用(内部和外部)总是在继承层次结构中最派生的合约中执行同名(和参数类型)的函数。这必须使用 virtual 和 override 关键字在层次结构中的每个函数上显式启用。有关更多详细信息,请参阅函数覆盖。
可以通过使用 ContractName.functionName() 或使用 super.functionName() 显式指定合同在内部继承层次结构中进一步调用函数,如果您想在扁平继承层次结构中调用更高一级的函数(见下文)。
当一个合约继承自其他合约时,区块链上只创建一个合约,所有基础合约的代码都编译到创建的合约中。这意味着对基础合约函数的所有内部调用也只使用内部函数调用(super.f(..) 将使用 JUMP 而不是消息调用)。
状态变量遮蔽被视为错误。派生合约只能声明状态变量 x,如果在其任何基础中都没有同名的可见状态变量。
通用的继承系统与 Python 的非常相似,尤其是在多重继承方面,但也存在一些差异。
以下示例中给出了详细信息。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 =0.7.0 =0.7.0 =0.7.0 =0.6.0 =0.6.0 =0.6.0 =0.6.0 =0.6.0 =0.7.0 =0.7.0 =0.4.0 =0.7.0 =0.6.0 =0.6.0 =0.6.2 =0.6.2 =0.6.0 bool) flags;
}
library Set {
// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter `self`, if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
Data knownValues;
function register(uint value) public {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
require(Set.insert(knownValues, value));
}
// In this contract, we can also directly access knownValues.flags, if we want.
}
当然,您不必按照这种方式使用库:也可以在不定义结构数据类型的情况下使用它们。
函数也可以在没有任何存储引用参数的情况下工作,并且它们可以有多个存储引用参数并且可以在任何位置。
对 Set.contains、Set.insert 和 Set.remove 的调用都编译为对外部合约/库的调用 (DELEGATECALL)。
如果您使用库,请注意执行了实际的外部函数调用。
但是,msg.sender、msg.value 和 this 将在这次调用中保留它们的值(在 Homestead 之前,由于使用了 CALLCODE,msg.sender 和 msg.value 改变了)。
以下示例显示了如何使用存储在内存中的类型和库中的内部函数来实现自定义类型,而无需外部函数调用的开销:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
struct bigint {
uint[] limbs;
}
library BigInt {
function fromUint(uint x) internal pure returns (bigint memory r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint memory a, bigint memory b) internal pure returns (bigint memory r) {
r.limbs = new uint[](max(a.limbs.length, b.limbs.length));
uint carry = 0;
for (uint i = 0; i 0))
carry = 1;
else
carry = 0;
}
}
if (carry > 0) {
// too bad, we have to add a limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
uint i;
for (i = 0; i b ? a : b;
}
}
contract C {
using BigInt for bigint;
function f() public pure {
bigint memory x = BigInt.fromUint(7);
bigint memory y = BigInt.fromUint(type(uint).max);
bigint memory z = x.add(y);
assert(z.limb(1) > 0);
}
}
可以通过将库类型转换为地址类型来获取库的地址,即使用地址(LibraryName)。
由于编译器不知道库的部署地址,因此编译后的十六进制代码将包含 __$30bbc0abd4d6364515865950d3e0d10953$__
形式的占位符。
占位符是完全限定库名称的 keccak256 哈希的十六进制编码的 34 个字符前缀,例如,如果库存储在 library/ 中名为 bigint.sol 的文件中,则为 library/bigint.sol:BigInt目录。
此类字节码不完整,不应部署。占位符需要替换为实际地址。
您可以通过在编译库时将它们传递给编译器或使用链接器更新已编译的二进制文件来做到这一点。
有关如何使用命令行编译器进行链接的信息,请参阅库链接。
与合约相比,库在以下方面受到限制:
他们不能有状态变量
他们不能继承也不能被继承
他们无法接收以太币
他们不能被摧毁
(这些可能会在稍后解除。)
库中的函数签名和选择器
虽然对公共或外部库函数的外部调用是可能的,但此类调用的调用约定被认为是 Solidity 内部的,与为常规合约 ABI 指定的不同。
外部库函数支持比外部合约函数更多的参数类型,例如递归结构和存储指针。
出于这个原因,用于计算 4 字节选择器的函数签名是按照内部命名模式计算的,并且合约 ABI 中不支持的类型的参数使用内部编码。
以下标识符用于签名中的类型:
值类型、非存储字符串和非存储字节使用与合约 ABI 中相同的标识符。
非存储数组类型遵循与合约 ABI 中相同的约定,即 []
用于动态数组,[M]
用于 M 元素的固定大小数组。
非存储结构由它们的完全限定名称引用,即合同 C { struct S { ... } } 的 C.S。
存储指针映射使用 mapping( => )
存储,其中 和
分别是映射的键和值类型的标识符。
其他存储指针类型使用其对应的非存储类型的类型标识符,但会附加一个空格,然后是存储。
参数编码与常规合约 ABI 相同,除了存储指针,它们被编码为 uint256 值,指的是它们指向的存储槽。
与合约 ABI 类似,选择器由签名的 Keccak256-hash 的前四个字节组成。它的值可以使用 .selector 成员从 Solidity 中获取,如下所示:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.14 bool) flags; }
// Now we attach functions to the type.
// The attached functions can be used throughout the rest of the module.
// If you import the module, you have to
// repeat the using directive there, for example as
// import "flags.sol" as Flags;
// using {Flags.insert, Flags.remove, Flags.contains}
// for Flags.Data;
using {insert, remove, contains} for Data;
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
contract C {
Data knownValues;
function register(uint value) public {
// Here, all variables of type Data have
// corresponding member functions.
// The following function call is identical to
// `Set.insert(knownValues, value)`
require(knownValues.insert(value));
}
}
也可以以这种方式扩展内置类型。
在这个例子中,我们将使用一个库。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
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 type(uint).max;
}
}
using Search for uint[];
contract C {
uint[] data;
function append(uint value) public {
data.push(value);
}
function replace(uint from, uint to) public {
// This performs the library function call
uint index = data.indexOf(from);
if (index == type(uint).max)
data.push(to);
else
data[index] = to;
}
}
请注意,所有外部库调用都是实际的 EVM 函数调用。
这意味着如果您传递内存或值类型,将执行复制,即使是 self 变量。
唯一不会执行复制的情况是使用存储引用变量或调用内部库函数时。
参考资料
https://docs.soliditylang.org/en/latest/control-structures.html