本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。

日本円ステーブルコイン JPYC を使った試作を始めるにあたって、まずテストネット用の JPYC を手元に用意しました。手順そのものは公式の Faucet(蛇口。テスト用トークンを無償で配る仕組み)に沿って進めるだけで、既に多くの解説があります。

にもかかわらず、いくつかの箇所で手が止まりました。止まった理由は操作の難しさではなく、「いま自分が何を触っているのか」が分かっていなかったことでした。以下は、その分かっていなかった部分の整理と、最終的に受け取った 1 本のトランザクションを読んだ記録です。手順書としてではなく、つまずいた箇所の説明として書いています。

「JPYC を受け取る」だけでは、何も指定できていない

最初に引っかかったのがここでした。Faucet の画面には「ネットワーク」を選ぶ欄があり、なぜ JPYC を受け取るのにネットワークの指定が要るのかが分かりませんでした。

理由は、JPYC がチェーンの外側に存在しないからです。JPYC はファイルでも持ち物でもなく、あるチェーンの上で動いているプログラム(スマートコントラクト)の内部にある、残高の表の 1 行です。

JPYC              ← 何を持つか(資産の種類)
  ↑ が乗っている
JPYC コントラクト   ← 残高の表を管理しているプログラム
  ↑ が動いている
チェーン            ← そのプログラムを動かす台帳

上の層は下の層なしには存在できません。したがって「JPYC を受け取る」は、正確には「どの台帳の上にある JPYC コントラクトの表に、自分のアドレスの行を書き加えるか」という指定を伴わないと成立しません。

そして JPYC のコントラクトは、Ethereum・Polygon・Avalanche C-Chain・Kaia の各本番と、それぞれのテストネットに配置されています。しかもコントラクトのアドレスは全チェーンで同一で、0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29 です。

4 つのチェーンで同じアドレス・同じコントラクト番地でも残高は別であることを示す図。上部に「あなたのアドレス(1つだけ)0xA87A…67BA」の枠があり、そこから 4 つの枠へ矢印が伸びる。左から「Ethereum 本番 chainId 1」「Polygon 本番 chainId 137」「Polygon Amoy chainId 80002(テストネット)」「Avalanche 本番 chainId 43114」。各枠の中には「① 台帳が直接持つ残高」と「② JPYC コントラクト 0xE7C3…3c29 の balances[あなた]」が並ぶ。Polygon Amoy だけが強調され、台帳の残高 0.0956 POL、balances[あなた] 100,000 と表示されている。他の 3 つはいずれも 0。図の下部に「契約の番地は 4 つとも同一。それでも帳簿が別なので、残高は互いに影響しない」と書かれている。

番地が同じなのは、決定論的デプロイと呼ばれる配置方法によるものです。コントラクトのアドレスが配置元・salt・コードのハッシュから計算されるため、チェーンが違っても入力が同じなら同じアドレスになります。設定ファイルでチェーンごとにアドレスを分岐しなくてよいという利点がありますが、同時に「番地が同じなら同じもの」という誤解を招きやすい形でもあります。台帳が違えば、そこは別の帳簿です。

アドレスが 2 種類あって、性質が逆

もうひとつ混乱したのが「アドレス」という語でした。上の説明には 2 つのアドレスが出てきますが、性質がほぼ正反対です。

自分のアドレスコントラクトのアドレス
0xA87A…67BA0xE7C3…3c29
正体公開鍵から導出した固有の識別子プログラムが置かれている番地
誰のものか自分だけのもの利用者全員で共通
チェーンをまたぐと同じ同じ

「全チェーンで同じ」という点だけが共通なので混ざりやすいのですが、理由はまったく別です。自分のアドレスが全チェーンで同じなのは、導出の過程にネットワークの情報が一切入らないからです。

秘密鍵(乱数) → 公開鍵 → Keccak-256 の下位 20 バイト → 0xA87A…67BA
                                    ↑
                    ここまでどこにも「Polygon」が出てこない

発行機関も登録簿も存在せず、手元のマシンが乱数から鍵を作って機械的に導出しているだけです。アドレスは 160 ビット、およそ 1.46 × 10^48 通りあるので、重複の確認という手続き自体が存在しません。

MetaMask の表示が変わっている

ネットワークを切り替えるとアドレスが変わるように見えたので、上の理解が誤りかと思いました。調べた限り、これは MetaMask 側の変更によるものです。

2025 年 10 月から Multichain accounts が既定になり、1 つの「アカウント」が EVM(Ethereum Virtual Machine、Ethereum 互換の実行環境)用・Solana 用・Bitcoin 用のアドレスを 1 本ずつ束ねて持つ形になっています。

切り替えたものアドレス
Ethereum ⇄ Polygon ⇄ Amoy ⇄ Base変わらない
EVM ⇄ Solana ⇄ Bitcoin変わる

Solana は ed25519、Bitcoin は独自形式と、鍵の種類も表記も違うので、同じ 0x… を使い回すことができません。1 つの秘密の言葉(シードフレーズ)から、それぞれ別に導出しています。分かれ目はネットワークごとではなくチェーンの系統ごと、と捉えると整理できました。

なお、EVM 系の中でアドレスが変わって見えた場合は、ネットワークではなくアカウント(Account 1 / Account 2)を切り替えている可能性があります。

ガス代のトークンと、受け取るトークンは別

Faucet を 2 つ回す必要があるのも、最初は理由が分かりませんでした。

チェーンの上には、性質の違う 2 種類の残高があります。ひとつは台帳のプロトコル自身が管理するネイティブ残高(Polygon では POL)で、もうひとつはコントラクトの内部にある表です。JPYC は後者です。

手数料をネイティブ残高でしか払えないのは、手数料の徴収がコントラクトの実行より前に起きるからです。手数料を JPYC で払おうとすると、JPYC の残高を確認するために JPYC コントラクトを実行する必要があり、実行するには手数料が要る、という循環になります。そのため、コントラクトを一切動かさずに読める場所だけが手数料の原資として認められています。

配布元が別々なのも、この構造に対応しています。POL を配るのはネットワークの側、JPYC を配るのは JPYC 株式会社の側で、本番でも「円を発行する主体」と「ネットワークを運営する主体」は無関係です。

実際に止まった 3 か所

テストネットは既定で隠れていて、Amoy は組み込まれていない

MetaMask はテストネットを既定で表示しません。設定の「高度な設定」で「テストネットワークを表示」を有効にすると一覧に現れます。

ただ、これを有効にしても Polygon Amoy は出てきませんでした。組み込みのテストネットに含まれていないためで、別途追加が必要です。Faucet の画面にある「Add Chain to Wallet」を押すとウォレット側に追加の確認が出るので、手で入力するより確実でした。手動で入れる場合の値は次の通りです。

項目
ネットワーク名Polygon Amoy Testnet
RPC URLhttps://rpc-amoy.polygon.technology
チェーン ID80002
通貨記号POL
エクスプローラhttps://amoy.polygonscan.com

使い捨てのアドレスは、Faucet の条件を満たさないことがある

テスト用の作業には、資産を持たない新規のアドレスを用意するのが安全です。開発を進めれば秘密鍵をスクリプトに渡す場面が出てくるので、本番の資産と同じ秘密の言葉から導出した鍵はそこに置けません。

ところが、Faucet の側は自動化された大量取得を防ぐ必要があります。Alchemy の Faucet は、Ethereum 本番に 0.001 ETH 以上の残高と一定の取引履歴があることを条件にしていて、新規のアドレスでは次のように返ります。

Insufficient balance! You need at least 0.001 ETH on Ethereum Mainnet.

新品のアドレスには履歴がないので、再試行しても条件は満たせません。ここは、テスト用に使い捨てのアドレスを用意するという方針と、配布側の条件が噛み合わない箇所です。条件の立て方が配布元ごとに違うので、結果的には別の Faucet から 0.1 POL を受け取りました。Polygon の開発者ドキュメントにサードパーティ Faucet の一覧が掲載されています。

なお、条件を満たすために本番の ETH を送るという選択は採りませんでした。送金元と使い捨てのアドレスがチェーン上で恒久的に結び付き、アドレスを分けた意味がなくなるためです。

Polygon 公式の Faucet は本人確認に GitHub もしくは X.COM を使う形になっていますが、GitHub を選ぶと認可画面が 404 を返す状態でした。認可 URL 自体は github.com 宛で、戻り先も faucet-api.polygon.technology、要求している権限も user:email のみだったので、URL の内容としては問題のないものでした。調べた限り、/login/oauth/authorize が 404 を返すのは client_id に対応するアプリが解決できないときの挙動です。

公開 RPC が応答しないことがある

JPYC の受け取りを実行したところ、MetaMask が次のエラーを返しました。

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 URL を差し替えたところ解消しました。

RPC URL備考
https://polygon-amoy-bor-rpc.publicnode.com鍵不要
https://polygon-amoy.drpc.org鍵不要
https://polygon-amoy.g.alchemy.com/v2/<APIキー>要登録。Faucet の条件とは無関係に発行できる

RPC を登録するということは、その提供者にチェーンの状態を正しく報告してもらう前提を置くことでもあります。残高の表示や送信の扱いを左右できる位置なので、素性の確認できる提供元から選んでいます。

受け取ったトランザクションを読む

Faucet の量には 100,000 JPYC を指定しました。1 回の上限は 3,000,000 JPYC、再取得できるのは残高 1,000,000 JPYC 以下のときなので、追加請求の余地を残せる値です。

受け取り後のアドレスは次の状態になりました。

項目
POL 残高0.09562810… POL
JPYC 残高100,000
トランザクション数2

0.1 POL から減っているのは、この Faucet が利用者側で受け取りのトランザクションを送る作りになっているためです。手数料は受け取る側が負担します。先に POL を用意していなければ、この時点で進めませんでした。

該当のトランザクションをエクスプローラで開くと、次のように表示されます。

項目
StatusSuccess
Block45,315,462
From0xA87A6c519560cd33FAC1bfe469a059aE524767BA
To0x192fACC5B2c6fdAe484B1e76f542E983E418f26E
MethodsendToken(address _to, uint256 _amount)
Value0 POL
Transaction Fee0.004371896709464931 POL
Gas Price29.100000063 Gwei
Gas Limit & Usage155,270 / 150,237(96.76%)
Txn Type2(EIP-1559)
Nonce0

読み方で迷いやすい箇所が 3 つありました。

To が JPYC ではありません。 動いたトークンは JPYC ですが、呼び出した先は Faucet のコントラクト 0x192f…f26E です。トランザクションの To は「最初に呼んだ相手」であって、「何が動いたか」ではありません。

Value が 0 なのに 100,000 JPYC が動いています。 Value はネイティブトークン(ここでは POL)の送付額だけを表します。JPYC はコントラクトの内部で表が書き換わるので、Value には現れません。

Nonce が 0 です。 そのアドレスから送信されたトランザクションの通し番号で、0 はこれが最初の 1 本であることを示します。

手数料は掛け算で確かめられます。

150,237 × 29.100000063 Gwei = 0.004371896709464931 POL

内訳は Base 0.000000063 Gwei、Max Priority 29.1 Gwei で、ほぼ全額が優先手数料です。Polygon には優先手数料の下限が設けられているため、この構成になります。

トランザクションとログの関係

このトランザクションには、ログが 3 件記録されていました。1 本のトランザクションに複数のログが付くのは、呼び出しが連鎖して複数のコントラクトを通るためです。

1 本のトランザクションが 3 つのログを残すまでを示した図。左側は上から下への呼び出しの連鎖で、「あなた(外部所有アカウント)0xA87A…67BA、nonce 0 — このアドレス初の送信」から矢印「sendToken(宛先, 数量)」で「Faucet のコントラクト 0x192f…f26E(トランザクションの To はここ。JPYC ではない)」へ、さらに矢印「transfer(宛先, 数量)」で「JPYC のコントラクト 0xE7C3…3c29(表を書き換える。これが状態の変化)」へつながる。右側は「このトランザクションが残したログ=状態そのものではなく、外向けの通知」という枠で、3 件が並ぶ。#11 は JPYC が発行した Transfer(from, to, value) で「規格で決まった名前。ウォレットとエクスプローラはこれを読む」。#12 は Faucet が発行した TokenSent(to, amount) で「このアプリ独自の名前。規格には無い」。#13 は Polygon の手数料系が発行した LogFeeTransfer で「0x0000…1010 が出す。手数料の移動そのもの」。図の下部に「手数料 0.004371896709464931 POL = 使ったガス 150,237 × 単価 29.100000063 Gwei」「送った POL の額(value)は 0。それでも 100,000 JPYC が動いている」と書かれている。

3 件の内訳は次の通りです。

ログ発行元イベント
#11JPYC 0xe7c3…3c29Transfer(address indexed from, address indexed to, uint256 value)
#12Faucet 0x192f…f26eTokenSent(address indexed to, uint256 amount)
#130x0000000000000000000000000000000000001010LogFeeTransfer(…)

#11 は ERC-20(Ethereum Request for Comments 20、Ethereum のトークン規格)が定めているイベントです。ウォレットやエクスプローラが「100,000 JPYC を受け取った」と表示できるのは、この名前と引数の並びが規格で決まっていて、どのトークンでも同じように読めるからです。

#12 は Faucet 自身が出している、このアプリ固有のイベントです。規格には含まれないので、意味を知っているのはこの Faucet を作った側とその利用者だけです。

#13 は Polygon の手数料まわりのシステムコントラクトが出しているもので、手数料の移動を記録しています。ネイティブトークンの扱いがチェーンごとに違うため、この種のログは Polygon に固有です。

ここで大事なのは、ログは状態の変化そのものではないという点でした。残高が変わったという事実はコントラクトのストレージに記録されていて、ログはそれとは別に「こういうことが起きました」と外向けに出される通知です。ログはコントラクトから読み返すことができず、チェーンの外にいる読み手のためだけに存在します。

裏を返すと、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 です。

本番の JPYC は資金移動業の枠組みで発行されていて、1 JPYC = 1 円で償還できる設計になっています。したがって本番で 100,000 JPYC を持っていれば、それは 10 万円に相当します。ただしその 100,000 JPYC は Faucet からは出てきません。発行者に 10 万円を預けて初めて発行されるものです。

テストネットの側は、同じコントラクトのコードが別の台帳に配置されているだけで、償還請求権が存在しません。数字と単位が同じでも、裏付けが丸ごと無い、という状態です。POL についても同様で、テストネットの POL に市場価格はありません。今回支払った 0.00437 POL の手数料も、実際には何も支払っていないことになります。

この「同じ形をしていて、裏付けだけが無い」という性質があるからこそ、いくら失敗しても損失が出ない環境として使えます。

ここまでで用意できたもの

ネットワークPolygon Amoy(chainId 80002)
アドレス0xA87A6c519560cd33FAC1bfe469a059aE524767BA
POL0.0956(実測で 1 トランザクションあたり 0.0044 前後)
JPYC100,000(decimals 18)
JPYC コントラクト0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29

手数料の実測から逆算すると、残りの POL で打てるトランザクションは 20〜60 本程度です。足りなくなれば Faucet を再訪すれば済みますが、反復の多い作業では公開テストネットではなくローカルにチェーンをフォークするほうが向いています。RPC の不調もレート制限も起きず、残高も任意に設定できるためです。

次は、この環境の上で JPYC の決済を検知し、その結果として利用許諾の証明書を発行する仕組みを組みます。