关于 solidity storage layout 的理解

solidity storage layout

1.概括

存储就像一个键值数据结构,保存 Solidity 智能合约的状态变量值。

将存储视为数组将有助于我们更好地理解它。该存储“数组”中的每个空间称为槽,可保存 32 字节数据(256 位)。这个存储“数组”的最大长度是2²⁵⁶-1,因此我们可以在其中容纳很多元素。

智能合约中声明的每个状态变量将根据其声明位置和类型占用一个槽位

注意:

  • 默认值:不给状态变量赋值相当于根据类型为其指定默认值或零值

  • 存储方式:存储中的所有值均以ABI 编码保存,当使用其变量检索值时,它们会自动解码。直接使用获取存储值web3.eth.getStorageAt不会自动解码该值,因为基于变量的类型 Solidity 会知道如何解码它,但不知道web3.eth.getStorageAt该值应该是什么类型存储插槽,无法解码

    // const storageOne = await ethers.provider.getStorage(demo.target, 0)
    const abiCoder = ethers.AbiCoder.defaultAbiCoder();
    // console.log(abiCoder.decode(["uint64"], storageOne))

2.分类

2.1值类型状态变量存储方式

32 字节类型始终占用一个槽。较小的类型可以共享一个公共插槽

请注意它们如何从右到左组合到一个槽中,其中声明的第一个变量占据最右边的字节。

我们需要分析我们的状态变量声明以相应地对它们进行排序,以便它们占用尽可能少的空间。

2.2引用类型存储布局

动态类型并不是那么简单,因为它们可以动态地增加和减少其保存的数据量。因此它们不能像值类型那样按顺序存储

数组

对于数组,在槽中仅声明其长度被保存,其元素存储在存储中的其他位置,使用从数组声明槽索引和元素索引派生的计算值作为存储槽索引

storage本身看成一个数组,然后先看当前要计算的数组在storage这个大数组中的索引的散列值,加上元素的索引

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

contract StorageLayout {
    uint8[] public values = [1,2,3,4,5,6,7,8];

    bytes32 constant public startingIndexOfValuesArrayElementsInStorage = keccak256(abi.encode(0));

    function getElementIndexInStorage(uint256 _elementIndex) public pure returns(bytes32) {
        return bytes32(uint256(startingIndexOfValuesArrayElementsInStorage) + _elementIndex);
    }
}
    const arrayIndex = ethers.keccak256(abiCoder.encode(["uint8"], [1]))
    console.log(arrayIndex)
    console.log(abiCoder.decode(["uint256"], arrayIndex))
    console.log(await ethers.provider.getStorage(demo.target, "80084422859880547211683076133703299733277748156566366325829078699459944778997"))

映射

映射元素不按顺序存储为数组。映射元素槽索引是通过keccak256我们想要的元素的键(左侧填充 0 到 32 字节)和映射的声明槽索引的串联哈希来计算的

声明映射的槽不包含任何信息,仅包含空字节,这与包含数组长度的数组槽相反。但是,这个槽仍然是为映射保留的,以便了解其索引并使用它来计算其元素的槽索引,并防止在其他地方声明的其他映射为不同元素计算相同的槽索引。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

contract StorageLayout {
    mapping(address => uint) public balances; // This 'balances' mapping is in slot 0 since is the first state variable declared.

    uint8 constant balancesMappingIndex = 0; // This is not in any slot since its constant.

    constructor() {
        balances[0x6827b8f6cc60497d9bf5210d602C0EcaFDF7C405] = 678;
        balances[0x66B0b1d2930059407DcC30F1A2305435fc37315E] = 501;
        balances[0x9Edc86C3BAe6B04Ce2ea8EFF3abAC0C3160ba243] = 321;
    }

    function getStorageLocationForKey(address _key) public pure returns(bytes32) {
         // 1) left pad with 0 both the key and the mapping declaation index storage before concatenating.
         // 2) concatenate the key with the mapping declaration index in storage.
         // 3) Calculate the keccak256 hash of that concatenation. That's the element slot index for that key.
        // abi.encode works pretty well as left padding with 0 and concatenation.
        // For the address 0x6827b8f6cc60497d9bf5210d602C0EcaFDF7C405, 
        // the storage slot index hash would be: 0x86dfc0930cb222883cc0138873d68c1c9864fc2fe59d208c17f3484f489bef04
        return keccak256(abi.encode(_key, balancesMappingIndex));
    }

}

上面的合约声明mapping为第一个(也是唯一的)状态变量。然后它声明一个值为 0 的常量变量(由于它是常量,因此不会放入任何槽中),表示存储槽中余额映射的索引。以及一个实用getStorageLocationForKey函数,用于计算指定映射键的元素的存储槽索引

因此,我们可以得出结论,对于映射值,无法像数组一样通过将较小的类型放入单个槽中来排序它们以节省空间。

2.3 string and bytes

1.当字符串和字节数组长度小于31个字节的时候,可以按照插槽位置读取,返回编码后的string和bytes和最后一位保存的长度

2.当长度大于31个字节的时候,保存方式和数组一样,索引位置只保存数组的长度,获取数据与数组的计算方式一样

2.4 struct

对于结构体状态变量,声明它的槽索引是为它具有的第一个值保留的,下一个槽是第二个值,下一个槽是第三个值,依此类推。

参考文档:

https://medium.com/coinsbench/solidity-layout-and-access-of-storage-variables-simply-explained-1ce964d7c738

Solidity 开发基础测试 完成挑战,领取SBT,给你的链上履历加上一笔 !
开始挑战
点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
yuanmingtao
yuanmingtao
江湖只有他的大名,没有他的介绍。