每個合約都有自己的儲存區域,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;
}
本文主要參考這篇 What is Smart Contract Storage Layout?
以下引用該文的附圖:
Struct
Static variable
Dynamic variable
Mappings
以下針對 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;
}
}
以下使用 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
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 可升級合約,留待下回介紹。
剛剛本來打完了,結果不小心按到上一頁,回過神來發現只剩標題還在...直接吐血...差點棄賽,最後還是忍痛重寫了一次,昏倒...
--
關於昨天在 GCP 上運行的節點,一早醒來發現 geth 優雅地停止了,理由是 no space left on device,QQ,太天真了竟然沒有把硬體要求好好讀過