Crypto OS
Technical Crypto OS第2段階 · スマートコントラクト開発

T9 · ERC Standards

標準は何を解決する?

この章を 3D で体験する →
練習する能力
BuilderProtocol Literacy
実践
ERC20 を実装し、あえて bool を返さない transfer 関数を作って、どの呼び出し元が失敗するか確認する。
AI Lab
AI に ERC-20 の典型的な非標準実装を列挙させ、メインネットで実例を一つ自分で探す。

現実の問題

任意の ERC20 に対応すると説明するステーキングプールを作りました。核心は単純です。

token.transferFrom(msg.sender, address(this), amount);
shares[msg.sender] += amount;

公開から三日で、三つの出来事が起きます。

一日目:主要ステーブルコインの入金が失敗し、エラーから理由が分かりません。コードを変えず通貨を変えると成功します。

二日目:3% の送金税がある通貨を預けられ、受取は 97、記帳は 100 です。通知もなく債務超過になります。

三日目:残高が自動変動する通貨で、100 を預け翌日 105 を受け取ります。余分の 5 は他人の持分から出ます。

同じコードです。transferFrom の使い方は標準にあるのに、なぜ事故になるのでしょう。

思考実験

標準のない世界を考えます。

発行者が send、pay、moveTokens など独自の名前と引数順を決めます。

百通貨を表示するウォレットは百実装、取引所も百、貸付も百です。費用は N × Mで、新通貨のたび全員がコードを変え再公開します。

標準で transfer の宛先と数量、balanceOf の残高照会を決めます。

費用は N × M から N + M へ変わり、さらに魔法のようなことが起きます。

2018 年の取引所コードが、2025 年に作られた通貨と、作者同士が会わず、どちらも更新せずに連携できます。

これが合成可能性です。機能の多さでなく、書く時に相手を知る必要がないことです。

ただし標準が約束するのは名前、引数、返却型で、実行後の世界の状態は約束できません。

正しいシグネチャの transfer が 3% を引き、相手へコールバックし、または何もせずイベントだけ出すこともあります。

シグネチャの一致は、挙動の一致ではありません。 三日間の問題はすべてここから来ます。

  1. 標準がインターフェースを約束
  2. 連携可能性を得る
  3. 挙動は制約できない
  4. 組み込む側が結果を検証する
第三段階が最も飛ばされやすく、問題はそこに集まります。

あなたが決める

プールでどう受け取りますか。四案です。

結果を観察する

四案と三事故を比較します。

方法返却なし送金手数料自動残高変動悪意の通貨
直接呼ぶ防げない防げない防げない防げない
返却対応防げる防げない防げない防げない
残高差防げる防げる防げない防げない
許可リスト防げる防げる防げる大部分

二つの結論です。

第一に、標準はインターフェースで、保証ではありません。 呼べることと安全に組み込めることは別で、標準は前者だけを与えます。

第二に、防御は積み重ねます。 成熟した実装は返却対応、残高差、許可リストを全部使います。各層が別問題を防ぎ、欠ければ事故が増えます。

T19 の手数料通貨のスリッページ、T22 の変動担保でも再登場します。今の十分が二章分の混乱を減らします。

モデルを作る

標準の三層

ERC20 準拠には三段階の異なる約束があります。

層内容強制力
インターフェース関数、型、返却、イベントコンパイラは確認できても正しい実装を強制しない
意味処理、残高保存、イベント時刻自主性だけで、チェーンは確認しない
エコシステムウォレット表示、解析、DEX 価格他人のコードが決める

事故の大多数は、インターフェースが合っても意味が違う第二層です。

三つの通貨規格

ERC20ERC721ERC1155
単位分割可能な量分割できない一品両方
識別残高だけ固有 IDID ごとの量
送金数量特定 ID複数 ID と量
許可アドレスごとの量個別 ID または全部全部
受取保護なしコントラクトへ確認コールバック同じ。バッチ対応

ERC20 は量、ERC721 はどれ、ERC1155 はどの種類をいくつです。

確認コールバックは NFT の永久凍結を防ぎますが、送金中に相手へ実行権を渡します。T28 で扱う安全性の要点です。

許可モデル

コントラクトは勝手に資金を取れず、二段階です。

第一  自分 → トークン:allowance[自分][プロトコル] = 100
第二  自分 → プロトコル:100 を預ける
      プロトコル → トークン:transferFrom(自分, プロトコル, 100)
      allowance を確認し、減らして送金

解決と同時に三問題を生みます。

  1. 二取引です。 初回は二署名で、最初の Gas は追加費用です。
  2. 許可が残ります。 ウォレットは多くの場合無制限を選び、各プロトコルがいつでも全額取れる許可が何十も残ります。管理されなくなったものもあります。
  3. 変更には競合があります。 100 から 50 にする前に 100 を使われ、変更後さらに 50 を使われ得ます。

Permit は第一への回答です。nonce と期限付きのメッセージへ署名し、取引も Gas もなしで渡します。プロトコルの取引に含め検証して設定し、一取引になります。

第二は習慣で解決します。不要な許可を定期的に確認し撤回します。T13 で試します。

最小の ERC20

外部ライブラリなしでコンパイルできる実装です。

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

contract MiniToken {
    string public name = "Mini";
    string public symbol = "MINI";
    uint8 public decimals = 18;          // 18 は必須でなく、最も一般的なだけ

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor(uint256 initialSupply) {
        totalSupply = initialSupply;
        balanceOf[msg.sender] = initialSupply;
        // 発行はゼロアドレスからの送金として記録し、外部索引が総量を再構築
        emit Transfer(address(0), msg.sender, initialSupply);
    }

    function transfer(address to, uint256 value) external returns (bool) {
        _transfer(msg.sender, to, value);
        return true;
    }

    function approve(address spender, uint256 value) external returns (bool) {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }

    function transferFrom(address from, address to, uint256 value) external returns (bool) {
        uint256 allowed = allowance[from][msg.sender];
        if (allowed != type(uint256).max) {        // 無制限は減らさず、書き込みを一回省く
            require(allowed >= value, "allowance");
            allowance[from][msg.sender] = allowed - value;
        }
        _transfer(from, to, value);
        return true;
    }

    function _transfer(address from, address to, uint256 value) internal {
        require(to != address(0), "to zero");
        uint256 bal = balanceOf[from];
        require(bal >= value, "balance");
        balanceOf[from] = bal - value;
        balanceOf[to] += value;
        emit Transfer(from, to, value);
    }
}

三点を確認します。

  • 発行はゼロアドレスから、焼却はゼロへという慣行です。コードの要件より生態系の約束で、従わないと外部の総量表示が誤ります。
  • decimals は必ず 18 ではありません。 6 や 8 の通貨で固定すると桁を誤ります。
  • 無制限は減らさない最適化で、一度の許可が永久に残ります。

何と呼ぶか

ERC20代替可能トークンの規格

分割可能な通貨の残高、送金、許可、二イベントを定義します。

金融の最小共通項です。インターフェースを約束しても、挙動は約束しません。この章の要点です。

ERC721代替不可能トークンの規格

分割できない固有 ID の所有者を管理し、量ではありません。

受取確認は保護であり、外部へ実行権を渡す入口でもあります。

ERC1155複数トークンの規格

一コードに複数 ID と数量を持ち、バッチ送金に対応します。

ゲーム道具、券、同種の証票など、多種類を扱い費用を削減します。

Allowance / Approve許可額

あるアドレスが持ち主から引き出せる量です。

既定は無制限(規格でなくウォレットの選択)、使用後も自動で消えず、変更に競合があります。

Permit署名による許可

取引をオフライン署名へ変え、プロトコルが提出します。

一取引を省いても、「署名だけ」のつもりが無制限許可という詐欺を生みます。内容を正確に示す必要があり、T13 で扱います。

Composability合成可能性

違う作者が違う時期に書いたコードが、共通インターフェースで連携することです。

標準が存在する理由は、会ったことのない二コードを協力させることです。

実践

実践ERC20 と bool を返さない版を作り、失敗する呼び出しを観察するSolidity 開発フレームワーク + ローカルノード0 円、ローカルとテストネットのみ

シグネチャと挙動の不一致を自分で見るのが目的です。

MiniToken をローカルへ配備します。 送金、許可、引き出しを試し、イベントを確認します。

bool を返さない版にします。 シグネチャだけ変えます。

function transfer(address to, uint256 value) external {
    _transfer(msg.sender, to, value);
}

送金は実際に動き、インターフェース宣言と違うだけです。

二つの呼び出し側を作ります。

interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

// 厳格版:宣言したインターフェースで呼ぶ
contract StrictConsumer {
    function push(address token, address to, uint256 value) external {
        require(IERC20(token).transfer(to, value), "transfer failed");
    }
}

// 互換版:低レベルで返却を判断
contract TolerantConsumer {
    function push(address token, address to, uint256 value) external {
        (bool ok, bytes memory data) = token.call(
            abi.encodeWithSignature("transfer(address,uint256)", to, value)
        );
        // 空の返却か、true を受け入れる
        require(ok && (data.length == 0 || abi.decode(data, (bool))), "transfer failed");
    }
}

二呼び出し側と二通貨の四組を試し、失敗箇所を記録します。

厳格版は返却なしで巻き戻ります。bool の復号に足りず、一日目の理由になります。

3% の手数料版を作ります。 _transfer で手数料先へ引き、ほかは変えません。

二呼び出し側とも成功します。true とイベントは正常でも、受取は 3% 少なくなります。

核心はその防御がまったく効かないことです。

残高差の第三版を試します。 push で前後の受取残高を読み、実受取を計算します。

三通貨で正しい受取額を確認します。

テストネットの表示を見ます。 イベントだけを見るエクスプローラーでは、二版は多くの場合同じです。

正しい表示は安全な連携を意味しません。 実資産を使わず確認します。

AI Lab

AI LabAI の非標準 ERC20 一覧から、メインネットの実例を自分で探すLevel 2 · AI Copilot

二段階で、第二が実作業です。

第一段階:
実世界の ERC20 の非標準実装を列挙してください。
違反する条項、影響する呼び出し側、防御を、頻度順に示してください。

第二段階:
最初の種類のメインネット実例と、自分で検証する方法を示してください。
確信できない具体例は、明確に不確かとしてください。

返却なし、false、手数料、自動変動、停止、拒否リスト、18 以外、ゼロ宛先、許可の事前ゼロ化など、一覧は通常充実します。組み込みチェック表に使えます。

第二では、形式もチェックサムも正しくても、何も存在しないアドレスをモデルが作れると分かります。実際の照合率は低いものです。

42 文字列の正確な記憶は難しいのです。正確な記憶が必要なものは自分で確認し、分類と列挙はモデルで加速します。 この境界を覚えます。

自分で取引量順の主要通貨の transfer ソースを読みます。三十分で少なくとも三種類の実例を探せます。

AI の回答後、自分で必ず検証すること

  • 各種類について実コントラクトを一つ以上探し、ソースを自分で読む
  • モデルのアドレスは創作を疑い、一件ずつエクスプローラーで確認する
  • 送金手数料と自動残高変動を別問題として扱う
  • 互換呼び出しがデータ長ゼロを正しく扱う
  • EIP とライブラリ関数が実在するか確認する
  • 各防御が防ぐ種類と防がない種類を説明できる

実例

bool を返さない大規模ステーブルコイン

最大級のドル通貨の一つは、transfer と transferFrom に返却がありません。

インターフェースの詳細が固まる前の配備で、規模が大きすぎて準拠のため変更できません。生態系が合わせ、多くの互換ラッパーがこれに対応します。

不変のシステムでは、先行実装が書面と違っても事実上の標準になります。

手数料通貨がプールの記帳を狂わせる

配当、焼却、宣伝用に一定割合を引く通貨があります。

amount の全額を記帳すると、取引量とともに不足が増え、最後の退出者が出金できなくなります。

出金側も引かれるため、出力額の計算まで誤ります。多くの主要プロトコルが非対応とする、許可リストの理由です。

コールバック通貨の再入攻撃2020 年、ある貸付プロトコル

受取を知らせるため、送金中にフックを呼ぶ規格があります。

担保に受け入れたプロトコルへ、フックから再度入金を呼び、最初の記帳前に同資金を何度も計上して他資産を借りました。

送金は静かな原子操作とは限りません。相手へ実行権を渡すか確認し、T28 で全体を学びます。

無制限許可と詐欺署名

一度承諾すれば永久で、活動するウォレットには何十、何百もの許可が残ります。

攻撃者はプロトコルを破らず、偽ページで permit を署名させ、または悪意のコードへ許可させます。直後は何もなく、忘れた数日後に取ります。

対策は正確な署名内容の表示と、定期的な撤回です。

変数を一つ変える

もしdecimals が 18 でなく 6

18 固定の計算は桁を誤り、価格、清算、表示が壊れます。

コントラクトから decimals を取得してキャッシュします。ただし decimals は任意項目なので未実装もあり、失敗も扱います。

もし発行者が送金を停止できる

入金、出金、清算がすべて止まります。

停止可能な担保と送金依存の清算では、急落時の不良債権を自社が負います。

transfer だけでなく管理関数も読みます。

もし通貨自体が更新可能

今日準拠でも明日は違うかもしれず、交換可能なコードに全仮定が依存します。

主要通貨も更新可能なので禁止ではありません。更新者、発効時間、遅延を確認します。 T29 の安全性です。

もし許可リストだけ受け入れる

三日の事故を防げますが、誰でも上場という利点を失います。

追加権限、誤追加の責任、悪意の投票追加という統治の問題になります。事故はコードより手順からも起きます。

開放性と安全性は取捨選択で、閉じて始め必要に応じ開くほうが管理しやすいものです。

持ち帰る問い

1
どんな問題を解決するか?

標準は機能を増やすのでなく、見知らぬ二コードを連携させます。新規格の評価では、どんな二種類を新たにつなぐか問います。

3
なぜトークンが必要なのか?

Token は共有する記帳インターフェースです。許可や自社 API を通さず他コードが直接使えるため役立ちます。

9
誰がリスクを負うか?

リスクは組み込む側です。作者が何を書いても、資金は自社のプールから消えます。返却対応、残高差、許可リストはそれぞれユーザーを守る層です。

章末チェック

要点

標準は組み合わせ可能性の前提であり、機能一覧ではない。

この章の実践課題を終えたらチェックしてください進捗をすべて見る

このページの内容