👏

JPYC決済関数の完全ガイド 〜ステーブルコイン決済の機能とユースケース〜

に公開

はじめに

JPYCは単なるERC20ではなく、USDCと同様に決済のしやすさやセキュリティなどを考慮し、ERC20規格を拡張して作られています。この記事は、JPYC決済の導入をこれから検討している方たちに向けて、JPYCが準拠するトークン規格の基本的な関数から、ユーザーのガス代負担をなくす機能まで、その仕組みをコードベースで解説して、具体的にどのようなユースケースが考えられるかなど私の意見を交えてお伝えできたらと思います。
「技術的な話はいらない!」という方は3: JPYC決済ユースケース別実装パターンに飛んでください。

基本的な送信から、ECサイト決済、サブスクリプションまで、どのような場面でどの関数を使えば最適なユーザー体験を実現できるのか、皆さんに具体的な実装イメージを掴んでいただければ幸いです。

JPYC 決済機能の全体像

JPYCはUSDCと同じ規格で開発されており、決済用の関数として代表出来なものは以下の図のようになっております。

通常のERC20のトークン規格では不便だったことをEIP-2612EIP-3009などの規格を取り入れることで機能を拡張し、より決済として使いやすいものになりました。どのような背景でこれらの拡張機能が追加されたのか、そしてそれぞれの関数は具体的にどのようなユースケースで使うべきなのかを以降に詳しく書いていきます!

1: ERC20の基本と決済における課題

まずは、決済の基本となるERC20の主要な関数とその挙動、そして決済システムを構築する上での課題を見ていきます。

1-1. transfer: シンプルなP2P送信


transferは、最も基本的な送信機能です。トークンの所有者が指定したアドレスに、指定した数量のトークンを直接送ります。

コードで見るtransfer

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/FiatTokenV1.sol#L309-L349

  • 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の意図した挙動になりません。


1-2. approve + transferFrom: 決済の拡張性を実現する仕組み

この課題を解決するのが、approvetransferFromの組み合わせです。
これは「ユーザーが直接送信する」のではなく、「ユーザーが第三者(決済用のスマートコントラクト)に、自分の代わりにトークンを送信する権限を与える」という二段階のプロセスです。

  1. approve: ユーザーが、PaymentGatewayコントラクト(Spender)に対して、自分のウォレット(以降、アドレス)から最大value分のJPYCを引き出すことを許可(Approve)します。
  2. transferFrom: PaymentGatewayコントラクトが、その許可(Allowance)の範囲内で、ユーザー(Sender)のアドレスから事業者のアドレスへJPYCを送信します。

コードで見るapprove

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/FiatTokenV1.sol#L244-L279

  • approve(address spender, uint256 value): 呼び出し元(owner)がspenderに対しvalue分の送信権限を与えます。

コードで見るallowance

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/FiatTokenV1.sol#L206-L221

  • allowance(address owner, address spender): ownerspenderに与えている許可額を確認します。

コードで見るtransferFrom

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/FiatTokenV1.sol#L280-L308

  • transferFrom(address from, address to, uint256 value): 呼び出し元(spender)が、fromのアドレスからtoのアドレスへvalue分のトークンを送信します。この時、spenderfromから事前に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つの大きな壁が残っています。

  1. ガス代: ユーザーはapproveと決済実行のトランザクションで、2回分のガス代(ETHなど)を支払う必要がある
  2. UXの複雑さ: 2回のトランザクション署名が必要で、ハードルが高い

ブロックチェーンを触る際の大きな障壁の一つがガス代にあると思っています。JPYCや他のステーブルコインで決済ができると言ってもまずETHなどネイティブトークンを保有しなくてはならないというのはハードルが高いです。
この問題を解決するのが、メタトランザクションという技術です。JPYCは、このメタトランザクションを実現するための規格EIP-2612EIP-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で悪用される「リプレイ攻撃」を防ぐことができ、安全なオフチェーン署名が実現します。

2-2. EIP-2612 permit: approveのガスレス化

EIP-2612は、approve操作をガスレスで行うための規格です。

ユーザーはapproveの内容(誰に、いくら許可するか)にオフチェーンで署名し、その署名データをリレイヤーに渡します。

コードで見るpermit

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/EIP2612.sol#L61-L99

  • permit(...): リレイヤーがこの関数を呼び出します。引数として渡された署名(v, r, s)がownerのものであり、内容が正しければ(上記91行目)、コントラクトは_approve(owner, spender, value)を内部的に実行します。
  • deadline: 署名の有効期限。期限を過ぎた署名は無効になります。

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/EIP2612.sol#L50-L59

  • nonces: リプレイ攻撃を防ぐためのカウンター。署名ごとにインクリメントされるため、同じ署名を二度使うことはできません。

このpermitを使えば、ユーザーは署名するだけでapproveが完了し、ガス代はリレイヤー(事業者)が負担します。

2-3. EIP-3009 transferWithAuthorization: transferのガスレス化

EIP-2612はapproveをガスレス化しましたが、決済実行(transferFrom)には依然としてガス代が必要でした。EIP-3009はさらに一歩進め、transfer操作そのものをガスレス化します。

ユーザーはtransferの内容(誰に、いくら送るか)にオフチェーンで署名し、リレイヤーがその署名を使って送信を実行します。

コードで見るtransferWithAuthorization

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/EIP3009.sol#L88-L129

  • validAfter, validBefore: 署名が有効になる期間を指定できます。これにより「2025年10月20日から10月31日まで有効」といったように、柔軟に期間の制御ができます。
  • nonce: EIP-2612と異なり、32バイトのランダムな値を使用します。これにより、複数のメタトランザクションを並行して作成でき、順番を気にする必要がありません。

2-4. receiveWithAuthorizationcancelAuthorization:その他のEIP-3009ガスレス関数

コードで見るreceiveWithAuthorization

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/EIP3009.sol#L131-L175

  • receiveWithAuthorization: transferWithAuthorizationとほぼ同じですが、大きいな違いがあります。それは、トランザクションの実行者(msg.sender)は、受取人(to)のみ可能ということです。

    これにより、信頼できる特定のスマートコントラクト(or EOA)のみがこのメタトランザクションを実行できるように制限でき、セキュリティが向上します。さらにそのことによって、受取人が自分の都合で受け取ることができるなどユースケースも広がると思っています。

コードで見るcancelAuthorization

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/EIP3009.sol#L177-L206

  • cancelAuthorization: transferWithAuthorizationreceiveWithAuthorizationで使ったnonceを指定して、署名を無効化する機能です。ユーザーが誤って署名してしまった場合や、リレイヤーに署名を渡した後に取引をキャンセルしたい場合に、トランザクションが実行される前にその署名を無効にできます。

EIP-3009のnonce管理

https://github.com/jcam1/JPYCv2/blob/main/contracts/v1/EIP3009.sol#L61-L64

  • _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
  • フロー:
    1. 送信者Aが、自身のアドレスから受取人Bのアドレスと金額を指定。
    2. transferトランザクションに署名し、実行。ガス代はAが支払う。
  • 解説: 最もシンプルで直接的な方法です。ユーザー間の送信や、クリエイターへの投げ銭など、オンチェーンでの注文管理が不要なケースに適していると言えます。

3-2. ケース2: ECサイトでのオンライン決済

  • 使用関数: approve + transferFrom(PaymentGatewayコントラクト経由)
  • フロー:
    1. ユーザーがECサイトで決済に進む。
    2. (トランザクション1): ユーザーは、ECサイトのPaymentGatewayコントラクトアドレスに対し、商品代金分のJPYCをapproveする。ガス代を支払う。
    3. ECサイトのバックエンドは、ユーザーがapproveしたことを検知。
    4. (トランザクション2): バックエンド(またはユーザー自身)が、PaymentGatewayコントラクトのexecutePayment(orderId, ...)関数を実行する。この中でtransferFromが呼ばれ、JPYCがユーザーから事業者へ移動する。ガス代が発生。
  • 解説: 注文IDと決済を、オンチェーンのイベントを使用することで紐付けられる方法です。しかし、ユーザーは2回のトランザクションと2回分のガス代(ユーザーがexecutePayment()を実行する場合)が必要となり、UXは良くありません。ただ、信頼できるスマートコントラクトであればまとまったJPYCをApproveしておくことで、毎月定額を引き出すようなサブスクリプション決済も実装可能です。

3-3. ケース3: ガスレスECサイト決済(UX向上版)

  • 使用関数: permit + transferFrom(PaymentGatewayコントラクト経由)
  • フロー:
    1. ユーザーがECサイトで決済に進む。
    2. (オフチェーン署名): ユーザーはアドレスでpermitのデータ(PaymentGatewayコントラクトに代金分を許可する内容)に署名する。ガス代は不要
    3. ECサイトのバックエンド(リレイヤー)が、ユーザーの署名を受け取る。
    4. (トランザクション1回のみ): リレイヤーは、executePayment(内部でpermittransferFromの両方を呼ぶ)を実行する。ガス代はリレイヤー(事業者)が負担する。
  • 解説: ユーザーはガス代不要で、署名一回だけで決済が完了します。事業者はガス代を負担しますが、ユーザーの離脱率を低下させることができるのではないかと思います。

コードイメージ

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
  • フロー:
    1. 店舗側: POSレジで会計金額を入力し、支払情報(店舗アドレス、金額、nonce等)を含むQRコードを生成・表示します。
    2. ユーザー側: スマートフォンのウォレットアプリでQRコードをスキャンします。
    3. (オフチェーン署名): ウォレットアプリに表示された支払内容(支払先、金額)を確認し、署名します(生体認証などで承認)。ユーザーのガス代は不要です。
    4. ユーザーのアプリは、生成した署名データを店舗のPOSシステムへ送信します(NFC, Bluetoothなど)。
    5. (トランザクション1回のみ): 店舗のPOSシステム(リレイヤー)が署名データを受け取り、transferWithAuthorizationトランザクションを実行します。ガス代は店舗側(事業者)が負担します。
  • 解説: ユーザーはガス代を持っていなくても、JPYCだけでスピーディーな支払いが可能になります。ユーザー体験は既存のQRコード決済とほぼ同じですね。前述したvalidAftervalidBeforeで現在から5分後までとしておくことでコンビニ決済などのその場での決済に向いていると思います。

3-5. ケース5: B2Bでの請求書払い

  • 使用関数: receiveWithAuthorization
  • フロー:
    1. 請求側(B社): 取引先(A社)に請求書を送付します。この際、支払いに必要な情報(請求額、請求番号に対応するnonceなど)を伝えます。
    2. 支払側(A社): 経理担当者が請求内容を確認し、receiveWithAuthorizationのためのデータ(「B社がA社のアドレスから請求額を引き出すことを許可する」内容)に署名します。
    3. (オフチェーン署名): A社は生成された署名データをB社に送付します(メールや専用システム経由)。A社はガス代を支払う必要がありません
    4. (トランザクション1回のみ): 署名を受け取ったB社は、自社のウォレットからreceiveWithAuthorizationトランザクションを実行します。ガス代はB社が負担します。
  • 解説: receiveWithAuthorizationは、トランザクションの実行者(msg.sender)と送金先(to)が一致する必要があるため、「資金の受け取り側が、支払いを能動的に回収する」というユースケースに最適です。支払側は署名を渡すだけで支払業務が完了し、請求側は資金回収のタイミングをコントロールできる(前述したvalidAftervalidBefore)という、B2B決済における課題を解決できるのではと考えています。

まとめ

今回、JPYCの決済で使える関数群についてまとめてみました。今回はユースケース等については私なりに考えだしたものであり、実際に決済システムを導入する際には、提供したいサービスとユーザー体験に応じて、これらの関数を戦略的に使い分けることが必要になってくると思います。

シンプルなシステムならtransferapprove/transferFromから始め、より良いUXを目指すならpermittransferWithAuthorizationを活用したガスレス決済の導入を考えられてみてください。

またJPYCでSDKを公開しているようです。私も実際に使ってみましたが、とても使いやすく実装がとても簡単でした!まだ資金移動業版はまだのようですが、Prepaid版(JPYCの機能はほぼ同じ)で今のうちに触ってみられることをお勧めします!
https://docs.jpyc.jp/jpyc-sdk-japanese

nodeだけでなく、ReactPython版のSDKもおいおい公開されるという噂も...。

GitHubで編集を提案

Discussion