【区块链安全 | 第二十八篇】合约(二)

在这里插入图片描述

合约

常量(Constant)和不可变(Immutable)状态变量

状态变量可以声明为常量(constant)或不可变(immutable)。在这两种情况下,变量在合约构建后不能再被修改。对于常量变量,值必须在编译时固定,而对于不可变变量,则可以在构造函数中进行赋值。

也可以在文件级别定义常量变量。

每次出现这些变量时,源代码中的变量都会被其底层值替换,且编译器不会为其保留存储槽。也不能使用transient关键字将其分配到临时存储槽。

与常规状态变量相比,常量和不可变变量的Gas费用要低得多。对于常量变量,赋给它的表达式会被复制到所有访问它的地方,并且每次都重新评估。这允许进行局部优化。不可变变量只在构造时评估一次,其值会复制到所有访问它的地方。对于这些值,仍然会保留32字节的存储空间,即使它们实际上可以适应更少的字节。因此,有时常量值比不可变值便宜。

并非所有类型都可以用于常量和不可变变量。目前,仅支持字符串(仅限常量)和值类型。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.21;

uint constant X = 32**22 + 8;

contract C {
    string constant TEXT = "abc";
    bytes32 constant MY_HASH = keccak256("abc");
    uint immutable decimals = 18;
    uint immutable maxBalance;
    address immutable owner = msg.sender;

    constructor(uint decimals_, address ref) {
        if (decimals_ != 0)
            // 不可变变量仅在部署时不可修改。
            // 在构造时可以被赋值多次。
            decimals = decimals_;

        // 对不可变变量的赋值甚至可以访问环境变量。
        maxBalance = ref.balance;
    }

    function isBalanceTooHigh(address other) public view returns (bool) {
        return other.balance > maxBalance;
    }
}

常量(Constant)

对于常量变量,值必须在编译时是常量,并且必须在声明时赋值。任何访问存储、区块链数据(如block.timestampaddress(this).balanceblock.number)或执行数据(如msg.valuegasleft())的表达式,或者调用外部合约的表达式都不被允许。允许那些可能对内存分配产生副作用的表达式,但不允许那些可能对其他内存对象产生副作用的表达式。内置函数如keccak256sha256ripemd160ecrecoveraddmodmulmod是允许的(即使除了keccak256之外,它们确实调用外部合约)。

允许内存分配器产生副作用的原因是,应该能够构造像查找表这样的复杂对象。这个特性目前还未完全实现。

不可变(Immutable)

声明为不可变的变量比常量变量的限制稍微少一些:不可变变量可以在构造时赋值。赋值可以在部署之前的任何时间进行,然后它会变得永久。

另外一个限制是:不可变变量只能在构造函数中赋值,不能在其他修改器或函数中赋值。

对于不可变变量的读取没有任何限制。在第一次赋值之前也允许读取这些变量,因为Solidity中的变量始终有一个明确的初始值。因此,甚至可以从不显式地为不可变变量赋值。

当在构造时访问不可变变量时,请注意初始化顺序。即使你提供了明确的初始化器,一些表达式可能会在初始化器之前进行评估,特别是当它们位于继承层次结构中的不同级别时。

在Solidity 0.8.21之前,不可变变量的初始化限制更加严格。此类变量必须在构造时初始化一次,并且在此之前不能读取。

编译器生成的合约创建代码会在返回合约的运行时代码之前修改它,替换所有对不可变变量的引用为赋给它们的值。这对于将编译器生成的运行时代码与实际存储在区块链上的字节码进行比较非常重要。编译器会在JSON标准输出的immutableReferences字段中输出这些不可变变量在部署字节码中的位置。

自定义存储布局

合约可以使用布局说明符定义其存储的任意位置。合约的状态变量,包括从基类继承的变量,将从指定的基础槽(base slot)开始,而不是默认的零槽。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.29;

contract C layout at 0xAAAA + 0x11 {
    uint[3] x; // 占用槽 0xAABB..0xAABD
}

如上例所示,说明符使用 layout at <base-slot-expression> 语法,并位于合约定义的头部。

布局说明符可以放在继承说明符之前或之后,并且最多只能出现一次。base-slot-expression 必须是一个整数文字表达式,可以在编译时进行求值,并且生成一个在 uint256 范围内的值。

自定义布局不能使合约的存储“环绕”。如果选择的基础槽会将静态变量推到存储的末端,编译器将发出错误。请注意,动态数组和映射的数据区域不受此检查的影响,因为它们的布局不是线性的。无论使用哪个基础槽,它们的位置总是根据某种方式计算的,始终确保它们位于 uint256 范围内,并且它们的大小在编译时不可知。

虽然对基础槽没有其他限制,但建议避免选择太接近地址空间末端的位置。留下太少的空间可能会使合约升级复杂化,或者在合约使用内联汇编存储额外值时引发问题。

存储布局只能为继承树中的最顶层合约指定,并影响该树中所有合约的存储变量位置。变量按照其定义顺序以及合约在线性化继承层次结构中的位置布局,且自定义基础槽会将它们的位置整体平移。

存储布局不能为抽象合约、接口

猜你喜欢

转载自blog.csdn.net/2301_77485708/article/details/147035689