前章までで、機関の外側に witness(証人)が要ることが分かりました。この章では、その witness を用意します。使うのは日本円ステーブルコイン JPYC と、Polygon Amoy というテストネットです。
暗号資産の知識は前提にしません。躓いた箇所を順に説明します。
JPYC とは何か
JPYC は 2025年10月27日に発行が始まった、日本円連動のステーブルコインです。発行するのは第二種資金移動業者(関東財務局長第00099号、2025年8月18日登録)である JPYC 株式会社で、トークン自体は資金決済法第2条第5項の「電子決済手段」にあたります。本番では 1 JPYC = 1 円で償還できる設計です。
なお、同社が2025年6月に新規発行を終了した JPYC Prepaid(自家型前払式支払手段)とは別のトークンです。名前も記号も小数点桁数も同じ(JPY Coin / JPYC / 18)ですが、コントラクトのアドレスが違い、しかも旧版はチェーンごとにアドレスが異なります。検索で古いアドレスを掴まないよう注意してください。
ただしこの章で扱うのはテストネット版で、円の裏付けはありません。同じコードが別の台帳に配置されているだけで、償還請求権が存在しない、という状態です。数字と単位は同じでも、裏付けが丸ごと無い。だからこそ、いくら失敗しても損失ゼロの実験場として使えます。
「JPYC を受け取る」だけでは何も指定できていない
最初に引っかかったのがここでした。Faucet(テスト用トークンを無償配布する仕組み)の画面には「ネットワーク」を選ぶ欄があり、なぜ JPYC を受け取るのにネットワークの指定が要るのかが分かりませんでした。
理由は、JPYC がチェーンの外側に存在しないからです。
JPYC ← 何を持つか(資産の種類)
↑ が乗っている
JPYC コントラクト ← 残高の表を管理しているプログラム
↑ が動いている
チェーン ← そのプログラムを動かす台帳
JPYC はファイルでも持ち物でもなく、あるチェーンの上で動いているプログラムの内部にある、残高の表の1行です。上の層は下の層なしには存在できません。
そして JPYC のコントラクトは、Ethereum・Polygon・Avalanche・Kaia の各本番と、それぞれのテストネットに配置されています。しかもアドレスは全チェーンで同一です。
0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29
これは決定論的デプロイという配置方法によるもので、コントラクトのアドレスが配置元・salt・コードのハッシュから計算されるため、チェーンが違っても入力が同じなら同じアドレスになります。設定でチェーンごとに分岐しなくてよい利点がある一方、「番地が同じなら同じもの」という誤解を招きやすい形でもあります。台帳が違えば、そこは別の帳簿です。
アドレスが2種類あって、性質が逆
もうひとつ混乱したのが「アドレス」という語でした。
| 自分のアドレス | コントラクトのアドレス | |
|---|---|---|
| 例 | 0xA87A…67BA | 0xE7C3…3c29 |
| 正体 | 公開鍵から導出した固有の識別子 | プログラムが置かれている番地 |
| 誰のものか | 自分だけのもの | 利用者全員で共通 |
| チェーンをまたぐと | 同じ | 同じ |
「全チェーンで同じ」だけが共通していて、理由はまったく別です。自分のアドレスが全チェーンで同じなのは、導出の過程にネットワークの情報が1ビットも入らないからです。
秘密鍵(乱数) → 公開鍵 → Keccak-256 の下位20バイト → 0xA87A…67BA
↑
ここまでどこにも「Polygon」が出てこない
発行機関も登録簿も存在しません。手元のマシンが乱数から鍵を作り、機械的に導出しているだけです。アドレスは 160 ビット、およそ 1.46 × 10^48 通りあるので、重複の確認という手続き自体が存在しません。
MetaMask の表示に注意
ネットワークを切り替えるとアドレスが変わるように見えて、上の理解が誤りかと思いました。調べた限り、これはウォレット側の変更によるものです。
2025年10月から MetaMask は Multichain accounts が既定になり、1つの「アカウント」が EVM(Ethereum Virtual Machine、Ethereum と同じ命令セットで動くチェーンの系統)用・Solana 用・Bitcoin 用のアドレスを1本ずつ束ねて持つ形になっています。
| 切り替えたもの | アドレス |
|---|---|
| Ethereum ⇄ Polygon ⇄ Amoy ⇄ Base | 変わらない |
| EVM ⇄ Solana ⇄ Bitcoin | 変わる |
Solana は ed25519、Bitcoin は独自形式と、鍵の種類も表記も違うので同じ 0x… を使い回せません。分かれ目はネットワークごとではなくチェーンの系統ごとです。
ガス代のトークンと、受け取るトークンは別
Faucet を2つ回す必要があるのも、理由が分かりませんでした。
チェーンの上には性質の違う2種類の残高があります。台帳のプロトコル自身が管理するネイティブ残高(Polygon では POL)と、コントラクトの内部にある表です。JPYC は後者です。
先に結論だけ書くと、ガス代は台帳が直接持っている残高(POL)から引かれ、JPYC では払えません。JPYC をいくら持っていても、POL が 0 なら何も実行できません。
手数料をネイティブ残高でしか払えないのは、手数料の徴収がコントラクトの実行より前に起きるからです。手数料を JPYC で払おうとすると、
JPYC の残高を確認するには JPYC コントラクトを実行する必要がある → 実行するには手数料が要る → 手数料を払うには残高を確認する必要がある …
という循環になります。そのため、コントラクトを一切動かさずに読める場所だけが手数料の原資として認められています。
身近な言い方をすると、こうなります。
| 役割 | |
|---|---|
| POL(ネイティブ残高) | 切手代。この台帳で何かを実行する権利を買う |
| JPYC(コントラクトの中の表) | 送る中身。それ自体では何も動かせない |
中身が現金でも、切手は郵便局の通貨で買う必要がある、という関係です。
配布元が別々なのも同じ構造の反映です。POL を配るのはネットワークの側、JPYC を配るのは JPYC 株式会社の側で、本番でも「円を発行する主体」と「ネットワークを運営する主体」は無関係です。
使い捨てのアドレスが弾かれる
テスト用には資産を持たない新規のアドレスを用意しました。開発を進めれば秘密鍵をスクリプトに渡す場面が出てくるので、本番の資産と同じ秘密の言葉から導出した鍵はそこに置けません。
ところが、Faucet の側は自動化された大量取得を防ぐ必要があります。Alchemy の Faucet は Ethereum 本番に 0.001 ETH 以上の残高と一定の取引履歴があることを条件にしていて、新規のアドレスでは次のように返ります。
Insufficient balance! You need at least 0.001 ETH on Ethereum Mainnet.
新品のアドレスには履歴がないので、再試行しても条件は満たせません。テスト用に使い捨てのアドレスを用意するという方針と、配布側の条件が噛み合わない箇所です。条件は配布元ごとに違うので、別の Faucet から 0.1 POL を受け取りました。
条件を満たすために本番の ETH を送る、という選択は採りませんでした。送金元と使い捨てのアドレスがチェーン上で恒久的に結び付き、分けた意味がなくなるためです。
公開 RPC が応答しないことがある
RPC(Remote Procedure Call)は、ここではチェーンの状態を読み書きするための問い合わせ口を指します。ウォレットも自分のスクリプトも、チェーンに直接繋がるわけではなく、誰かが立てたこの窓口を経由します。誰でも使える窓口が「公開 RPC」です。
JPYC の受け取りを実行したところ、次のエラーが返りました。
RPC 0x13882 Custom eth_getBlockByNumber:
RPC endpoint returned too many errors, retrying in 0.5 minutes.
Consider using a different RPC endpoint.
0x13882 は 80002、つまり Amoy なのでネットワークの選択は正しく、失敗したのは最新ブロックの取得という基本的な読み取りでした。公開エンドポイントは共有資源で、レート制限や安定性の変動があり、サービス品質の保証もありません。RPC の接続先を差し替えたところ解消しました。
| RPC URL | 備考 |
|---|---|
https://polygon-amoy-bor-rpc.publicnode.com | 鍵不要 |
https://polygon-amoy.drpc.org | 鍵不要 |
https://polygon-amoy.g.alchemy.com/v2/<APIキー> | 要登録 |
RPC を登録するということは、その提供者にチェーンの状態を正しく報告してもらう前提を置くことでもあります。残高の表示や送信の扱いを左右できる位置なので、素性の確認できる提供元から選んでいます。
受け取った結果
Faucet で 100,000 JPYC を請求しました。1回の上限は 3,000,000 JPYC、再取得できるのは残高 1,000,000 JPYC 以下かつ前回の受け取りから24時間以上あいたときなので、追加請求の余地を残せる値です。
受け取り後の状態です。
① ネイティブ残高 (台帳が直接管理。手数料の原資)
0.095628 POL
② コントラクトの中の残高 (ただのデータ。手数料には使えない)
JPY Coin (JPYC) 0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29
decimals 18
生の値 100000000000000000000000
表示 100,000 JPYC
0.1 POL から減っているのは、この Faucet が利用者側で受け取りのトランザクションを送る作りになっているためです。手数料は受け取る側が負担します。先に POL を用意していなければ、この時点で進めませんでした。
トランザクションを読む
受け取りのトランザクションは、エクスプローラで誰でも開けます。公開テストネットなので、URL を共有すれば手元でも同じものが見られます。
| URL | |
|---|---|
| このトランザクション | https://amoy.polygonscan.com/tx/0x60547e55d13a2d8ca2ac696f9c2420ac6cea6c47852b6d624b80a7b8d7310449 |
| 受け取ったアドレス | https://amoy.polygonscan.com/address/0xA87A6c519560cd33FAC1bfe469a059aE524767BA |
| JPYC のコントラクト | https://amoy.polygonscan.com/token/0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29 |
開くと、次のように表示されます。
| 項目 | 値 |
|---|---|
| Status | Success |
| Block | 45,315,462 |
| From | 0xA87A6c519560cd33FAC1bfe469a059aE524767BA |
| To | 0x192fACC5B2c6fdAe484B1e76f542E983E418f26E |
| Method | sendToken(address _to, uint256 _amount) |
| Value | 0 POL |
| Transaction Fee | 0.004371896709464931 POL |
| Gas Price | 29.100000063 Gwei |
| Gas Limit & Usage | 155,270 / 150,237(96.76%) |
| Nonce | 0 |
読み方で迷いやすい箇所が3つあります。
To が JPYC ではありません。 動いたトークンは JPYC ですが、呼び出した先は Faucet のコントラクトです。To は「最初に呼んだ相手」であって「何が動いたか」ではありません。
Value が 0 なのに 100,000 JPYC が動いています。 Value はネイティブトークンの送付額だけを表します。JPYC はコントラクトの内部で表が書き換わるので、Value には現れません。
Nonce が 0 です。 そのアドレスから送信されたトランザクションの通し番号で、これが最初の1本であることを示します。
手数料は掛け算で確かめられます。
150,237 × 29.100000063 Gwei = 0.004371896709464931 POL
トランザクションとログの関係
このトランザクションには、ログが3件記録されていました。1本のトランザクションに複数のログが付くのは、呼び出しが連鎖して複数のコントラクトを通るためです。
あなた ──sendToken(宛先,数量)──▶ Faucet 0x192f…f26E
│
└─transfer(…)──▶ JPYC 0xE7C3…3c29
└ log #11 Transfer
└ log #12 TokenSent
Polygon の手数料システム 0x…1010 ──── log #13 LogFeeTransfer
| ログ | 発行元 | イベント |
|---|---|---|
| #11 | JPYC | Transfer(address indexed from, address indexed to, uint256 value) |
| #12 | Faucet | TokenSent(address indexed to, uint256 amount) |
| #13 | 0x0000…1010 | LogFeeTransfer(…) |
#11 は ERC-20(Ethereum のトークン規格。残高と送金のインタフェースを定めたもの)が定めているイベントです。 ウォレットやエクスプローラが「100,000 JPYC を受け取った」と表示できるのは、この名前と引数の並びが規格で決まっていて、どのトークンでも同じように読めるからです。
#12 は Faucet 独自のイベントで、規格には含まれません。意味を知っているのはこの Faucet を作った側だけです。
#13 は Polygon の手数料まわりのシステムコントラクトが出しているもので、チェーンに固有です。
番号の順序に意味はあるか
あります。ログは「発行された順」に並びます。 そして呼び出しの入れ子では、内側が先に終わるので、内側のログが先に付きます。
Faucet.sendToken() が始まる
└─ JPYC.transfer() を呼ぶ
└─ JPYC が Transfer を発行 ← #11
JPYC.transfer() が返る
Faucet が TokenSent を発行 ← #12
Faucet.sendToken() が返る
手数料の精算 → LogFeeTransfer を発行 ← #13
最初に呼ばれたのは Faucet ですが、Faucet が自分のイベントを出せるのは、内側の送金が成功して戻ってきた後です。まだ送金が済んでいない時点で「送りました」とは書けません。だから JPYC のほうが小さい番号になります。
呼び出しの順序を知りたいときは、ログの並びではなく内部トランザクション(internal transactions)を見ることになります。ログは「何が起きたかの通知」であって、「どう呼ばれたかの記録」ではありません。
番号が 0 でなく 11 から始まっているのにも理由があります。 この番号はトランザクション単位ではなくブロック単位の通し番号で、同じブロックに含まれる他のトランザクションが、先に11件のログを出していた、ということです。
ログは状態の変化そのものではない
ここが大事なところでした。残高が変わったという事実はコントラクトのストレージに記録されていて、ログはそれとは別に「こういうことが起きました」と外向けに出される通知です。
ログはコントラクトから読み返すことができず、チェーンの外にいる読み手のためだけに存在します。
裏を返すと、Transfer イベントを出さないトークンを作ることも技術的には可能で、その場合ウォレットは残高の変化を検知できません。トークンの残高が画面に出るのは、規格が「状態を変えたら必ずこの形で知らせる」と定めていることに依存しています。
小数点の桁数がログから確認できる
ログ #11 の value は生の値で 100,000,000,000,000,000,000,000 と記録されていました。表示上の 100,000 JPYC との比を取ると 10^18 なので、この JPYC の decimals は 18 だと分かります。
コントラクトの内部は常に最小単位の整数で、小数は存在しません。実装時には decimals() を読んでから換算する必要があり、ここを決め打ちにすると桁が18個ずれます。
テストネットの 100,000 JPYC は 10 万円ではない
念のため確認しておくと、いま受け取った 100,000 JPYC に金銭的な価値はありません。エクスプローラの表示も $0.00 です。
本番で 100,000 JPYC を持っていれば、それは 10 万円に相当します。ただしその 100,000 JPYC は Faucet からは出てきません。発行者に 10 万円を預けて初めて発行されるものです。
テストネットの側は、同じコントラクトのコードが別の台帳に配置されているだけで、償還請求権が存在しません。POL についても同様で、テストネットの POL に市場価格はありません。今回支払った 0.00437 POL の手数料も、実際には何も支払っていないことになります。
次章で、この環境の上に「支払いを根拠に証明書を出す」仕組みを組みます。