T9 · ERC Standards
標準は何を解決する?
- 練習する能力
- 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% を引き、相手へコールバックし、または何もせずイベントだけ出すこともあります。
シグネチャの一致は、挙動の一致ではありません。 三日間の問題はすべてここから来ます。
- 標準がインターフェースを約束
- 連携可能性を得る
- 挙動は制約できない
- 組み込む側が結果を検証する
あなたが決める
プールでどう受け取りますか。四案です。
結果を観察する
四案と三事故を比較します。
| 方法 | 返却なし | 送金手数料 | 自動残高変動 | 悪意の通貨 |
|---|---|---|---|---|
| 直接呼ぶ | 防げない | 防げない | 防げない | 防げない |
| 返却対応 | 防げる | 防げない | 防げない | 防げない |
| 残高差 | 防げる | 防げる | 防げない | 防げない |
| 許可リスト | 防げる | 防げる | 防げる | 大部分 |
二つの結論です。
第一に、標準はインターフェースで、保証ではありません。 呼べることと安全に組み込めることは別で、標準は前者だけを与えます。
第二に、防御は積み重ねます。 成熟した実装は返却対応、残高差、許可リストを全部使います。各層が別問題を防ぎ、欠ければ事故が増えます。
T19 の手数料通貨のスリッページ、T22 の変動担保でも再登場します。今の十分が二章分の混乱を減らします。
モデルを作る
標準の三層
ERC20 準拠には三段階の異なる約束があります。
| 層 | 内容 | 強制力 |
|---|---|---|
| インターフェース | 関数、型、返却、イベント | コンパイラは確認できても正しい実装を強制しない |
| 意味 | 処理、残高保存、イベント時刻 | 自主性だけで、チェーンは確認しない |
| エコシステム | ウォレット表示、解析、DEX 価格 | 他人のコードが決める |
事故の大多数は、インターフェースが合っても意味が違う第二層です。
三つの通貨規格
| ERC20 | ERC721 | ERC1155 | |
|---|---|---|---|
| 単位 | 分割可能な量 | 分割できない一品 | 両方 |
| 識別 | 残高だけ | 固有 ID | ID ごとの量 |
| 送金 | 数量 | 特定 ID | 複数 ID と量 |
| 許可 | アドレスごとの量 | 個別 ID または全部 | 全部 |
| 受取保護 | なし | コントラクトへ確認コールバック | 同じ。バッチ対応 |
ERC20 は量、ERC721 はどれ、ERC1155 はどの種類をいくつです。
確認コールバックは NFT の永久凍結を防ぎますが、送金中に相手へ実行権を渡します。T28 で扱う安全性の要点です。
許可モデル
コントラクトは勝手に資金を取れず、二段階です。
第一 自分 → トークン:allowance[自分][プロトコル] = 100
第二 自分 → プロトコル:100 を預ける
プロトコル → トークン:transferFrom(自分, プロトコル, 100)
allowance を確認し、減らして送金解決と同時に三問題を生みます。
- 二取引です。 初回は二署名で、最初の Gas は追加費用です。
- 許可が残ります。 ウォレットは多くの場合無制限を選び、各プロトコルがいつでも全額取れる許可が何十も残ります。管理されなくなったものもあります。
- 変更には競合があります。 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 の通貨で固定すると桁を誤ります。
- 無制限は減らさない最適化で、一度の許可が永久に残ります。
何と呼ぶか
分割可能な通貨の残高、送金、許可、二イベントを定義します。
金融の最小共通項です。インターフェースを約束しても、挙動は約束しません。この章の要点です。
分割できない固有 ID の所有者を管理し、量ではありません。
受取確認は保護であり、外部へ実行権を渡す入口でもあります。
一コードに複数 ID と数量を持ち、バッチ送金に対応します。
ゲーム道具、券、同種の証票など、多種類を扱い費用を削減します。
あるアドレスが持ち主から引き出せる量です。
既定は無制限(規格でなくウォレットの選択)、使用後も自動で消えず、変更に競合があります。
取引をオフライン署名へ変え、プロトコルが提出します。
一取引を省いても、「署名だけ」のつもりが無制限許可という詐欺を生みます。内容を正確に示す必要があり、T13 で扱います。
違う作者が違う時期に書いたコードが、共通インターフェースで連携することです。
標準が存在する理由は、会ったことのない二コードを協力させることです。
実践
シグネチャと挙動の不一致を自分で見るのが目的です。
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
二段階で、第二が実作業です。
第一段階:
実世界の ERC20 の非標準実装を列挙してください。
違反する条項、影響する呼び出し側、防御を、頻度順に示してください。
第二段階:
最初の種類のメインネット実例と、自分で検証する方法を示してください。
確信できない具体例は、明確に不確かとしてください。返却なし、false、手数料、自動変動、停止、拒否リスト、18 以外、ゼロ宛先、許可の事前ゼロ化など、一覧は通常充実します。組み込みチェック表に使えます。
第二では、形式もチェックサムも正しくても、何も存在しないアドレスをモデルが作れると分かります。実際の照合率は低いものです。
42 文字列の正確な記憶は難しいのです。正確な記憶が必要なものは自分で確認し、分類と列挙はモデルで加速します。 この境界を覚えます。
自分で取引量順の主要通貨の transfer ソースを読みます。三十分で少なくとも三種類の実例を探せます。
AI の回答後、自分で必ず検証すること
- 各種類について実コントラクトを一つ以上探し、ソースを自分で読む
- モデルのアドレスは創作を疑い、一件ずつエクスプローラーで確認する
- 送金手数料と自動残高変動を別問題として扱う
- 互換呼び出しがデータ長ゼロを正しく扱う
- EIP とライブラリ関数が実在するか確認する
- 各防御が防ぐ種類と防がない種類を説明できる
実例
最大級のドル通貨の一つは、transfer と transferFrom に返却がありません。
インターフェースの詳細が固まる前の配備で、規模が大きすぎて準拠のため変更できません。生態系が合わせ、多くの互換ラッパーがこれに対応します。
不変のシステムでは、先行実装が書面と違っても事実上の標準になります。
配当、焼却、宣伝用に一定割合を引く通貨があります。
amount の全額を記帳すると、取引量とともに不足が増え、最後の退出者が出金できなくなります。
出金側も引かれるため、出力額の計算まで誤ります。多くの主要プロトコルが非対応とする、許可リストの理由です。
受取を知らせるため、送金中にフックを呼ぶ規格があります。
担保に受け入れたプロトコルへ、フックから再度入金を呼び、最初の記帳前に同資金を何度も計上して他資産を借りました。
送金は静かな原子操作とは限りません。相手へ実行権を渡すか確認し、T28 で全体を学びます。
一度承諾すれば永久で、活動するウォレットには何十、何百もの許可が残ります。
攻撃者はプロトコルを破らず、偽ページで permit を署名させ、または悪意のコードへ許可させます。直後は何もなく、忘れた数日後に取ります。
対策は正確な署名内容の表示と、定期的な撤回です。
変数を一つ変える
18 固定の計算は桁を誤り、価格、清算、表示が壊れます。
コントラクトから decimals を取得してキャッシュします。ただし decimals は任意項目なので未実装もあり、失敗も扱います。
入金、出金、清算がすべて止まります。
停止可能な担保と送金依存の清算では、急落時の不良債権を自社が負います。
transfer だけでなく管理関数も読みます。
今日準拠でも明日は違うかもしれず、交換可能なコードに全仮定が依存します。
主要通貨も更新可能なので禁止ではありません。更新者、発効時間、遅延を確認します。 T29 の安全性です。
三日の事故を防げますが、誰でも上場という利点を失います。
追加権限、誤追加の責任、悪意の投票追加という統治の問題になります。事故はコードより手順からも起きます。
開放性と安全性は取捨選択で、閉じて始め必要に応じ開くほうが管理しやすいものです。
持ち帰る問い
標準は機能を増やすのでなく、見知らぬ二コードを連携させます。新規格の評価では、どんな二種類を新たにつなぐか問います。
Token は共有する記帳インターフェースです。許可や自社 API を通さず他コードが直接使えるため役立ちます。
リスクは組み込む側です。作者が何を書いても、資金は自社のプールから消えます。返却対応、残高差、許可リストはそれぞれユーザーを守る層です。
章末チェック
宣言型の bool を復号しようとして、返却がなく長さ不足になるためです。
低レベルで成功を確認し、空か true を受け入れるのが主なラッパーの方式です。
いいえ。エラーなしと受取量は別です。
手数料や自動変動でも true を返します。実受取は残高差で見ます。
悪意のコードは任意の処理ができるので、さらに許可リストが必要です。
100 から 50 の変更を見た相手が先に 100 を使い、変更後に 50 を使うと 150 になります。
先にゼロ化する歴史的対処を強制する通貨もあります。期限付きの正確な permit を使う方法もあります。
両方です。
扱えないコントラクトで NFT が永久に凍るのを防ぎます。
同時に相手へ実行権を渡し、記帳が後なら再入できます。
送金行の後に、送金前の状態へ依存する処理が残るかを見ます。
唯一の正解はありません。次を参考にします。
transferとtransferFromの返却、失敗時の巻き戻しか false か。- 全額届くか。手数料、焼却、配当があるか。
- rebase や利息で自動変動するか。
decimalsの値と実装有無。- 停止、凍結、拒否リスト、発行、更新の管理権限と遅延。
- 送金中の外部呼び出しやフック。
- 総量が残高合計か。ゼロアドレス残高があるか。
一項目でも答えられなければ、まだ許可しません。 不明そのものが答えです。
要点
標準は組み合わせ可能性の前提であり、機能一覧ではない。