📘

foundry chiselについて

2023/10/21に公開

はじめに

スマートコントラクト開発している時、ちょっとした動作確認したいことありますよね?
例えば、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に吐き出されます。

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