見出し画像

【技術者向けテックノート02】 スマートコントラクトは契約書ではない

EVM、Solidity、State、Events、Reentrancyを解剖する

はじめに
ステーブルコインやトークン化預金の企画では、「スマートコントラクトで実装する」という表現が頻繁に使われる。しかし、その一言の中には、外部アプリからの呼出し、ABIによるデータ変換、EVM上での命令実行、Storageの更新、Eventの出力、外部システムによる監視まで、複数の処理が含まれている。
スマートコントラクトを「条件が満たされると自動執行される契約書」とだけ理解すると、実装時に重要な論点を見落とす。コードは法律上の契約そのものではなく、ブロックチェーン上の状態を変更するプログラムである。そこには、通常の業務アプリケーションと同様に、入力値、権限、例外、外部呼出し、データ保存、テスト、障害対応が存在する。
本号では、Solidityを初めて読む実務家でも、コードがどのように実行され、どこに状態が保存され、どのような脆弱性が生じるかを追えるようにする。単なるプログラミング入門ではなく、金融トークンのPoCや設計レビューで使える視点を目標とする。


本号の目次
1.最初に結論
2.スマートコントラクトはなぜ「契約書」ではないのか
3.EVMは何をしているのか
4.Solidityコードを最小単位で読む
5.ABIとFunction Selector
6.ここから有料:EVMのデータ領域
7.StorageとEventをどう設計するか
8.call・delegatecall・staticcall
9.Revertと例外処理
10.主要な脆弱性を俯瞰する
11.Reentrancyをコードで理解する
12.トークン規格と状態モデル
13.テスト・Fuzz・形式検証
14.金融PoCの実装チェックリスト
15.まとめ・次回予告


1.最初に結論

スマートコントラクトを理解するうえで、最初に押さえるべき結論は次の五点である。
(1) スマートコントラクトは、法的な契約書ではなく、ブロックチェーン上の状態を変更するプログラムである。
(2) アプリはABIに従って関数名と引数をCall Dataへ変換し、EVMがBytecodeとして実行する。
(3) 残高や権限の真実はStorageにあり、Event Logsは外部システムが処理を検知するためのログである。
(4) 外部コントラクトを呼び出すと制御が相手へ移るため、Reentrancyなどの攻撃経路が生じる。
(5) 金融PoCでは、正常に動くことだけでなく、権限、例外、重複実行、監視、不変条件をテストする必要がある。

図1 スマートコントラクト実行の全体像

図1のポイントは、スマートコントラクトが単独で完結していないことである。ウォレット、RPC、EVM、Storage、Event Logs、業務システムが一つの処理連鎖を構成する。

2.スマートコントラクトはなぜ「契約書」ではないのか

「Smart Contract」という名称から、契約条件をブロックチェーンへ記載し、機械が自動執行するものだと理解されがちである。しかし、技術的には、特定のアドレスに配置されたプログラムコードと状態データの組合せである。
契約書は、当事者、権利義務、準拠法、解除、紛争処理などを文章で定める。一方、スマートコントラクトが直接扱うのは、条件判定、数値計算、アドレス、残高、権限、イベント出力などである。

金融実装では、法的契約、業務規程、スマートコントラクト、オフチェーン台帳の対応関係を明示する必要がある。コードに書かれていない救済や取消しが必要なら、Pause、Freeze、Clawback、手動補正などの実装と規約上の根拠を組み合わせる。

3.EVMは何をしているのか

EVM(Ethereum Virtual Machine)は、スマートコントラクトのBytecodeを実行する仮想計算機である。Solidityのソースコードはコンパイルされ、EVMが理解できるOpcode列へ変換される。
EVMはStack Machineであり、値をStackへ積み、ADD、SSTORE、CALL、LOGなどのOpcodeを順番に処理する。処理ごとにGasを消費し、Gasが尽きるか、条件違反が起こると実行は失敗する。

同じトランザクションを同じ事前状態で実行すれば、各ノードは同じ結果へ到達する。これが決定的実行の基本である。一方、外部Web APIの応答のようにノードごとに結果が変わり得る情報は、EVMから直接参照できない。

4.Solidityコードを最小単位で読む

以下は、送信者の残高を減らし、受取人の残高を増やす最小限の例である。実運用のトークンとしては機能不足だが、状態、関数、条件、イベントの関係を読むには適している。

コード1 残高更新とEvent出力を行う最小例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract SimpleLedger {
mapping(address => uint256) public balances;

event Transfer(
address indexed from,
address indexed to,
uint256 amount
);

error InsufficientBalance(uint256 requested, uint256 available);
error InvalidRecipient();

function transfer(address to, uint256 amount) external {
if (to == address(0)) revert InvalidRecipient();

uint256 available = balances[msg.sender];
if (available < amount) {
revert InsufficientBalance(amount, available);
}

balances[msg.sender] = available - amount;
balances[to] += amount;

emit Transfer(msg.sender, to, amount);
}
}

この例では、残高更新を外部呼出しより前に完了している。後述するReentrancy対策の基本にもつながる。

5.ABIとFunction Selector

アプリケーションは、Solidityの関数名を文字列のままEVMへ送るわけではない。ABI(Application Binary Interface)に従い、関数名と型からFunction Selectorを作り、引数を32バイト単位でエンコードしてCall Dataを構成する。

図2ABIとFunction Selector

Function Selectorは、関数シグネチャのKeccak-256ハッシュの先頭4バイトである。たとえば transfer(address,uint256) は、よく知られた 0xa9059cbb になる。

JSON例1 アプリからノードへ渡される呼出し情報のイメージ
{
"to": "0xTokenContract...",
"data": "0xa9059cbb000000000000000000000000...00000064",
"value": "0x0"
}

ABIは自己記述的ではない。Call Dataだけを見ても、型定義がなければ正しくデコードできない。そのため、コントラクトのABIとデプロイ先アドレスの組合せを、アプリケーション側で厳格に管理する必要がある。
·         ABIのバージョンとデプロイ済みコードが一致しているか
·         MainnetとTestnetでアドレスを取り違えていないか
·         関数オーバーロード時に正しいシグネチャを指定しているか
·         Decimalsを含む金額変換をアプリ側で誤っていないか
·         アップグレード後もABI互換性が維持されているか

ここから先では、EVMのデータ領域、StorageとEventの設計、外部呼出しの違い、Reentrancy、トークン規格の状態モデル、テスト方法まで、PoCや設計レビューに直接使える粒度へ踏み込む。

ここから先は

7,010字 / 13画像

¥ 980

この記事が気に入ったらチップで応援してみませんか?