こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
このブログ以外でも情報発信しているので、よければ他の記事も見ていってください。
https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58
今回は「プライベート変数」について取り上げていきます。
Solidityには「public
」と「private
」の2つの変数が存在します。
名前の通り「public
」は公開されているデータで、「private
」は公開されていないデータです。
しかし、実は「private
」データは見ることができるんです...。
Solidityスキルを1段階あげたい方やバグ・バウンティに参加したいバグハンターを目指している方は必須の知識となるので、この記事で学んでいってください。
バグ・バウンティが何かわからない人は以下の記事を読んでください。
プライベートデータとは?
まずはプライベートデータが何かから確認していきましょう。
プライベートデータを一言で
プレイベートデータとは、一言で言うと「宣言したコントラクト内からのみアクセス可能な変数」のことです。
宣言したコントラクト内からのみアクセスできるため、他のコントラクトや継承先のコントラクトからはアクセスができない。
ストレージデータの保管方法
ストレージデータがどのように格納されているか知っているでしょうか?
スマートコントラクトでは、以下のように32バイト
ずつのスロットと言われるものが2^256
個積み重なっています。
このスロットに宣言順でデータが格納されています。
スロットに格納する時は右から格納され、1スロット内のバイト数が32バイト
に収まる場合は複数のデータ同じスロットに格納されます。
言葉だけではわかりにくいので図で確認していきましょう。
以下のように変数が定義されているとします。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
contract Private {
string name;
uint8 age;
uint8 income;
bytes32 hash;
address walletAddress;
bool engineer;
bool creator;
}
実際に上記の7つの変数を格納すると以下のようになります。
この図を見ることでなんとなくイメージができたのではないでしょうか?
実際にデータを入れると以下のようになります。
先ほどのコードに何番目のスロットに保存されているかのコメントをつけてみます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
contract Private {
string name; // スロット0
uint8 age; // スロット1
uint8 income; // スロット1
bytes32 hash; // スロット2
address walletAddress; // スロット3
bool engineer; // スロット3
bool creator; // スロット3
}
配列やMappingの格納方法は若干異なるのですが、今回の記事の主題ではないので省かせていただきます(今後まとめます)。
ストレージの詳細
ストレージデータの格納方法を確認できたので、ストレージに保管されるデータの詳細の説明をしていきましょう。
プライベートデータはストレージ内にデータが保存されるため、以下のような条件がつきます。
ストレージ内に保存されたデータブロックチェーンに書き込まれるため、ガス代がかかり永続的に保存される。
32バイト
(256ビット
)のスロットを占有するためのガスコストは20,000
ガス。
この際、各スロットがフル稼働(パンパンにデータが詰まっている状態)していなくても、コストは発生します。
ストレージの値を変更するためにかかるガスコストは5,000
ガス。
ストレージスロットのクリーンアップ(0
以外のバイトを0
にすること)時に、一定量のガス代が払い戻される。
これはpublic
データにも共通する内容ですので頭に入れておきましょう。
プライベートデータの閲覧
private
変数に格納した値を誰かにみられることはないと直感的には思いますが、実は見ることができます。
private
変数に格納した値は、他のコントラクトから変更することは防ぐことはできますが、見ることは誰でも可能です。
ブロックチェーンは透明ということを念頭に置いておくと良いですね。
では本当にprivate
変数の値を見ることができるのか確認してみましょう。
Truffleのセットアップ
まずはtruffleのセットアップから行います。
truffleとはスマートコントラクトのコンパイルやテスト、デプロイを自動化するフレームワークです。
truffleを使うことで簡単にprivate
変数に格納されたデータを見れてしまうので、早速見ていきましょう。
まずはtruffleをインストールしていきます。
npm install -g truffle
このコマンドを実行する際、Node.jsをインストールしておく必要があるので、まだNode.jsをインストールしていない方は、以下の記事を参考にインストールしてください。
https://note.com/npaka/n/nf6412fe9026f
次にディレクトリを作成していきます。
mkdir private_truffle
cd private_truffle
ディレクトリを作成できたら以下を実行してください。
truffle init
これで新たにtruffleプロジェクトが作成できました。
以下のような構成になっているはずです。
.
├── contracts
├── migrations
├── test
└── truffle-config.js
次に以下のコマンドを実行してください。
truffle create contract Private
truffle create test test
このコマンドはContractファイルとテストファイルを作成するコマンドです。
上記コマンドを実行すると以下のような構成になっているはずです。
.
├── contracts
│ └── Private.sol
├── migrations
├── test
│ └── test.js
└── truffle-config.js
では以下のコードをPrivate.sol
に貼り付けてください。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.17;
contract Private {
string private name = "cardene";
uint8 private age = 30;
uint8 private income = 30;
bytes32 private hash = "6a78e3e709447c0b671c573a4b562d6";
address private walletAddress = 0xC255C784Ac559339A99F91a22E2063ff66B83354;
bool private engineer = true;
bool private creator = false;
}
上記貼り付けが完了したら以下のコマンドを実行してください。
truffle compile
上記実行すると以下のような出力が確認できます。
Compiling your contracts...
===========================
> Compiling ./contracts/Private.sololc-bin. Attempt #1
> Artifacts written to /Users/.../build/contracts
> Compiled successfully using:
- solc: 0.8.17+commit.8df45f5f.Emscripten.clang
ディレクトリの構成を確認すると、buildディレクトリが作られているのが確認できます。
.
├── build
│ └── contracts
│ └── Private.json
├── contracts
│ └── Private.sol
├── migrations
├── test
│ └── test.js
└── truffle-config.js
では次の以下のコマンドを実行してください。
truffle develop
そうすると大量のアドレスが出力され、以下のようにプロンプトが表示されます。
truffle(develop)>
では次にmigrate用のファイルを作成していきましょう。
migrationsディレクトリの中に1_initial_private.js
というファイルを作成してください。
作成できたらそのファイルに以下を追記してください。
const Migrations = artifacts.require("Private");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};
追記できたら以下のコマンドを実行してください。
truffle(develop)> migrate
そうすると以下が出力されます。
Deploying 'Private'
-------------------
> transaction hash: 0xe55cecefb706e6dd3c544a587c2ea3d210d60ff5b8007c23ce9fa9ebc8a7a9da
> Blocks: 0 Seconds: 0
> contract address: 0xD4daa96901Ad1112Ec750c84C852D4F6BBc5e83E
> block number: 1
> block timestamp: 1671512753
> account: 0x1F09Ca739ae733511eE0ba49B38Fa72a8557a9A6
> balance: 99.999414407125
> gas used: 173509 (0x2a5c5)
> gas price: 3.375 gwei
> value sent: 0 ETH
> total cost: 0.000585592875 ETH
上記の出力のうち「contract address」の部分だけ使用します。
以下を実行してください。
truffle(develop)> data = "contract addressの値"
例)data = "0xD4daa96901Ad1112Ec750c84C852D4F6BBc5e83E"
'0xD4daa96901Ad1112Ec750c84C852D4F6BBc5e83E'
truffle(develop)> web3.eth.getStorageAt(data, 0)
'0x63617264656e650000000000000000000000000000000000000000000000000e'
何やら長い数字が出力されました。
これがストレージ内に保存されているスロットの値になります。
Nameの取得
1つ目の値は「name」だったので、本当に格納されているのか確認してみましょう。
truffle(develop)> name = "0x63617264656e650000000000000000000000000000000000000000000000000e"
'0x63617264656e650000000000000000000000000000000000000000000000000e'
truffle(develop)> web3.utils.toAscii(name)
'cardene\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0E'
先頭に「cardene」という文字がついていますね!
private
変数にしていたのにこんなに簡単に値を取得できてしまいました。
「cardene」の後ろの\x00
は値がないためこのように格納されています。
ストレージの詳細でもお伝えしたように、スロットの一部にしか値を格納しなくても、ガス代が発生するのはこのためです。
Slot1以降の値を取得
ではせっかくなのでprivate
変数に格納しているすべての値を出力してみましょう。
truffle(develop)> slot1 = web3.eth.getStorageAt(data, 1)
'0x0000000000000000000000000000000000000000000000000000000000001e1e'
truffle(develop)> slot2 = web3.eth.getStorageAt(data, 2)
'0x3661373865336537303934343763306236373163353733613462353632643600'
truffle(develop)> slot3 = web3.eth.getStorageAt(data, 3)
'0x000000000000000000000001c255c784ac559339a99f91a22e2063ff66b83354'
スロットごとに複数の値が格納されている部分もあります。
Slot1
slot1とslot3がそうですね。
早速中身を確認していきましょう。
truffle(develop)> parseInt("0x1e", 16)
30
slot1の末尾の1e
に0x
をつけた値を16進数から10進数に変換すると30
と出力されました。
private
変数に格納されている値と一致するため、このslot1は以下のようになっています。
0x0000000000000000000000000000000000000000000000000000000000003030
Slot2
slot2は以下の値なので、末尾の00
を取り除いて中身を確認します。
0x3661373865336537303934343763306236373163353733613462353632643600
web3.utils.toAscii("0x36613738653365373039343437633062363731633537336134623536326436")
'6a78e3e709447c0b671c573a4b562d6'
指定した値がしっかり格納されていますね。
Slot3
まずはwalletaddressから取得します。
'0x000000000000000000000001c255c784ac559339a99f91a22e2063ff66b83354'
上記のslot3から「000000000000000000000001
」を取り除いて取得します。
truffle(develop)> web3.utils.numberToHex("0xc255c784ac559339a99f91a22e2063ff66b83354")
'0xc255c784ac559339a99f91a22e2063ff66b83354'
指定した通りの値が取得できましたね。
最後にengineerとcreatorのbool値
を取得しましょう。
先ほど取り除いた000000000000000000000001
の末尾2文字ずつが、それぞれengineerとcreatorになります。
engineer = 01
creator = 00
この値を使用して以下を実行します。
truffle(develop)> parseInt("0x01", 16)
1
truffle(develop)> parseInt("0x00", 16)
0
1と0が出力され、それぞれ1がTrue、0がFalseなのでしっかり取得できていますね。
これですべてのprivate変数の値を取得できてしまいました。
いかがでしょうか?
例えprivate変数に値を格納しても、このように値が取得できてしまうのです。
対処法
ではこのprivate
変数から値を見られないようにする方法はあるのでしょうか?
答えはNoで、どうやっても取得できてしまいます。
そのため、パスワードなど公開されると良くない機密データは変数に格納しないでください。
最後に
今回は「プライベートデータ」について解説してきました。
いかがだったでしょうか?
ポイント
- プライベートデータが何かわかった!
- プライベートデータにはアクセスできることと方法がわかった!
- プライベートデータを扱う際の注意点を理解できた!
上記のどれかに当てはまっていたら嬉しいです!
もし何か質問などがあれば以下のTwitterなどから連絡ください!
普段はSolidityやブロックチェーン、Web3についての情報発信をしています。
Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!
参考
https://medium.com/@ashwin.yar/how-to-access-private-data-from-a-smart-contract-eb478fca6e15
https://learnblockchain.cn/article/1759
https://coinsbench.com/accessing-private-data-hack-solidity-4-f94d479432c7
https://nimil.jp/blog/sol-check-private-variables