foundry chiselについて
はじめに
スマートコントラクト開発している時、ちょっとした動作確認したいことありますよね?
例えば、function の bytes コードとか error の bytes コードとか・・・
なんだったら、120 の bytes ってなに?とか
少し前までは、下記のようなコードを叩いて取得していました。
function getBytes(string memory str) public returns (bytes4) {
return bytes4(keccak256(bytes(str)));
}
inline assembly を用いての開発においては、特に bytes が必要になってくるので、非常に非効率ですよね。
それを解決してくれるのが、foundry chisel です!
foundry chisel とは
foundry chisel は、完全に機能する Solidity REPL で、コマンドラインで Solidity を直接書き、実行、デバッグできます。もちろん、新しい solidity ファイルにエクスポートすることもできます。
chisel 使い方
早速使っていきましょう!
chisel
でスタートしていきます。まずは、変数の定義から
uint a = 1;
uint b = 2;
次に変数の確認を行います。
a
すると、
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000001
└ Decimal: 1
同じように、色々入力してみましょう。
➜ b
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000002
└ Decimal: 2
➜ a+b
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000003
└ Decimal: 3
➜ b-a
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000001
└ Decimal: 1
➜ a-b
Traces:
[348] 0xBd770416a3345F91E4B34576cb804a576fa48EB1::run()
└─ ← "Arithmetic over/underflow"
⚒️ Chisel Error: Failed to inspect expression
➜ a*b
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000002
└ Decimal: 2
➜ a/b
Type: uint
├ Hex: 0x0
└ Decimal: 0
b-a
のエラーもちゃんと補足してくれています。素晴らしいですね!
もちろん bit シフトなどもお手のもの
➜ b = a<<0x08
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000100
└ Decimal: 256
ちなみに初めにに書いていた 120 の bytes 型はこんな感じで取得できます。
➜ uint256 c = 120
➜ c
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000078
└ Decimal: 120
bytes の取得は、
➜ bytes4 sign
➜ sign = bytes4(keccak256(bytes("test")));
➜ sign
Type: bytes32
└ Data: 0x9c22ff5f00000000000000000000000000000000000000000000000000000000
無茶苦茶効率上がりますね!
スタック割り当て
アドレス型のスタック割り当てを見てみましょう。アドレス型は、160bits なので、先頭の 12 個が0でパディング埋めがされるはずです。
address addr
addr = 0x82fEDBC04ddB1BDf63754DFBf97a804C644b5525
Type: address
└ Data: 0x82fEDBC04ddB1BDf63754DFBf97a804C644b5525
➜ !rs addr
Type: bytes32
└ Data: 0x00000000000000000000000082fedbc04ddb1bdf63754dfbf97a804c644b5525
ちゃんと生のスタック割り当ての形で表示されましたね!次に、文字列をぶちこんでみましょう!
➜ string memory str
➜ str = "abc"
str = "abc"
Type: string
├ UTF-8: abc
├ Hex (Memory):
├─ Length ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000003
├─ Contents ([0x20:..]): 0x6162630000000000000000000000000000000000000000000000000000000000
├ Hex (Tuple Encoded):
├─ Pointer ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000020
├─ Length ([0x20:0x40]): 0x0000000000000000000000000000000000000000000000000000000000000003
└─ Contents ([0x40:..]): 0x6162630000000000000000000000000000000000000000000000000000000000
良い感じで出力されますね!
パッキング
次にパッキングを体験していきましょう。
32bytes すなわち 256bits が一つの単位です。アドレスは、160bits なので、あと 96bits 格納することができます。
今回のパッキングでは、アドレスと数量を格納してみます。パッキングのメインは、packed = count << 160 | uintAddr
ですね。
➜ address addr = 0x82fEDBC04ddB1BDf63754DFBf97a804C644b5525
➜ addr
Type: address
└ Data: 0x82fEDBC04ddB1BDf63754DFBf97a804C644b5525
➜ uint256 uintAddr = uint256(uint160(addr))
➜ uintAddr
Type: uint
├ Hex: 0x00000000000000000000000082fedbc04ddb1bdf63754dfbf97a804c644b5525
└ Decimal: 747852332515847654753637389460732727376986461477
➜ uint256 count = 120
➜ count
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000078
└ Decimal: 120
➜ uint256 packed
➜ packed = count << 160 | uintAddr
Type: uint
├ Hex: 0x00000000000000000000007882fedbc04ddb1bdf63754dfbf97a804c644b5525
└ Decimal: 176128048812224197839195817315414695086088891618597
綺麗にパッキングされているのがよくわかります。素晴らしい!
コントラクト作成
コントラクトを作成してみましょう!ここでややこしいのが変数の定義は外だしする必要があります。
contract Test {
function add(uint256 a,uint256 b) public returns (uint256){
return a+b;
}
}
コントラクトが書けたら、コントラクトの発行を行いましょう
Test t = new Test()
最後にコントラクトの評価を行いましょう。
uint256 c
➜ c
Type: uint
├ Hex: 0x0
└ Decimal: 0
c = t.add(3,4)
Type: uint
├ Hex: 0x0000000000000000000000000000000000000000000000000000000000000007
└ Decimal: 7
バッチリですね!ちなみにですが、function を書き間違えると指摘してくれます。
function add(uint256 a, uint256 b) returns (uint256) {
return a + b;
}
これを入力すると、
Compiler errors:
Error (4937): No visibility specified. Did you intend to add "public"?
--> ReplContract.sol:10:5:
|
10 | function add(uint256 a, uint256 b) returns (uint256) {
| ^ (Relevant source part starts here and spans across multiple lines).
素晴らしい!今回作ったものを出力してみましょう
!export
すると、script/REPL.s.sol
に吐き出されます。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;
import {Script} from "forge-std/Script.sol";
contract Test {
function add(uint256 a, uint256 b) public returns (uint256) {
return a + b;
}
}
contract REPL is Script {
/// @notice Script entry point
function run() public {
Test t = new Test();
uint256 c;
c = t.add(3, 4);
}
}
全部手打ちなので、実用的かどうかわかりませんが、なかなかの優れもの。
メモリーを取得
一度メモリーを消してください。
!clear
次に、先ほどのコントラクトを発行し、同様の操作を行います。
contract Test {
function add(uint256 a,uint256 b) public returns (uint256){
return a+b;
}
}
Test t = new Test();
uint256 c;
c = t.add(3,4);
その後、!memdump
してみましょう
➜ !memdump
[0x00:0x20]: 0x0000000000000000000000000000000000000000000000000000000000000000
[0x20:0x40]: 0x0000000000000000000000000000000000000000000000000000000000000000
[0x40:0x60]: 0x00000000000000000000000000000000000000000000000000000000000000a0
[0x60:0x80]: 0x0000000000000000000000000000000000000000000000000000000000000000
[0x80:0xa0]: 0x0000000000000000000000000000000000000000000000000000000000000007
[0xa0:0xc0]: 0x0000000300000000000000000000000000000000000000000000000000000000
[0xc0:0xe0]: 0x0000000402f714610030575b600080fd5b61004a600480360381019061004591
[0xe0:0x100]: 0x906100b1565b610060565b6040516100579190610100565b60405180910390f3
[0x100:0x120]: 0x5b6000818361006e919061014a565b905092915050565b600080fd5b60008190
[0x120:0x140]: 0x50919050565b61008e8161007b565b811461009957600080fd5b50565b600081
[0x140:0x160]: 0x3590506100ab81610085565b92915050565b600080604083850312156100c857
[0x160:0x180]: 0x6100c7610076565b5b60006100d68582860161009c565b92505060206100e785
[0x180:0x1a0]: 0x82860161009c565b9150509250929050565b6100fa8161007b565b8252505056
[0x1a0:0x1c0]: 0x5b600060208201905061011560008301846100f1565b92915050565b7f4e487b
[0x1c0:0x1e0]: 0x7100000000000000000000000000000000000000000000000000000000600052
[0x1e0:0x200]: 0x601160045260246000fd5b60006101558261007b565b91506101608361007b56
[0x200:0x220]: 0x5b92508282019050808211156101785761017761011b565b5b9291505056fea2
[0x220:0x240]: 0x646970667358221220c1fc4c4c1b65cac94c21366d3ac1b1a5ba6403dacff98d
[0x240:0x260]: 0x6ccf6151f9dd7aeb8b64736f6c63430008120033000000000000000000000000
凄くないですか?inline assembly での開発がかなり捗りますね
inline assembly
inline assembly でメモリー操作してみましょう。
次のコードは、0x20 に 1 を書き込むメモリー操作です。
assembly{
mstore(0x20,1)
}
!memdump
すると
➜ !memdump
[0x00:0x20]: 0x0000000000000000000000000000000000000000000000000000000000000000
[0x20:0x40]: 0x0000000000000000000000000000000000000000000000000000000000000001
[0x40:0x60]: 0x0000000000000000000000000000000000000000000000000000000000000080
素晴らしい!先ほどの packed をやってみましょう
address addr = 0x82fEDBC04ddB1BDf63754DFBf97a804C644b5525
次のコードは、メモリーポインター 0x20 の位置にアドレスを、0x0c の位置に数字の 1 を書き込んでいます。
※32bytes 単位で書き込まれるため、順番が大切です。
assembly{
mstore(0x20,addr)
mstore(0x0c,1)
}
!memdump
すると
➜ !memdump
[0x00:0x20]: 0x0000000000000000000000000000000000000000000000000000000000000000
[0x20:0x40]: 0x00000000000000000000000182fedbc04ddb1bdf63754dfbf97a804c644b5525
[0x40:0x60]: 0x0000000000000000000000000000000000000000000000000000000000000080
ちゃんと 0x20 の位置に書き込まれていますね!
inline assembly で取り出してみましょう。まずは、変数を定義して
uint256 d
次のコードは、メモリーポインター 0x20 の値を取り出しています。
assembly{
d := mload(0x20)
}
d
で値を出力すると、
➜ d
Type: uint
├ Hex: 0x00000000000000000000000182fedbc04ddb1bdf63754dfbf97a804c644b5525
└ Decimal: 2209353969846750572957322222177015747032919004453
完璧ですね!
いかがだったでしょうか?これをいつもノートに書いてメモリー構造をいじくり回してた私にとっては、かなり捗るようになりました。
もし記事があなたのお役に立ったなら、ぜひ「いいね!」ボタンをクリックしてくださいね。
Discussion