iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0
Modern Web

web3 短篇集系列 第 11

認識 Storage Slots

  • 分享至 

  • xImage
  •  

每個合約都有自己的儲存區域,solidity 編譯時,會將 state variables 依序映射到儲存槽 (storage slots)。

例如以下合約,它的 slot[0x0] 是 0xc,slot[0x1] 是 0xd,slot[0x2] 是 0xe。

contract Storage {
    uint256 a = 0xc;
    uint256 b = 0xd;
    uint256 c = 0xe;
}

Storage slots 可以想成是 32-bytes:32-bytes 的鍵值對。

我們可以透過 JSON RPC API 的 eth_getStorageAt 來取得指定儲存槽內的值:

curl -X POST \
$sepolia \
-d '{"jsonrpc":"2.0", "method": "eth_getStorageAt", "params": ["0xC21bEa2028ADf9d6dc9f948730dC0187ede89c58", "0x0", "latest"], "id": 1}'

使用 cast storage

cast storage --rpc-url $sepolia \
0xC21bEa2028ADf9d6dc9f948730dC0187ede89c58 \
0

因為區塊鏈上的資料都是公開透明的,因此就算 function visibility modifier 寫 private 或 internal,雖然透過外部合約無法存取,用戶還是能透過 JSON RPC API 找到該 storage slot 的值。

contract Storage {
    uint256 private secret = 32;
}

Storage Layout

本文主要參考這篇 What is Smart Contract Storage Layout?

以下引用該文的附圖:

Struct

Static variable

Dynamic variable

Mappings

實作找出 storage slot 的 key

以下針對 dynamic array 和 mappings,實作找出儲存槽位置:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;

// sepolia 0xC21bEa2028ADf9d6dc9f948730dC0187ede89c58
contract DynamicArray {
    uint256 a = 0x23;
    uint256 b = 0x45;
    uint256[] c;
    uint256 d = 0x67;

    constructor() {
        c.push(0xa);
        c.push(0xb);
        c.push(0xc);
    }
}

// sepolia 0x11A1b5888D3Bc765462Bf626e166872C41F6a38f
contract Mapping {
    uint256 a = 0x23;
    uint256 b = 0x45;
    mapping(uint256 => uint256) c;
    uint256 d = 0x67;

    constructor() {
        c[30] = 0xdeff;
        c[122] = 0x101;
    }
}
  • dynamic array 的找法,原本的 state variable 順序指向陣列的長度,也就是 slot[0x2] = 0x3,而陣列第一個值的 slot 位置會在 slot[keccak256(0x2)],第二個值位在 slot[keccak256(0x2)+1],以此類推。
  • mappings 的找法,原本的 state variable 順序指向 0x0,要將 mapping 的 key 和 state variable 的順序,兩者結合起來做雜湊,例如 c[30] 的位置會在 slot[keccak256(0x1e,0x2)]。

以下使用 TypeScript 和 ethers

import { keccak256, toBeHex, zeroPadValue } from 'ethers'

console.log('========= dynamic array =========')

const position = zeroPadValue('0x02', 32)
console.log(position)
const dynamicArrayStorageKey = keccak256(position)
console.log(dynamicArrayStorageKey)

console.log(addOneToHex(keccak256(zeroPadValue('0x02', 32))))

console.log('========= mapping =========')

function addOneToHex(hexStr: string): string {
	const decimalValue = BigInt(hexStr)
	const newValue = decimalValue + BigInt(1)
	return '0x' + newValue.toString(16)
}

Output

========= dynamic array =========
0x0000000000000000000000000000000000000000000000000000000000000002
0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5acf
========= mapping =========
000000000000000000000000000000000000000000000000000000000000001e
0000000000000000000000000000000000000000000000000000000000000002
0x6ea47ca2f9e3a67b0e336c514aa9f125109f49309b7162caec32e7d27e5c838c

確認有取到值

cast storage --rpc-url $sepolia \
0xC21bEa2028ADf9d6dc9f948730dC0187ede89c58 \
0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5acf
0x000000000000000000000000000000000000000000000000000000000000000b
0x000000000000000000000000000000000000000000000000000000000000000b
 cast storage --rpc-url $sepolia \
0x11A1b5888D3Bc765462Bf626e166872C41F6a38f \
0x6ea47ca2f9e3a67b0e336c514aa9f125109f49309b7162caec32e7d27e5c838c
0x000000000000000000000000000000000000000000000000000000000000deff

StorageSlot Library

Openzeppelin 提供可以在合約存取指定 slot 的套件

import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";

Library StorageSlot.sol 大致如下:

library StorageSlot {
    struct AddressSlot {
        address value;
    }

    function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly {
            r.slot := slot
        }
    }
    ...
}

使用方法:

contract ERC1967 {
    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    function _getImplementation() internal view returns (address) {
        return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
    }

    function _setImplementation(address newImplementation) internal {
        require(newImplementation.code.length > 0);
        StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
    }
}

關於 ERC-1967 和 ERC-1822 可升級合約,留待下回介紹。

Murmur

剛剛本來打完了,結果不小心按到上一頁,回過神來發現只剩標題還在...直接吐血...差點棄賽,最後還是忍痛重寫了一次,昏倒...

--

關於昨天在 GCP 上運行的節點,一早醒來發現 geth 優雅地停止了,理由是 no space left on device,QQ,太天真了竟然沒有把硬體要求好好讀過

Geth hardware requirements:

  • CPU: quad-core or dual-core hyperthreaded 四核心或雙核心超執行緒
  • Memory: 16GB RAM
  • Disk space: >650GB for a snap-synced full node, with the default cache size, grows about 14GB/week. Pruning brings the total storage back down to the original 650GB.
  • A fast SSD 固態硬碟 rather than HDD 傳統硬碟
  • Bandwidth: 25Mbps download speed

上一篇
以太坊客戶端跑起來
下一篇
以太坊客戶端跑起來 Part 2
系列文
web3 短篇集14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言