💸

EVM と比較しながら XRP Ledger を理解する #8

2024/06/23に公開

本記事の目的

Ethereum Virtual Machine(EVM) の開発経験や知識は多少あるが XRP のことは全く知らない人を主な対象とし、EVM と比較しながら XRP Ledger を理解するシリーズです。コンセンサスアルゴリズムなどの理論にはあまり焦点を当てず、アプリ開発の中で必要になりそうな部分を中心に EVM と比較しながらコンパクトにまとめていこうと思います。

小切手とは

Wikipedia によると

小切手とは、銀行等の支払人に対して口座を有する振出人が、所持人(または名宛人)に対し作成者(振出人)の口座から券面に表示された金額の一覧支払いを委託する有価証券

とのことですが、ここではもう少し大雑把に考えて『小切手を持っている者は、その小切手に書かれているアカウントからその小切手で指定された量のトークンを引き出すことができる』ものと考えます。

小切手機能の実装方法

EVM XRP Ledger
ERC-20 準拠のスマコンを実装
+ approve 関数を実行
+ transferFrom 関数を実行
checkCreate トランザクションを発行
+ checkCash トランザクションを発行

EVM の場合は ERC-20 の transferFrom, approve の2つの関数を使うことで許可されたウォレットが許可したウォレットのトークンを送ることができ、これを応用することで小切手のような仕組みを作ることができます。
XRP Ledger の場合は専用のメソッドが用意されているため、それらを利用することで小切手機能を実現することができます。
それでは早速それぞれの実装例と相違点を見ていきましょう。

XRP Ledger での実装例

check の作成

checkCreate メソッドを使用して作成します。 Destination に送金先のアカウント、SendMax に送金量を指定します。

import { Client, Wallet, xrpToDrops } from "xrpl";

const client = new Client("wss://testnet.xrpl-labs.com");

const senderWallet = Wallet.fromSeed("sEd7pHD6SSAmAahdcHUq9SN92q85Zos");
const receiverWallet = Wallet.fromSeed("sEdVXmpogg8XzRcdphu2j4ZPTHpQ7nP");

await client.connect();

const response = await client.submitAndWait(
  {
    TransactionType: "CheckCreate",
    Account: senderWallet.address,
    Destination: receiverWallet.address,
    SendMax: xrpToDrops(7),
  },
  { wallet: senderWallet }
);

console.log(response);

実行すると以下の内容が表示されます。無事作成できました。

{
  id: 10,
  result: {
    Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
    Destination: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
    Fee: '12',
    Flags: 0,
    LastLedgerSequence: 1707872,
    SendMax: '7000000',
    Sequence: 1560656,
    SigningPubKey: 'ED06CF7B5B17F8A764C94DC03EF5463E7080FB1CF15A5DACCE0FD5911190747BE9',
    TransactionType: 'CheckCreate',
    TxnSignature: 'EAF654C5179041543DF0633A7AB53AA44806F5542B83D33D60575711BA0351830CB0B98CA38505350BD4269B0082AF9FFBDED1FC5D71D053938EBB6401A7F906',
    ctid: 'C01A0F4E00000001',
    date: 772332521,
    hash: '0D1C21BA6FC31206BE2CDFEC484A0D129256645ADC966084202F43D516F7A7AB',
    inLedger: 1707854,
    ledger_index: 1707854,
    meta: {
      AffectedNodes: [Array],
      TransactionIndex: 0,
      TransactionResult: 'tesSUCCESS'
    },
    validated: true
  },
  type: 'response'
}

存在する check の確認

小切手(check)は object の一種のため、アカウントが保有する object を表示する account_objects を実行することで確認できます。

account_objects を実行するコード
import { Client, Wallet } from 'xrpl'

const secret = "sEd7pHD6SSAmAahdcHUq9SN92q85Zos"
const wallet = Wallet.fromSeed(secret)

const client = new Client('wss://testnet.xrpl-labs.com')
await client.connect()

// オブジェクトを取得
const response = await client.request({
	command: 'account_objects',
	account: wallet.address
})
console.dir(response.result)

ここで表示される index が小切手の ID になります。小切手を使用する際にパラメータに指定します。

{
  account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
  account_objects: [
    {
      Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
      Destination: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
      DestinationNode: '0',
      Flags: 0,
      LedgerEntryType: 'Check',
      OwnerNode: '0',
      PreviousTxnID: '0D1C21BA6FC31206BE2CDFEC484A0D129256645ADC966084202F43D516F7A7AB',
      PreviousTxnLgrSeq: 1707854,
      SendMax: '7000000',
      Sequence: 1560656,
      index: '5FF6D05E67533D4FD57837F6F59D3F1603410BA8D445F9DC1465A6E02EF62583'
    }
  ],
  ledger_current_index: 1707874,
  validated: false
}

check を換金

CheckCash を実行することで小切手を換金することができます。CheckID に使用したい小切手の ID を設定して実行します。

import { Client, Wallet, xrpToDrops } from "xrpl";

const client = new Client("wss://testnet.xrpl-labs.com");

const receiverWallet = Wallet.fromSeed("sEdVXmpogg8XzRcdphu2j4ZPTHpQ7nP");

await client.connect();

const responseCheck = await client.submitAndWait(
  {
    TransactionType: "CheckCash",
    Account: receiverWallet.address,
    CheckID: "5FF6D05E67533D4FD57837F6F59D3F1603410BA8D445F9DC1465A6E02EF62583",
    Amount: xrpToDrops(7),
  },
  { wallet: receiverWallet }
);

console.log(responseCheck);

無事実行され XRP を手に入れることができました。

{
  id: 12,
  result: {
    Account: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
    Amount: '7000000',
    CheckID: '5FF6D05E67533D4FD57837F6F59D3F1603410BA8D445F9DC1465A6E02EF62583',
    Fee: '12',
    Flags: 0,
    LastLedgerSequence: 1708088,
    Sequence: 1560079,
    SigningPubKey: 'ED0B108D383CBA3E7CB0F82AFCBF83FEB6EA92C7E4DC392AFFE7507BFFF9796B61',
    TransactionType: 'CheckCash',
    TxnSignature: '9BD916F1115B6551F17A7AF084C07A73B9C16CFC8286B713B2EE3834FA8C73AD100E826C2C7C24275B0B8BD6C46D62896F629B5932CE6D985D319895500BD802',
    ctid: 'C01A102600000001',
    date: 772333231,
    hash: 'A9C8AD628A2D336637AD029884917E71A9BA6F0D75C71EE3FBB5E16F2609FDCA',
    inLedger: 1708070,
    ledger_index: 1708070,
    meta: {
      AffectedNodes: [Array],
      TransactionIndex: 0,
      TransactionResult: 'tesSUCCESS',
      delivered_amount: '7000000'
    },
    validated: true
  },
  type: 'response'
}

check の取り消し

小切手はもちろん削除することもできます。小切手を作成したアカウントで checkCancel 実行することで取り消すことができます。

const responseCheck = await client.submitAndWait(
  {
    TransactionType: "CheckCancel",
    Account: senderWallet.address,
    CheckID: "CD09F5CF2DD141E68971DEBB57D8C1BFD086163CEDB4A9D3EB1DEEFD12FE2117",
  },
  { wallet: senderWallet }
);

Solidity での実装例

ERC-20 ではどのような仕組みで動いているかを少し覗いてみます。ソースコードは OpenZeppelin から拝借します。

_allowances 変数

_allowances という変数が定義されており、この変数が小切手の送付元、送付先、数量を保持します。以降の関数ではこの変数の値を書き換えることで、小切手の作成や消化などの操作を表現しています。

_allowances
mapping(address account => mapping(address spender => uint256)) private _allowances;

approve 関数

approve 関数の実行は小切手を作成する操作にあたります。関数の実行者(のウォレット)が、指定したウォレットに指定した量のトークンを使用することを許可します。_allowances[owner][spender] = value; がその処理を行っている部分です。

approve
function approve(address spender, uint256 value) public virtual returns (bool) {
    address owner = _msgSender();
    _approve(owner, spender, value);
    return true;
}

(中略)

function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
    if (owner == address(0)) {
        revert ERC20InvalidApprover(address(0));
    }
    if (spender == address(0)) {
        revert ERC20InvalidSpender(address(0));
    }
    _allowances[owner][spender] = value;
    if (emitEvent) {
        emit Approval(owner, spender, value);
    }
}

transferFrom 関数

transferFrom は関数は小切手を利用する操作に値します。関数の実行者のウォレットが from に指定したウォレットが保有するトークンを送ることを許可されているかを確認し、許可されている場合、to に指定されたウォレットへトークンを送金します。

transferFrom
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
    address spender = _msgSender();
    _spendAllowance(from, spender, value); // from から to へ value 分のトークンを送る
    _transfer(from, to, value);
    return true;
}

(中略)

function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
    uint256 currentAllowance = allowance(owner, spender);
    if (currentAllowance != type(uint256).max) {
        if (currentAllowance < value) {
            revert ERC20InsufficientAllowance(spender, currentAllowance, value);
        }
        unchecked {
            _approve(owner, spender, currentAllowance - value, false);
        }
    }
}

相違点

XRP Ledger の小切手は期限を設定することができる

XRP Ledger では以下のように小切手に有効期間を設定することができます。

const response = await client.submitAndWait(
  {
    TransactionType: "CheckCreate",
    Account: senderWallet.address,
    Destination: receiverWallet.address,
    SendMax: xrpToDrops(10),
    Expiration: isoTimeToRippleTime("2024-07-01T00:00:00Z"),
  },
  { wallet: senderWallet }
);

ERC-20 では同じ支払い元・支払い先の小切手を複数作ることができない

先ほどの _allowances の mapping の構造からわかるように ERC-20 では同じ送付元・送付先の小切手を複数個作ることはできません。さらに小切手を発行したい場合は、すでに存在している小切手のトークンの量を増やすことになります。
XRP Ledger の場合は特に問題なく作ることができます。以下は同じ支払い元・支払い先で10 XRP の小切手と7 XRP の小切手を発行した例です。

{
  account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
  account_objects: [
    {
      Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
      Destination: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
      DestinationNode: '0',
      Flags: 0,
      LedgerEntryType: 'Check',
      OwnerNode: '0',
      PreviousTxnID: '551E9B2C2F1FB47671CC400FC05E340200AE96774C523D2E2635D98CF96BAE5E',
      PreviousTxnLgrSeq: 1742085,
      SendMax: '10000000',
      Sequence: 1560658,
      index: '0BC29EE52D96105882DE08F00D8F450C7D837AFE502B3B9B983AD93197A0403E'
    },
    {
      Account: 'r4rqXdD35fRRxsXQSx1fZ9pwJPR2apC7oz',
      Destination: 'r4sGNi5n9tmV2vULg7dmy8qBCzFH7fUaK2',
      DestinationNode: '0',
      Flags: 0,
      LedgerEntryType: 'Check',
      OwnerNode: '0',
      PreviousTxnID: '9866C2C04F9288DDC73F22A35ECC3ACF4E1F2F50F5E744046E09390795B1BFDF',
      PreviousTxnLgrSeq: 1742080,
      SendMax: '7000000',
      Sequence: 1560657,
      index: 'E033FD03212B6DAE2B4919AF4A904B89BA668B855F4568FF108AA8C646CE8569'
    }
  ],
  ledger_current_index: 1742089,
  validated: false
}

ERC-20 では小切手を利用時に送金先を指定できる

XRP Ledger では小切手を消化した際には必ず destination で指定されているアカウントに送金されますが、ERC-20 では消化する際に to にウォレットアドレスを指定することで第三者へ送金することができます。


小切手機能について EVM と XRP Ledger それぞれでの実装方法を取り上げました。XRP Ledger は相変わらず手軽に実装できるなと感じました。また、XRP Ledger の機能を参考にしながら ERC-20 を独自に拡張することで、より面白い機能を EVM でも実現できるかもしれませんね。

Discussion