JPYC決済関数の完全ガイド 〜ステーブルコイン決済の機能とユースケース〜
はじめに
JPYCは単なるERC20ではなく、USDCと同様に決済のしやすさやセキュリティなどを考慮し、ERC20規格を拡張して作られています。この記事は、JPYC決済の導入をこれから検討している方たちに向けて、JPYCが準拠するトークン規格の基本的な関数から、ユーザーのガス代負担をなくす機能まで、その仕組みをコードベースで解説して、具体的にどのようなユースケースが考えられるかなど私の意見を交えてお伝えできたらと思います。
「技術的な話はいらない!」という方は3: JPYC決済ユースケース別実装パターンに飛んでください。
基本的な送信から、ECサイト決済、サブスクリプションまで、どのような場面でどの関数を使えば最適なユーザー体験を実現できるのか、皆さんに具体的な実装イメージを掴んでいただければ幸いです。
JPYC 決済機能の全体像
JPYCはUSDCと同じ規格で開発されており、決済用の関数として代表出来なものは以下の図のようになっております。
通常のERC20
のトークン規格では不便だったことをEIP-2612
やEIP-3009
などの規格を取り入れることで機能を拡張し、より決済として使いやすいものになりました。どのような背景でこれらの拡張機能が追加されたのか、そしてそれぞれの関数は具体的にどのようなユースケースで使うべきなのかを以降に詳しく書いていきます!
1: ERC20の基本と決済における課題
まずは、決済の基本となるERC20の主要な関数とその挙動、そして決済システムを構築する上での課題を見ていきます。
transfer
: シンプルなP2P送信
1-1.
transfer
は、最も基本的な送信機能です。トークンの所有者が指定したアドレスに、指定した数量のトークンを直接送ります。
transfer
コードで見る
-
transfer(address to, uint256 value)
: 呼び出し元(_msgSender()
)からto
アドレスへvalue
分のトークンを送信します。 -
emit Transfer(from, to, value)
: 送信イベントをブロックチェーン上に記録します。
決済における課題
transfer
はP2Pの送信などにはとても便利ですが、ECサイトのような事業者決済で使うには課題があります。それは、「どの注文に対する支払いなのか」をオンチェーンデータだけで紐付けるのが難しい点です。
ユーザーが事業者のアドレスに直接transfer
しても、そのトランザクションには送信元・送信先・金額以外の情報(例:注文ID)を含めることができません。事業者はオフチェーンのデータベースと照合する必要があり、処理が煩雑になります。例えば、注文IDを紐づけるために、transfer関数を下記のように他の関数で使用することはできません。
// PaymentGatewayコントラクトの例
contract PaymentGateway {
IERC20 public jpyc;
address payable public merchant; // 事業者のアドレス
event PaymentSuccessful(uint256 orderId, address customer, uint256 amount);
constructor(address _jpycAddress, address payable _merchant) {
jpyc = IERC20(_jpycAddress);
merchant = _merchant;
}
function executePayment(uint256 orderId, uint256 amount) public {
// このようにtransferは実装できない
jpyc.transfer(merchant, amount);
// 決済成功イベントに注文IDを含める
emit PaymentSuccessful(orderId, msg.sender, amount);
}
}
もしこのような決済コントラクト(以降、PaymentGateway)を実装した場合、pay関数を実行すれば emit PaymentSuccessful
によって 注文ID orderId
と紐づいた実装ができそうですが、このtransferの送信者は、pay関数の実行者ではなく、msg.sender
であるPaymentGatewayコントラクトになってしまい、送信者(from)が実行するというtransferの意図した挙動になりません。
approve
+ transferFrom
: 決済の拡張性を実現する仕組み
1-2. この課題を解決するのが、approve
とtransferFrom
の組み合わせです。
これは「ユーザーが直接送信する」のではなく、「ユーザーが第三者(決済用のスマートコントラクト)に、自分の代わりにトークンを送信する権限を与える」という二段階のプロセスです。
-
approve
: ユーザーが、PaymentGatewayコントラクト(Spender)に対して、自分のウォレット(以降、アドレス)から最大value
分のJPYCを引き出すことを許可(Approve)します。 -
transferFrom
: PaymentGatewayコントラクトが、その許可(Allowance)の範囲内で、ユーザー(Sender)のアドレスから事業者のアドレスへJPYCを送信します。
approve
コードで見る
-
approve(address spender, uint256 value)
: 呼び出し元(owner
)がspender
に対しvalue
分の送信権限を与えます。
allowance
コードで見る
-
allowance(address owner, address spender)
:owner
がspender
に与えている許可額を確認します。
transferFrom
コードで見る
-
transferFrom(address from, address to, uint256 value)
: 呼び出し元(spender
)が、from
のアドレスからto
のアドレスへvalue
分のトークンを送信します。この時、spender
はfrom
から事前にapprove
されている必要があります。
決済への応用:注文IDとの紐付け
この仕組みを使えば、決済専用のスマートコントラクトを作成できます。
// PaymentGatewayコントラクトの例
contract PaymentGateway {
IERC20 public jpyc;
address payable public merchant; // 事業者のアドレス
event PaymentSuccessful(uint256 orderId, address customer, uint256 amount);
constructor(address _jpycAddress, address payable _merchant) {
jpyc = IERC20(_jpycAddress);
merchant = _merchant;
}
function executePayment(uint256 orderId, uint256 amount) public {
// ユーザーからこのコントラクトへ、事前にamount分のJPYCがapproveされている必要がある
jpyc.transferFrom(msg.sender, merchant, amount);
// 決済成功イベントに注文IDを含める
emit PaymentSuccessful(orderId, msg.sender, amount);
}
}
このPaymentGatewayコントラクトを介することで、transferFrom
をラップした関数を作り、そこの引数に注文ID(orderId
)を渡します。そうすることでPaymentSuccessful`イベントに注文IDを記録でき、オンチェーンデータだけで決済情報を確実に追跡できるようになります。
2: ガスレス - UXを向上させる拡張規格
approve
+ transferFrom
で機能的な決済は実現できましたが、ユーザーには2つの大きな壁が残っています。
-
ガス代: ユーザーは
approve
と決済実行のトランザクションで、2回分のガス代(ETHなど)を支払う必要がある - UXの複雑さ: 2回のトランザクション署名が必要で、ハードルが高い
ブロックチェーンを触る際の大きな障壁の一つがガス代にあると思っています。JPYCや他のステーブルコインで決済ができると言ってもまずETHなどネイティブトークンを保有しなくてはならないというのはハードルが高いです。
この問題を解決するのが、メタトランザクションという技術です。JPYCは、このメタトランザクションを実現するための規格EIP-2612とEIP-3009に対応しています。
2-1. EIP-712: オフチェーン署名の標準規格
メタトランザクションを実現する大きな要素が「オフチェーン署名」です。
ユーザーはトランザクションの内容に秘密鍵で署名だけ行い、その署名データを第三者(リレイヤー)に渡します。リレイヤーがユーザーの代わりにガス代を支払い、トランザクションを実行します。
ここで重要になるのがEIP-712です。これは、単なるハッシュ値ではなく、人間が読める「構造化データ」に対して署名を行うための標準規格です。
{
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Permit": [
{"name": "owner", "type": "address"},
{"name": "spender", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"}
]
},
"primaryType": "Permit",
"domain": {
"name": "JPYC",
"version": "...",
"chainId": 1,
"verifyingContract": "0x..."
},
"message": {
"owner": "0x...",
"spender": "0x...",
"value": "1000000000000000000",
"nonce": 0,
"deadline": 1729868400
}
}
メタマスクではこのようにみることができます!
EIP-712の重要な点は domain
セパレータです。これにより、署名がどのチェーンの、どのコントラクトで、どのバージョンに対して行われたものかが明確になります。これにより、あるDAppでの署名が別のDAppで悪用される「リプレイ攻撃」を防ぐことができ、安全なオフチェーン署名が実現します。
permit
: approve
のガスレス化
2-2. EIP-2612 EIP-2612は、approve
操作をガスレスで行うための規格です。
ユーザーはapprove
の内容(誰に、いくら許可するか)にオフチェーンで署名し、その署名データをリレイヤーに渡します。
permit
コードで見る
-
permit(...)
: リレイヤーがこの関数を呼び出します。引数として渡された署名(v, r, s
)がowner
のものであり、内容が正しければ(上記91行目)、コントラクトは_approve(owner, spender, value)
を内部的に実行します。 -
deadline
: 署名の有効期限。期限を過ぎた署名は無効になります。
-
nonces
: リプレイ攻撃を防ぐためのカウンター。署名ごとにインクリメントされるため、同じ署名を二度使うことはできません。
このpermit
を使えば、ユーザーは署名するだけでapprove
が完了し、ガス代はリレイヤー(事業者)が負担します。
transferWithAuthorization
: transfer
のガスレス化
2-3. EIP-3009 EIP-2612はapprove
をガスレス化しましたが、決済実行(transferFrom
)には依然としてガス代が必要でした。EIP-3009はさらに一歩進め、transfer
操作そのものをガスレス化します。
ユーザーはtransfer
の内容(誰に、いくら送るか)にオフチェーンで署名し、リレイヤーがその署名を使って送信を実行します。
transferWithAuthorization
コードで見る
-
validAfter
,validBefore
: 署名が有効になる期間を指定できます。これにより「2025年10月20日から10月31日まで有効」といったように、柔軟に期間の制御ができます。 -
nonce
: EIP-2612と異なり、32バイトのランダムな値を使用します。これにより、複数のメタトランザクションを並行して作成でき、順番を気にする必要がありません。
receiveWithAuthorization
とcancelAuthorization
:その他のEIP-3009ガスレス関数
2-4.
receiveWithAuthorization
コードで見る
-
receiveWithAuthorization
:transferWithAuthorization
とほぼ同じですが、大きいな違いがあります。それは、トランザクションの実行者(msg.sender
)は、受取人(to
)のみ可能ということです。
これにより、信頼できる特定のスマートコントラクト(or EOA)のみがこのメタトランザクションを実行できるように制限でき、セキュリティが向上します。さらにそのことによって、受取人が自分の都合で受け取ることができるなどユースケースも広がると思っています。
cancelAuthorization
コードで見る
-
cancelAuthorization
:transferWithAuthorization
やreceiveWithAuthorization
で使ったnonce
を指定して、署名を無効化する機能です。ユーザーが誤って署名してしまった場合や、リレイヤーに署名を渡した後に取引をキャンセルしたい場合に、トランザクションが実行される前にその署名を無効にできます。
EIP-3009のnonce管理
-
_authorizationStates[authorizer][nonce]
:EIP-3009のnonceを管理する状態です。これが0
であれば「未使用(発行済みだが未実行・未取消)」、1
であれば「使用済み/取消済み(以後は二度と使えない)」となります。
未使用(0)
├─ transferWithAuthorization 実行 → _authorizationStates[authorizer][nonce] = 1
├─ receiveWithAuthorization 実行 → _authorizationStates[authorizer][nonce] = 1
└─ cancelAuthorization 実行 → _authorizationStates[authorizer][nonce] = 1
3: JPYC決済ユースケース別実装パターン
これまで見てきた関数を、具体的な決済シーンでどのように使い分けるかを見ていきましょう。
3-1. ケース1: P2P送信・投げ銭(シンプル決済)
-
使用関数:
transfer
-
フロー:
- 送信者Aが、自身のアドレスから受取人Bのアドレスと金額を指定。
-
transfer
トランザクションに署名し、実行。ガス代はAが支払う。
- 解説: 最もシンプルで直接的な方法です。ユーザー間の送信や、クリエイターへの投げ銭など、オンチェーンでの注文管理が不要なケースに適していると言えます。
3-2. ケース2: ECサイトでのオンライン決済
-
使用関数:
approve
+transferFrom
(PaymentGatewayコントラクト経由) -
フロー:
- ユーザーがECサイトで決済に進む。
-
(トランザクション1): ユーザーは、ECサイトのPaymentGatewayコントラクトアドレスに対し、商品代金分のJPYCを
approve
する。ガス代を支払う。 - ECサイトのバックエンドは、ユーザーが
approve
したことを検知。 -
(トランザクション2): バックエンド(またはユーザー自身)が、PaymentGatewayコントラクトの
executePayment(orderId, ...)
関数を実行する。この中でtransferFrom
が呼ばれ、JPYCがユーザーから事業者へ移動する。ガス代が発生。
-
解説: 注文IDと決済を、オンチェーンのイベントを使用することで紐付けられる方法です。しかし、ユーザーは2回のトランザクションと2回分のガス代(ユーザーが
executePayment()
を実行する場合)が必要となり、UXは良くありません。ただ、信頼できるスマートコントラクトであればまとまったJPYCをApproveしておくことで、毎月定額を引き出すようなサブスクリプション決済も実装可能です。
3-3. ケース3: ガスレスECサイト決済(UX向上版)
-
使用関数:
permit
+transferFrom
(PaymentGatewayコントラクト経由) -
フロー:
- ユーザーがECサイトで決済に進む。
-
(オフチェーン署名): ユーザーはアドレスで
permit
のデータ(PaymentGatewayコントラクトに代金分を許可する内容)に署名する。ガス代は不要。 - ECサイトのバックエンド(リレイヤー)が、ユーザーの署名を受け取る。
-
(トランザクション1回のみ): リレイヤーは、
executePayment
(内部でpermit
とtransferFrom
の両方を呼ぶ)を実行する。ガス代はリレイヤー(事業者)が負担する。
- 解説: ユーザーはガス代不要で、署名一回だけで決済が完了します。事業者はガス代を負担しますが、ユーザーの離脱率を低下させることができるのではないかと思います。
コードイメージ
PaymentGatewayコントラクト (Solidity)
contract PaymentGatewayWithPermit {
IERC2612 public jpyc;
address payable public merchant;
function executePaymentWithPermit(
uint256 orderId,
address owner,
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public {
// 1. PermitでApproveをガスレスで実行
jpyc.permit(owner, address(this), amount, deadline, v, r, s);
// 2. transferFromで決済実行
jpyc.transferFrom(msg.sender, merchant, amount);
// ... イベント発行
}
}
3-4. ケース4: 店頭でのQRコード決済
-
使用関数:
transferWithAuthorization
-
フロー:
-
店舗側: POSレジで会計金額を入力し、支払情報(店舗アドレス、金額、
nonce
等)を含むQRコードを生成・表示します。 - ユーザー側: スマートフォンのウォレットアプリでQRコードをスキャンします。
- (オフチェーン署名): ウォレットアプリに表示された支払内容(支払先、金額)を確認し、署名します(生体認証などで承認)。ユーザーのガス代は不要です。
- ユーザーのアプリは、生成した署名データを店舗のPOSシステムへ送信します(NFC, Bluetoothなど)。
-
(トランザクション1回のみ): 店舗のPOSシステム(リレイヤー)が署名データを受け取り、
transferWithAuthorization
トランザクションを実行します。ガス代は店舗側(事業者)が負担します。
-
店舗側: POSレジで会計金額を入力し、支払情報(店舗アドレス、金額、
-
解説: ユーザーはガス代を持っていなくても、JPYCだけでスピーディーな支払いが可能になります。ユーザー体験は既存のQRコード決済とほぼ同じですね。前述した
validAfter
とvalidBefore
で現在から5分後までとしておくことでコンビニ決済などのその場での決済に向いていると思います。
3-5. ケース5: B2Bでの請求書払い
-
使用関数:
receiveWithAuthorization
-
フロー:
-
請求側(B社): 取引先(A社)に請求書を送付します。この際、支払いに必要な情報(請求額、請求番号に対応する
nonce
など)を伝えます。 -
支払側(A社): 経理担当者が請求内容を確認し、
receiveWithAuthorization
のためのデータ(「B社がA社のアドレスから請求額を引き出すことを許可する」内容)に署名します。 - (オフチェーン署名): A社は生成された署名データをB社に送付します(メールや専用システム経由)。A社はガス代を支払う必要がありません。
-
(トランザクション1回のみ): 署名を受け取ったB社は、自社のウォレットから
receiveWithAuthorization
トランザクションを実行します。ガス代はB社が負担します。
-
請求側(B社): 取引先(A社)に請求書を送付します。この際、支払いに必要な情報(請求額、請求番号に対応する
-
解説:
receiveWithAuthorization
は、トランザクションの実行者(msg.sender
)と送金先(to
)が一致する必要があるため、「資金の受け取り側が、支払いを能動的に回収する」というユースケースに最適です。支払側は署名を渡すだけで支払業務が完了し、請求側は資金回収のタイミングをコントロールできる(前述したvalidAfter
とvalidBefore
)という、B2B決済における課題を解決できるのではと考えています。
まとめ
今回、JPYCの決済で使える関数群についてまとめてみました。今回はユースケース等については私なりに考えだしたものであり、実際に決済システムを導入する際には、提供したいサービスとユーザー体験に応じて、これらの関数を戦略的に使い分けることが必要になってくると思います。
シンプルなシステムならtransfer
やapprove
/transferFrom
から始め、より良いUXを目指すならpermit
やtransferWithAuthorization
を活用したガスレス決済の導入を考えられてみてください。
またJPYCでSDKを公開しているようです。私も実際に使ってみましたが、とても使いやすく実装がとても簡単でした!まだ資金移動業版はまだのようですが、Prepaid版(JPYCの機能はほぼ同じ)で今のうちに触ってみられることをお勧めします!
node
だけでなく、React
やPython
版のSDKもおいおい公開されるという噂も...。
Discussion