文章目录

合约
库
在 Solidity 中,库(Library)与合约类似,但其主要目的是只在特定地址上部署一次,并通过 EVM 的 DELEGATECALL(在 Homestead 之前是 CALLCODE)功能重复使用其代码。这意味着,当调用库中的函数时,代码将在调用合约的上下文中执行,即 this 指向的是调用合约,特别是调用合约的存储会被访问。由于库本身是一个独立的源代码片段,它只有在显式提供的情况下,才能访问调用合约的状态变量(否则它无法命名这些变量)。库中的函数只能直接调用(即不使用 DELEGATECALL),并且必须是无状态的(即 view 或 pure 函数)。特别地,库是不能被销毁的。
注意:
在 Solidity 0.4.20 版本之前,可以通过绕过类型系统的方式销毁库。从 Solidity 0.4.20 版本开始,库包含了一种机制,禁止直接调用修改状态的函数(即不使用 DELEGATECALL)。
库可以被看作是使用它们的合约的隐式基合约。虽然它们不会在继承层次结构中显式地显示,但调用库函数的方式就像调用显式基合约的函数(通过限定访问,例如 L.f())。与内部函数调用不同,调用外部库函数时会使用外部调用约定,意味着所有传递给库的内存类型会按引用传递,而不是复制。为了在 EVM 中实现这一点,所有从合约中调用的内部库函数以及所有从库中调用的函数,都将在编译时包含在调用合约中,并通过常规的 JUMP 调用,而不是 DELEGATECALL。
以下示例说明了如何使用库(可以使用更高级示例的 for 来实现集合):
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// 我们定义一个新的结构体数据类型,用于
// 在调用合约中保存其数据。
struct Data {
mapping(uint => bool) flags;
}
library Set {
// 注意,第一个参数是 "storage 引用" 类型,
// 因此仅其存储地址,而不是其内容被作为调用的一部分传递。
// 这是库函数的一个特殊特性。
// 如果该函数可以被视为该对象的方法,通常将第一个参数命名为 `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 {
Data knownValues;
function register(uint value) public {
// 可以在没有库的特定实例的情况下调用库函数,
// 因为“实例”将是当前合约。
require(Set.insert(knownValues, value));
}
// 在这个合约中,我们也可以直接访问 knownValues.flags, 如果我们想的话。
}
当然,我们不必按照这种方式使用库:它们也可以在不定义结构体数据类型的情况下使用。函数也可以在没有任何存储引用参数的情况下工作,并且可以拥有多个存储引用参数,而且这些函数可以放置在任何位置。
例如,对 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 ;
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 < r.limbs.length; ++i) {
uint limbA = limb(a, i);
uint limbB = limb(b, i);
unchecked {
r.limbs[i] = limbA + limbB + carry;
if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0))
carry = 1;
else
carry = 0;
}
}
if (carry > 0) {
// 太糟糕了,我们得加一个 limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
uint i;
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint memory a, uint index) internal pure returns (uint) {
return index < a.limbs.length ? a.limbs[index] : 0;
}
function max(uint a, uint b) private pure returns (uint) {
return a > 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);
}
}
可以通过将库类型转换为地址类型来获取库的地址,即使用 address(LibraryName)。
由于编译器无法预知库将部署到哪个地址,因此编译后的字节码中会包含占位符,形式为 30 b b c 0 a b d 4 d 6364515865950 d 3 e 0 d 10953 30bbc0abd4d6364515865950d3e0d10953 30bbc0abd4d6364515865950d3e0d10953(在 0.5.0 版本之前格式不同)。这个占位符代表的是库名称全名的 keccak256 哈希值的前 34 个字符。例如,如果库存储在 libraries/ 目录下的 bigint.sol 文件中,那么占位符的值将是 libraries/bigint.sol:BigInt。这种字节码是不完整的,不能直接部署。占位符需要用实际的库地址替换。我们可以通过在库编译时将地址传递给编译器,或者使用链接器工具来更新已编译的二进制文件,完成地址替换。有关如何使用命令行编译器进行链接的详细信息,可以参考“库链接(Library Linking)”文档。
与合约相比,库在以下几个方面受到限制:
-
不能有状态变量:库不能存储任何状态。
-
不能继承或被继承:库不能作为基类被继承,也不能继承其他合约。
-
不能接收以太币:库不能接受直接发送给它的以太币。
-
不能被销毁:库不能被销毁或删除。
(这些限制可能会在未来版本中有所调整。)
库中的函数签名和选择器
虽然可以对公共或外部库函数进行外部调用,但这些调用的调用约定被视为 Solidity 内部的,而非常规合约 ABI 中指定的调用约定。外部库函数支持比外部合约函数更多的参数类型,例如递归结构体和存储指针。因此,用于计算 4 字节选择器的函数签名遵循内部命名方案,且在合约 ABI 中不支持的参数类型使用内部编码。
以下标识符用于签名中的类型:
-
值类型、非存储字符串和非存储字节:使用与合约 ABI 中相同的标识符。
-
非存储数组类型:遵循与合约 ABI 中相同的约定,即
<type>[]表示动态数组,<type>[M]表示固定大小为 M 的数组。 -
非存储结构体:通过其全名引用,即 C.S,其中 C 是合约,S 是合约 C 中定义的结构体。
-
存储指针映射:使用 mapping(
<keyType>=><valueType>) storage,其中<keyType>和<valueType>分别是映射的键和值类型的标识符。 -
其他存储指针类型:使用对应的非存储类型的类型标识符,但在其后加上一个空格和 storage。
参数编码与常规合约 ABI 相同,唯一不同的是存储指针,它们被编码为一个 uint256 值,表示指向的存储槽。
与合约 ABI 类似,选择器由签名的 Keccak256 哈希值的前四个字节组成。可以通过 Solidity 使用 .selector 成员来获取其值,示例如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.14 <0.9.0;
library L {
function f(uint256) external {}
}
contract C {
function g() public pure returns (bytes4) {
return L.f.selector;
}
}
库的调用保护
如在介绍中所提到的,如果库的代码使用 CALL 而不是 DELEGATECALL 或 CALLCODE 执行,则除非调用的是 view 或 pure 函数,否则将会回退(revert)。
EVM 并没有直接提供方法让合约检测是否使用 CALL 进行了调用,但合约可以利用 ADDRESS 操作码来查找当前执行的合约地址。通过将这个地址与构造时使用的地址进行比较,可以判断调用模式。
更具体地说,库的运行时代码总是以一个推送指令开始,这个指令在编译时是一个 20 字节的零值。当部署代码运行时,这个常量会在内存中被当前地址替换,并且修改后的代码会被存储到合约中。在运行时,这会导致部署时的地址成为第一个被推送到栈上的常量,而调度代码会将当前地址与该常量进行比较,用于判断是否进行非 view 和非 pure 函数的调用。
这意味着,存储在链上的库的实际代码与编译器报告的 deployedBytecode 代码可能不同。
6054

被折叠的 条评论
为什么被折叠?



