🎰

コントラクトのストレージを解剖してみよう!

2024/05/25に公開

はじめに

初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

https://cryptospells.jp/

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

https://twitter.com/cardene777

https://chaldene.net/

https://qiita.com/cardene

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

今回はコントラクトのストレージに格納されているデータに直接アクセスして、コントラクトのどこにどのようにデータが保存されているかを見ていきます。
実際に手を動かしながら確認できるので、実行しながら理解を深めていきましょう!

今回の記事ではRemixというブラウザ上で動くエディタを使用していきます。
https://remix.ethereum.org/

ストレージ

概要

まずはコントラクトのストレージについての理解を深めていきましょう。
コントラクトにはデータの保管場所が3つあります。

保管場所 特徴 ガス代消費
storage コントラクト(ブロックチェーン)にデータを永続的に書き込みます。どこからでもデータの取得が可能になります。 多い
memory コントラクト内の関数実行開始から完了までの間アクセスできる、一時的なデータ保管場所です。関数内からのみアクセス可能です。 普通
calldata memoryのように振る舞い、データの更新ができません。 少ない

今回は上記のstorageに焦点を当て、どのようにデータが保存されているのかを見ていきます。

スロット

ストレージ内のデータは、slotと呼ばれる単位で保管されています。

1slotは32byteで構成され、最大2^{256} slotまでデータを保管可能です。

stringuint256などのデータは32byteであるため、1slotずつデータが保存されていきます。

それ以下のサイズのデータは上記のように同じslot内に格納される場合があります。
この時注意しないといけないのが、変数などの定義順によってはデータの保管効率が悪くなります。

上記の画像だと、コントラクト内で以下のような定義になっています。

string name;
uint8 income;
uint8 age;
string description;
address walletAddress;
bool enginner;
bool creator;

ちなみにslotには定義した値が右詰めで格納されています。
では、以下のような定義だとどうなるでしょうか?

string name;
string description;
address walletAddress;
uint8 income;
uint8 age;
bool enginner;
bool creator;

上記のように4つのslotを使用しています。
これでは余分にstorageを使用しているため、ガス代が余計にかかってしまいます。
このように、storageにどのようにしてデータを格納するかは、ガス代の削減につながるので意識しておくと良いです。

実際にstorageに格納されているデータは、上記のように16進数になっています。

ストレージのまとめ

この章ではストレージについての理解を深めてきました。
コントラクトでのデータ保管方法を意識してコントラクトの作成までできると、1段エンジニアとしてステップアップできるはずです。
では、次の章から実際にストレージの中身を見ていきましょう。

コントラクトの作成&デプロイ

では、早速今回使用するコントラクトを紹介していきます。
Remixを使い慣れていない方は以下の記事などを参考にしてください。

https://zenn.dev/cryptogames/articles/3903c469ffff87

コントラクトの作成

https://gist.github.com/cardene777/127c0bb18c95f7b89b1473618d92bc12

Remixに以下のコードを貼り付けてください。

// SPDX-License-Identifier: MIT

pragma solidity 0.8.26;

contract Storage {
    string public name; // slot 0
    string public description; // slot 1
    uint256 public version; // slot 2
    uint256[2] public timestampArry; // slot 3 ~ 4
    address public admin; // slot 5
    address public owner; // slot 6
    uint128 private cardNumber; // slot 7
    uint128 private cardPassword; // slot 7

    struct Profile {
        string name;
        uint256 age;
        uint256 createdAt;
    }
    Profile public profile; // slot 8~10

    Profile[] profiles; // slot 11

    mapping(uint256 => string) public addressName; // slot 12

    constructor() {
        name = "Storage Contract";
        description = "Checl contract sotrage";
        version = 1;
        timestampArry[0] = block.timestamp;
        timestampArry[1] = block.timestamp;
        profile = Profile("cardene", 100, block.timestamp);
        admin = msg.sender;
        owner = msg.sender;
        profiles.push(Profile("Alice", 30, block.timestamp));
        profiles.push(Profile("Bob", 25, block.timestamp));
        addressName[7] = "cardene";
        addressName[10] = "test";
    }

    function registerCard(uint128 cardNumber_, uint128 cardPassword_) public {
        cardNumber = cardNumber_;
        cardPassword = cardPassword_;
    }

    function getRegisterCard() public view returns (uint128, uint128) {
        return (cardNumber, cardPassword);
    }
}

コントラクトの説明

先ほど作成したコントラクトについて簡単に説明します。

string public name; // slot 0
string public description; // slot 1
uint256 public version; // slot 2
uint256[2] public timestampArry; // slot 3 ~ 4
address public admin; // slot 5
address public owner; // slot 6
uint128 private cardNumber; // slot 7
uint128 private cardPassword; // slot 7

struct Profile {
    string name;
    uint256 age;
    uint256 createdAt;
}
Profile public profile; // slot 8~10

Profile[] profiles; // slot 11

mapping(address => string) public addressName; // slot 12

上記部分では変数などをいろいろ定義して、ストレージにデータを格納しています。
「slot 〇〇」というのは、各データがどのストレージスロットに格納されているかを示しています。

constructor() {
    name = "Storage Contract";
    description = "Checl contract sotrage";
    version = 1;
    timestampArry[0] = block.timestamp;
    timestampArry[1] = block.timestamp;
    profile = Profile("cardene", 100, block.timestamp);
    admin = msg.sender;
    owner = msg.sender;
    profiles.push(Profile("Alice", 30, block.timestamp));
    profiles.push(Profile("Bob", 25, block.timestamp));
    addressName[7] = "cardene";
    addressName[10] = "test";
}

constructorはコントラクトをデプロイするときに一度だけ実行される関数です。
このconstructorでいろいろ値を格納しています。

function registerCard(uint128 cardNumber_, uint128 cardPassword_) public {
    cardNumber = cardNumber_;
    cardPassword = cardPassword_;
}

function getRegisterCard() public view returns (uint128, uint128) {
    return (cardNumber, cardPassword);
}

mappingや配列にデータを格納・取得するための関数です。

コントラクトのデプロイ

ここまでできたら、コントラクトのコンパイルとデプロイをしていきましょう。

デプロイまでできたらOKです!

slot 0

まずはslot0に保存されているデータから見ていきましょう。

デプロイしたコントラクトのアドレスを以下の「0xB57ee0797C3fc0205714a577c02F7205bB89dF30」の部分にコピペしてください。

web3.eth.getStorageAt("0xB57ee0797C3fc0205714a577c02F7205bB89dF30", 0)

これは「コントラクトアドレス0xB57ee0797C3fc0205714a577c02F7205bB89dF30のstorage領域内のslot0のデータを取得する」という処理を実行します。
では早速実行していきましょう!

上記のように、Remix内のTerminalと呼ばれる部分に先ほどのコマンドをコピペして実行してください。

そうすると以下のような16進数データが出力されます。

0x53746f7261676520436f6e747261637400000000000000000000000000000020

では、次に以下のコマンドを実行します。

web3.utils.hexToUtf8("0x53746f7261676520436f6e747261637400000000000000000000000000000020")

上記は16進数文字列をUTF-8形式の文字列に変換する処理を実行します。

Storage Contract

上記のように出力されたはずです。
これでコントラクトのstorageに直接アクセスしてデータを取得できました。
この調子で他のデータにもアクセスしていきましょう。

slot 1~slot 6

slot1~slot6まではほとんど同じであるため一気にデータを取得してみましょう。

slot1

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 1)
# 0x436865636c20636f6e747261637420736f74726167650000000000000000002c
web3.utils.hexToUtf8("0x436865636c20636f6e747261637420736f74726167650000000000000000002c")
# Checl contract sotrage

slot2

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 2)
# 0x01
web3.utils.toBN(0x01).toString();
# 1

今回は数値データであったため、16進数から数値データに変換しています。

slot3slot4

# slot3
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 3)
# 0x66517164
web3.utils.toBN(0x66517164).toString();
# 1716613476

# slot4
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 4)
# 0x66517164
web3.utils.toBN(0x66517164).toString();
# 1716613476

slot3slot4は以下のコードの部分で、固定長の配列の要素です。

uint256[2] public timestampArry; // slot 3 ~ 4

固定長の配列は事前に確保する領域がわかるため、このように連続したslotに格納されています。
では固定長ではない配列の場合はどうなるのでしょうか?
これについては後ほど解説していきます。

slot5

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 5)
# 0x5b38da6a701c568545dcfcb03fcb875f56beddc4

これはウォレットアドレスなので変換が不要です。

slot6

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 6)
# 0x5b38da6a701c568545dcfcb03fcb875f56beddc4

privateデータにアクセス

slot7には、コントラクト内の以下のデータが格納されています。

uint128 private cardNumber; // slot 7
uint128 private cardPassword; // slot 7

まずはこの変数にデータを格納する必要があります。

上記のように、以下のデータを左にコピペして、「registerCard」というボタンを押してください。

123456789, 987654321

これでデータを登録できました。

uint128サイズのデータであるため、同じslot内に保存されています。
ここでのポイントはprivateというアクセス制限がついています。
これは、「コントラクト内からのみアクセス可能」という意味です。
では実際データを取得した場合どうなるのかみていきましょう。

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 7)
# 0x3ade68b1000000000000000000000000075bcd15
web3.utils.toBN(0x75bcd15).toString();
# 123456789
web3.utils.toBN(0x3ade68b1).toString();
# 987654321

しっかりデータが取得できましたね。
このように、コントラクトでprivateと定義していても、外部から見えてしまいます。

https://chaldene.net/solidity-private-data

構造体データにアクセス

slot 8~slot 10には以下の構造体データが格納されています。

struct Profile {
    string name;
    uint256 age;
    uint256 createdAt;
}
Profile public profile; // slot 8~10

ではデータを取得してみましょう。

# slot 8
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 8)
# 0x63617264656e650000000000000000000000000000000000000000000000000e
web3.utils.hexToUtf8("0x63617264656e650000000000000000000000000000000000000000000000000e")
# cardene

# slot 9
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 9)
# 0x64
web3.utils.toBN(0x64).toString();
# 100

# slot 10
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 10)
# 0x665173de
web3.utils.toBN(0x665173de).toString();
# 1716614110

構造体も連続したデータ領域に保存されているのが確認できました。

可変長配列データにアクセス

slot 11には以下の構造体の配列に関するデータが保管されています。

Profile[] profiles; // slot 11

constructor() {
    ...
    profiles.push(Profile("Alice", 30, block.timestamp));
    profiles.push(Profile("Bob", 25, block.timestamp));
}

ではデータを取得してみましょう。

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 11)
# 0x02
web3.utils.toBN(0x02).toString();
# 2

ん?「2」?
想定外のデータが取得されました...。
実はこれは配列の長さデータです。
profilesは可変長の配列であるため、あらかじめデータ領域を確保しておくことが難しいです。
そのため、ストレージ内の別のslotにデータを保存しています。
そのため、slot 11には先ほどのように配列の長さデータが格納されています。

配列内の値は上記のようにかけ離れたslotにデータが保管されています。
また、配列内のそれぞれのデータはかけ離れた連続したslot内に保存されています。

ではそのデータを見ていきましょう。

web3.utils.soliditySha3({ type: "uint", value: 11 }) # slot
# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9

先ほどデータの保管場所について、「かけ離れた」と説明しました。
では、具体的にはどこに保存するように指定しているのでしょうか?
正解は配列の長さデータをハッシュ化した値です。
ハッシュ化とは、特定のデータを別の値に変換することで、基本的に一位の値を出力します(一部重複が見つかっていますが)。

https://wa3.i-3-i.info/word110419.html

slotの値は一位であるため、コントラクト内の他のデータがアクセスすることはありません。

では、まず先ほど取得した値のslotにアクセスしてデータを取得してみましょう。

# index: 0
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9")
# 0x416c69636500000000000000000000000000000000000000000000000000000a
web3.utils.hexToUtf8("0x416c69636500000000000000000000000000000000000000000000000000000a")
# Alice

完璧です!
値が取得できました。
ただ、配列内のデータはこれだけではありません。
他のデータにアクセスするにはどうしたら良いでしょうか?
先ほど「かけ離れた連続したslot内に保存」と説明したように、連続してデータが保存されています。
この連続したslotにアクセスするのにはslotの番号に「+1」すれば良いです。

ただ、少し複雑なのが、今回取得したデータは16進数であるため、16進数の「+1」をする必要があります。
0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9に「+1」をすると、0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbaとなります。
ではこのデータをもとにアクセスしてみましょう。

# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dba
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dba")
# 0x1e
web3.utils.toBN(0x1e).toString();
# 30

ちゃんと取得できました。
この調子で最後のデータも取得しましょう。

# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbb
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbb")
# 0x665186b6
web3.utils.toBN(0x665186b6).toString();
# 1716618934

完璧です!
が、まだBobのデータが残っています。

profiles.push(Profile("Bob", 25, block.timestamp));

ただ、これも簡単に取得できます。
先ほどまでで取得したデータに、また「+1」すれば取得できます。

## index: 1
# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbc
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbc")
# 0x426f620000000000000000000000000000000000000000000000000000000006
web3.utils.hexToUtf8("0x426f620000000000000000000000000000000000000000000000000000000006")
# Bob

# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbd
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbd")
# 0x19
web3.utils.toBN(0x19).toString();
# 25

# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbe
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbe")
# 0x665186b6
web3.utils.toBN(0x665186b6).toString();
# 1716618934

ちゃんと取得できました。
これで可変長の配列へのアクセス方法が理解できました。

mappingデータにアクセス

これが最後の章になります。
最後はmappingに保存されたデータへのアクセス方法を見ていきます。
コントラクトでは以下のように定義していました。

mapping(uint256 => string) public addressName; // slot 12

constructor() {
    ...
    addressName[7] = "cardene";
    addressName[10] = "test";
}

先ほどの配列とは異なり、今回は連続したslotにデータが保存されていません。
早速実行しながらどこに保存されているのかを見ていきましょう。
まずは先ほどの可変長アドレス同様、slot 12内のデータを取得してみます。

web3.eth.getStorageAt("0xB57ee0797C3fc0205714a577c02F7205bB89dF30", 12)
# 0x0

空っぽです...。
もはや配列の長さなどではなく、何もデータを保存していません。

mapping配列内のデータにアクセスするには、key(mappingのキー)とslotindex(0からの連番)をハッシュ化する必要があります。
今回でいえば、keyは7slotindexは12になります。

web3.utils.soliditySha3({type: "uint", value: 7}, { type: "uint", value: 12 }) # key, slot
# 0x63617264656e650000000000000000000000000000000000000000000000000e

上記のように16進数の値が取得できました。
では取得した値にアクセスしてみましょう。

web3.utils.hexToUtf8("0x63617264656e650000000000000000000000000000000000000000000000000e")
# cardene

ちゃんと値が取得できました。
では、もう片方のデータも取得してみます。

web3.utils.soliditySha3({type: "uint", value: 10}, { type: "uint", value: 12 }) # key, slot
# 0x9e6c92d7be355807bd948171438a5e65aaf9e4c36f1405c1b9ca25d27c4ea3a0
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x9e6c92d7be355807bd948171438a5e65aaf9e4c36f1405c1b9ca25d27c4ea3a0")
# 0x7465737400000000000000000000000000000000000000000000000000000008
web3.utils.hexToUtf8("0x7465737400000000000000000000000000000000000000000000000000000008")
# test

取得できました!
これで一通りのデータを取得することができました!
お疲れ様です!

記事で使用したコマンドなどを以下にまとめておきます。

コマンド
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 0)
# 0x53746f7261676520436f6e747261637400000000000000000000000000000020
web3.utils.hexToUtf8("0x53746f7261676520436f6e747261637400000000000000000000000000000020")
# Storage Contract 

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 1)
# 0x436865636c20636f6e747261637420736f74726167650000000000000000002c
web3.utils.hexToUtf8("0x436865636c20636f6e747261637420736f74726167650000000000000000002c")
# Checl contract sotrage

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 2)
# 0x01
web3.utils.toBN(0x01).toString();
# 1

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 3)
# 0x66517164
web3.utils.toBN(0x66517164).toString();
# 1716613476

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 4)
# 0x66517164
web3.utils.toBN(0x66517164).toString();
# 1716613476

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 5)
# 0x5b38da6a701c568545dcfcb03fcb875f56beddc4

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 6)
# 0x5b38da6a701c568545dcfcb03fcb875f56beddc4

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 7)
# 0x3ade68b1000000000000000000000000075bcd15
web3.utils.toBN(0x75bcd15).toString();
# 123456789
web3.utils.toBN(0x3ade68b1).toString();
# 987654321

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 8)
# 0x63617264656e650000000000000000000000000000000000000000000000000e
web3.utils.hexToUtf8("0x63617264656e650000000000000000000000000000000000000000000000000e")
# cardene

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 9)
# 0x64
web3.utils.toBN(0x64).toString();
# 100

web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 10)
# 0x665173de
web3.utils.toBN(0x665173de).toString();
# 1716614110

## Array
## index: 0
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 11)
# 0x02
web3.utils.toBN(0x02).toString();
# 2
web3.utils.soliditySha3({ type: "uint", value: 11 }) # slot
# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9")
# 0x416c69636500000000000000000000000000000000000000000000000000000a
web3.utils.hexToUtf8("0x416c69636500000000000000000000000000000000000000000000000000000a")
# Alice
# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dba
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dba")
# 0x1e
web3.utils.toBN(0x1e).toString();
# 30
# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbb
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbb")
# 0x665186b6
web3.utils.toBN(0x665186b6).toString();
# 1716618934

## index: 1
# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbc
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbc")
# 0x426f620000000000000000000000000000000000000000000000000000000006
web3.utils.hexToUtf8("0x426f620000000000000000000000000000000000000000000000000000000006")
# Bob
# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbd
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbd")
# 0x19
web3.utils.toBN(0x19).toString();
# 25
# 0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbe
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbe")
# 0x665186b6
web3.utils.toBN(0x665186b6).toString();
# 1716618934

## Mapping
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", 12)
# 0x0
web3.utils.soliditySha3({type: "uint", value: 7}, { type: "uint", value: 12 }) # key, slot
# 0x949c9a93ae746871004aabe5aa16f6723dba12da47a01a4a63a7f95d60037905
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0xdae089abd7155aa13ce498edb0d7a7156b783d015031f10c9a3d4f5fcb518971")
# 0x63617264656e650000000000000000000000000000000000000000000000000e
web3.utils.hexToUtf8("0x63617264656e650000000000000000000000000000000000000000000000000e")
# cardene

web3.utils.soliditySha3({type: "uint", value: 10}, { type: "uint", value: 12 }) # key, slot
# 0x9e6c92d7be355807bd948171438a5e65aaf9e4c36f1405c1b9ca25d27c4ea3a0
web3.eth.getStorageAt("0x838F9b8228a5C95a7c431bcDAb58E289f5D2A4DC", "0x9e6c92d7be355807bd948171438a5e65aaf9e4c36f1405c1b9ca25d27c4ea3a0")
# 0x7465737400000000000000000000000000000000000000000000000000000008
web3.utils.hexToUtf8("0x7465737400000000000000000000000000000000000000000000000000000008")
# test

最後に

今回は、コントラクトのストレージに格納されているデータに直接アクセスして、コントラクトのどこにどのようにデータが保存されているかを見てきました。
いかがだったでしょうか?
普段あまり意識しない部分だと思いますが、仕組みを知るとより興味を持てると思います。
このような技術記事を今後も発信していきます。

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

https://twitter.com/cardene777

https://chaldene.net/

https://qiita.com/cardene

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

参考

https://programtheblockchain.com/posts/2018/03/09/understanding-ethereum-smart-contract-storage/

https://medium.com/@flores.eugenio03/exploring-the-storage-layout-in-solidity-and-how-to-access-state-variables-bf2cbc6f8018

https://solidity-by-example.org/hacks/accessing-private-data/

https://www.youtube.com/watch?v=Gg6nt3YW74o

https://scrapbox.io/sushiether/web3.js_web3.utils.soliditySha3

https://solidity-by-example.org/hacks/accessing-private-data/

CryptoGames

Discussion