【区块链安全 | 第三十一篇】合约(五)

在这里插入图片描述

合约

在 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 代码可能不同。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋说

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值