はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、DIFFと呼ばれるtransfer時に送付元アドレスが保有するトークン量を減らさずに、新たにFTをMintするERC20のようなファンジブルトークンの仕組みを提案しているERC7837についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

ERC7837では、新しいタイプのFTであるDiffusive Tokens(DIFF)を提案しています。 従来のERC20トークンと異なり、DIFFトークンを送信しても送付元アドレスの残高は減りません。 その代わりに、送付先アドレスに新しいトークンが発行されて総供給量が増加します。

ただし、無制限に増え続けるわけではないです。 トークンの発行者が設定する「最大供給量(Maximum Supply)」を超えることはなく、その範囲内でトークンの供給が増えていく仕組みです。 また、トークンの送付時には、送付元アドレスが一定のネイティブトークン(ETH)を手数料として支払う必要があります。 この手数料はコントラクトの所有者(発行者)に支払われるため、トークンの発行者はこの仕組みを活用して収益を得ることができます。 さらに、トークン保有者は任意で自分のトークンを「burn」することができ、これにより総供給量を減少させることが可能です。

このように、DIFFトークンは単なる送受信の仕組みではなく、トークンの供給が動的に変化して経済的なインセンティブが組み込まれた新しいタイプのトークンモデルとなっています。

動機

従来のERC20トークンは、総供給量が固定かコントラクト内で管理されたており、トークンの送受信によって所有権が移ります。 DIFFトークンは、トークンの送信によって供給量が増えることで、「価値の拡散(diffusion)」をシミュレーションするような仕組みを提供します。

例えば、製品の流通やデジタルアイテムの配布をトークン化する時、トークンの供給を動的に調整することでより柔軟な流通モデルを実現できます。 企業がある製品のトークンを発行し、それをユーザー間で取引できるようにするとします。 このとき、DIFFトークンの仕組みを使えば、トークンを送るたびに新たに発行されて流通が広がっていく仕組みを作ることができます。

また、トークンの送付ごとに一定の手数料を設定することで、無制限に送付を繰り返して供給を増やすことを防ぐこともできます。 さらに、最大供給量の上限を設定することでインフレを防ぎます。

具体的なユースケース

DIFFトークンの仕組みが活用できる具体的なケースとして、以下のようなものがあります。

まず、リアルワールドアセットのトークン化に活用できます。 例えば、スマートフォンや限定版グッズのような「上限数のある商品」をトークンで管理するケースを考えます。 メーカーが「iPhone 15の在庫1,000台」をDIFFトークンで発行し、購入希望者がそのトークンを受け取ることで、実際の商品を予約・所有している状態を作れます。 その後、トークンをBurnすることで実際の商品と交換するといった使い方が可能になります。

また、手数料を活用したインセンティブ設計もできます。 トークンを送るたびに一定の手数料を発生させることで、不必要な送付を抑えて必要なトランザクションのみが実行されるようになります。 さらに、この手数料はトークンの発行者に収益として還元されるため、継続的な運営資金として活用することもできます。

このように、DIFFトークンの仕組みは、トークンの供給と価値を動的に管理しつつ、経済的なインセンティブを組み合わせることで新しい形のトークンエコノミーを生み出す可能性を持っています。

仕様

用語の説明

DIFFトークン

通常のトークンと異なり、送付時に新たに発行される特殊なFTです。 通常のERC20トークンでは、送付元アドレスの保有トークン量が減少して、送付先アドレスの残高が増加するしますが、DIFFトークンは送付元アドレスの残高が変わらず新しく発行される形で受信者に付与されます。

最大供給量(Max Supply)

このトークンの発行上限値です。 この値はトークンの発行者が設定し、発行されるトークンの総数がこの値を超えることはなく無限に増加しないようになっています。

Transfer Fee

トークンを送るたびに発生する手数料です。 この手数料はネイティブトークン(例えばETH)で支払われ、送付元アドレスが負担する仕組みになっています。 手数料の総額は「transferFee × 送信するトークンの量」で計算され、送信時に十分なETHが支払われていない場合トランザクションは失敗します。

Burn

トークンを完全に削除する処理です。 トークンの保有者は、任意のトークン量をBurnすることで、保有しているトークン量を減らすと同時にトークンの総供給量を減らすことができます。 これにより、供給量を調整したり、トークンの裏付けとなる商品やサービスの引き換え(償還)ができます。

データ構造

DIFFトークンには、総供給量(totalSupply)と最大供給量(maxSupply)の変数があります。

  • totalSupply
    • 現在流通しているトークンの総数。
    • トークンの送付時に増加してBurn時に減少します。
  • maxSupply
    • トークンの最大供給量を示し、この値を超える新規発行は行われません。
      また、Transfer Feeを管理している transferFee は、トークンを送付するごとに発生する手数料の金額を記録しており、ネイティブ通貨の最小単位(例えばETHならwei単位)で表現されます この手数料の設定・変更はコントラクトの所有者(owner)が管理します。

トークンの振る舞い

DIFFトークンの最も特徴的な動作は「送付時に発行される」という点です。 通常のERC20トークンは、送付元アドレスの残高が減り送付先アドレスの残高が増えますがDIFFトークンは異なります。

AがBにトークンを送付すると、Aの残高は変わらずBのアドレスに新しいトークンが発行されます。 この時、totalSupply も送信量と同じだけ増加します。 ただし、この処理によって totalSupply が maxSupply を超える場合、取引は失敗されます。

また、トークン送付時には「送付するトークンの量 × transferFee」のネイティブトークンを送付元アドレスが支払う必要があります。 もし msg.value(送付時に支払われたETHの量)が不足している場合、取引は成立せず失敗します。 この仕組みにより、トークンを無制限に送信して供給を増やすことを防ぎます。

一方、トークンのBurnも可能です。 保有者は任意の量のトークンをBurnすることで保有残高と totalSupply の両方を減少させることができます。 これにより、トークンの希少性を高めることができ、特定の商品やサービスとトークンを引き換えたい場合はBurnを利用して交換を実現できます。

インターフェースと管理機能

DIFFトークンの基本的な関数はERC20の balanceOf や transfer を継承しつつ上書きしています。

  • balanceOf(address account)
    • 指定したアドレスのトークン残高を取得します。
  • transfer(address to, uint256 amount)
    • 指定したアドレスに amount だけトークンを送ります。
    • ただし、この関数は通常のERC20とは異なり、送付元アドレスの残高は減少せず、送付先アドレスに新しいトークンが発行されます。
    • また、送信時には msg.value >= transferFee * amount の条件を満たす必要があります。
  • burn(uint256 amount)
    • 指定した量のトークンをBurnします。
    • これにより、保有者の残高と totalSupply が減少します。

また、管理者(トークンの発行者)だけが実行できる管理用の関数も用意されています。

  • setMaxSupply(uint256 newMax)
    • 最大供給量を変更します。
  • setTransferFee(uint256 newFee)
    • 送信時の手数料を更新します。
  • withdrawFees(address payable recipient)
    • これまでに蓄積された手数料を指定したアドレスに送金します。

イベントの設計

DIFFトークンでは、特定のアクションが実行された場合にイベントを発行します。

  • Transfer(address indexed from, address indexed to, uint256 amount)

transfer が実行され、新しいトークンが to に指定されたアドレスに発行されます。 通常のERC20と異なり、 from はゼロアドレスではなく送付元アドレスになります。

  • Burn(address indexed burner, uint256 amount)

トークンがBurnされたときに発行されます。

  • FeeUpdated(uint256 newFee)

transferFee が変更されたときに発行されます。

  • MaxSupplyUpdated(uint256 newMaxSupply)

maxSupply が変更されたときに発行されます。

ERC20との互換性

これまで説明したように、DIFFトークンはトークンの送信アドレスの残高が変わらず、新たにトークンを発行します。

Fungibility

DIFFトークンはERC20と同じでFTです。 また、小数点以下の単位を扱うこともでき、通常のERC20と同じように扱うことができます。

残高と送付

DIFFトークンの balanceOf 関数は、通常のERC20と同じように特定のアドレスがどれだけのトークンを持っているかを取得します。 ただし、transfer や transferFrom の挙動が大きく異なります。

通常のERC20では、transfer を実行すると送付元アドレスの残高が減り送付先アドレスの残高が増加しますが、DIFFトークンでは送付元アドレスの残高はそのままで、送付先アドレスに新しいトークンが発行されます。 例えば、AがBに100 DIFFトークンを送った場合、Aの残高は変わらずBの残高が100増えます。 このとき、totalSupply も100増加します。 ただし、totalSupply が maxSupply を超えそうな場合、取引は失敗します。

Approvals

DIFFトークンは、ERC20の approve と transferFrom もサポートしています。 通常の*ERC20**では、approve は他のアドレスが自身のトークンの使用をを許可し、transferFrom はその許可をもとにトークンを送付する関数です。 しかし、DIFFトークンでは送付元アドレスの残高が減るわけではないため、この仕組みが少し複雑になります。

例えば、通常のERC20で approve(spender, 100) を実行した場合、spender は transferFrom を使って100トークンを第三者に送信できます。 しかし、DIFFトークンでは transferFrom を実行すると、送付元アドレスの残高を減らすのではなく、新しいトークンを to のアドレスに発行します。 そのため、トークンの所有者の許可によって新しいトークンが発行される仕組みになっています。

ERC20との互換性の範囲

インターフェースではERC20と互換性があるため、多くの既存のウォレットやコントラクトと連携できます。 ただし、トークンの送付方法が異なるため、ERC20ベースのアプリケーションでDIFFトークンを扱う時は注意する必要があります。

経済的な違い

表面的にはERC20と同じように見えますが、DIFFトークンの経済的な仕組みはまったく異なります。 ERC20は総供給量が一定(または管理者が増減を制御)ですが、DIFFトークンは送付によって自然に供給量が増加するため、供給管理が別のアプローチになります。 そのため、従来のERC20の流動性提供(Liquidity Providing)やトークン報酬の仕組みと併用する場合、注意が必要です。

また、トークンの送付ごとに発生する手数料が、DIFFトークンの流通に影響を与えます。 通常のERC20ではガス代のみが取引コストですが、DIFFトークンではネイティブ通貨(ETHなど)の追加コストがかかるため、頻繁なトークン送付には経済的な制約が生じます。 この仕組みによって、不要な送付を抑制して発行者に手数料収入をもたらす構造になっています。

補足

DIFFトークンの設計では、トークンの供給が送付時に増えるという特徴を持たせています。 しかし、無制限に発行できるようにするとインフレが制御できなくなってしまうため、最大供給量(maxSupply)を設定することで、供給量の膨張を防ぐ仕組みになっています。

また、トークンのBurn機能を導入することで、必要に応じてトークンを削減することができます。 これにより、供給量を抑えたり、特定の目的でトークンを償還(redeem)する仕組みを作ることが可能になります。 例えば、リアルワールドアセット(RWA)と紐づいたトークンを発行し、トークンをBurnすることで実際の商品やサービスと交換する、といったユースケースに対応できます。

DIFFトークンのもう1つの特徴は、発行者(オーナー)が手数料や最大供給量を調整できる点です。 市場の状況に応じて手数料を変更することで、トークンの流通量や経済的インセンティブを適切にコントロールすることができます。 例えば、流通量が増えすぎた場合に手数料を引き上げることで不要な取引を抑制したり、逆により流通させたい場合は手数料を下げるといった調整が可能です。

参考実装

DiffusiveToken.sol
contract DiffusiveToken {
  // -----------------------------------------
  // State Variables
  // -----------------------------------------

  string public name;
  string public symbol;
  uint8 public decimals;

  uint256 public totalSupply;
  uint256 public maxSupply;
  uint256 public transferFee; // Fee per token transferred in wei

  address public owner;

  // -----------------------------------------
  // Events
  // -----------------------------------------

  event Transfer(address indexed from, address indexed to, uint256 amount);
  event Burn(address indexed burner, uint256 amount);
  event FeeUpdated(uint256 newFee);
  event MaxSupplyUpdated(uint256 newMaxSupply);
  event Approval(address indexed owner, address indexed spender, uint256 value);

  // -----------------------------------------
  // Modifiers
  // -----------------------------------------

  modifier onlyOwner() {
      require(msg.sender == owner, "DiffusiveToken: caller is not the owner");
      _;
  }

  // -----------------------------------------
  // Constructor
  // -----------------------------------------

  /**
   * @dev Constructor sets the initial parameters for the Diffusive Token.
   * @param _name Token name
   * @param _symbol Token symbol
   * @param _decimals Decimal places
   * @param _maxSupply The max supply of tokens that can ever exist
   * @param _transferFee Initial fee per token transferred in wei
   */
  constructor(
      string memory _name,
      string memory _symbol,
      uint8 _decimals,
      uint256 _maxSupply,
      uint256 _transferFee
  ) {
      name = _name;
      symbol = _symbol;
      decimals = _decimals;
      maxSupply = _maxSupply;
      transferFee = _transferFee;
      owner = msg.sender;
      totalSupply = 0; // Initially, no tokens are minted
  }

  // -----------------------------------------
  // External and Public Functions
  // -----------------------------------------

  /**
   * @notice Returns the token balance of the given address.
   * @param account The address to query
   */
  function balanceOf(address account) external view returns (uint256) {
      return balances[account];
  }

  /**
   * @notice Transfers `amount` tokens to address `to`, minting new tokens in the process.
   * @dev Requires payment of native currency: transferFee * amount.
   * @param to Recipient address
   * @param amount Number of tokens to transfer
   * @return True if successful
   */
  function transfer(address to, uint256 amount) external payable returns (bool) {
      require(to != address(0), "DiffusiveToken: transfer to zero address");
      require(amount > 0, "DiffusiveToken: amount must be greater than zero");

      uint256 requiredFee = transferFee * amount;
      require(msg.value >= requiredFee, "DiffusiveToken: insufficient fee");

      // Check max supply limit
      require(totalSupply + amount <= maxSupply, "DiffusiveToken: would exceed max supply");

      // Mint new tokens to `to`
      balances[to] += amount;
      totalSupply += amount;

      emit Transfer(msg.sender, to, amount);
      return true;
  }

  /**
   * @notice Burns `amount` tokens from the caller's balance, decreasing total supply.
   * @param amount The number of tokens to burn
   */
  function burn(uint256 amount) external {
      require(amount > 0, "DiffusiveToken: burn amount must be greater than zero");
      require(balances[msg.sender] >= amount, "DiffusiveToken: insufficient balance");

      balances[msg.sender] -= amount;
      totalSupply -= amount;

      emit Burn(msg.sender, amount);
  }

  /**
   * @notice Approves `spender` to transfer up to `amount` tokens on behalf of `msg.sender`.
   * @param spender The address authorized to spend
   * @param amount The max amount they can spend
   */
  function approve(address spender, uint256 amount) external returns (bool) {
      require(spender != address(0), "DiffusiveToken: approve to zero address");
      allowances[msg.sender][spender] = amount;
      emit Approval(msg.sender, spender, amount);
      return true;
  }

  /**
   * @notice Returns the current allowance of `spender` for `owner`.
   * @param _owner The owner of the tokens
   * @param _spender The address allowed to spend the tokens
   */
  function allowance(address _owner, address _spender) external view returns (uint256) {
      return allowances[_owner][_spender];
  }

  /**
   * @notice Transfers `amount` tokens from `from` to `to` using the allowance mechanism.
   * @dev The `from` account does not lose tokens; this still mints to `to`.
   * @param from The address from which the allowance has been given
   * @param to The recipient address
   * @param amount The number of tokens to transfer (mint)
   */
  function transferFrom(address from, address to, uint256 amount) external payable returns (bool) {
      require(to != address(0), "DiffusiveToken: transfer to zero address");
      require(amount > 0, "DiffusiveToken: amount must be greater than zero");

      uint256 allowed = allowances[from][msg.sender];
      require(allowed >= amount, "DiffusiveToken: allowance exceeded");

      // Deduct from allowance
      allowances[from][msg.sender] = allowed - amount;

      uint256 requiredFee = transferFee * amount;
      require(msg.value >= requiredFee, "DiffusiveToken: insufficient fee");

      // Check max supply
      require(totalSupply + amount <= maxSupply, "DiffusiveToken: would exceed max supply");

      // Mint tokens to `to`
      balances[to] += amount;
      totalSupply += amount;

      emit Transfer(from, to, amount);
      return true;
  }

  // -----------------------------------------
  // Owner Functions
  // -----------------------------------------

  /**
   * @notice Updates the maximum supply of tokens. Must be >= current totalSupply.
   * @param newMaxSupply The new maximum supply
   */
  function setMaxSupply(uint256 newMaxSupply) external onlyOwner {
      require(newMaxSupply >= totalSupply, "DiffusiveToken: new max < current supply");
      maxSupply = newMaxSupply;
      emit MaxSupplyUpdated(newMaxSupply);
  }

  /**
   * @notice Updates the per-token transfer fee.
   * @param newFee The new fee in wei per token transferred
   */
  function setTransferFee(uint256 newFee) external onlyOwner {
      transferFee = newFee;
      emit FeeUpdated(newFee);
  }

  /**
   * @notice Allows the owner to withdraw accumulated native currency fees.
   * @param recipient The address that will receive the withdrawn fees
   */
  function withdrawFees(address payable recipient) external onlyOwner {
      require(recipient != address(0), "DiffusiveToken: withdraw to zero address");
      uint256 balance = address(this).balance;
      (bool success, ) = recipient.call{value: balance}("");
      require(success, "DiffusiveToken: withdrawal failed");
  }

  // -----------------------------------------
  // Fallback and Receive
  // -----------------------------------------

  // Allows the contract to receive Ether.
  receive() external payable {}
}

セキュリティ

Reentrancyへの対策

トークンの送付時には手数料が発生し、その手数料をコントラクトの所有者(オーナー)に送る処理が実行されます。 このような「外部アドレスへ資金を送る処理」は、再入可能性攻撃(Reentrancy Attack)の対象になりやすいため注意が必要です。 再入可能性攻撃とは、外部コントラクトに資金を送る時、その外部コントラクトが予期しない形で再びDIFFトークンの関数を呼び出し、不正に資金を引き出せるような攻撃手法です。

この対策として、Checks-Effects-Interactionsパターンを採用することが重要です。 このパターンでは、処理を以下の順番で行います。

  1. 取引が成立するかどうかのチェック(Checks)
  2. 必要な状態を更新する(Effects)
  3. 外部アドレスへの送金などの処理を実行する(Interactions)

この順番を守ることで、悪意のあるコントラクトが途中で予期しない再入を試みても、不正に資金を引き出すことができないように設計できます。 また、OpenZeppelinのReentrancyGuard を利用することで、特定の関数が同じトランザクション内で複数回呼ばれるのを防ぐこともできます。

オーバーフロー・アンダーフローの防止

整数のオーバーフロー(上限を超える計算)やアンダーフロー(0より小さくなる計算)は、コントラクトの脆弱性につながります。 しかし、Solidity 0.8以降では、デフォルトでオーバーフローやアンダーフローが発生した場合に自動でエラーを投げるようになっています。

計算に関わる部分(例えば maxSupply を超えた場合の処理や、手数料の計算など)では、明示的にエラー処理を記述して意図しない数値の操作が発生しないように注意する必要があります。

コントラクトの資金管理

DIFFトークンでは、送信時に送付アドレスが手数料を支払う仕組みになっています。 そのため、各トランザクションで十分なネイティブトークン(ETHなど)が送付されているかどうかをしっかり確認し、不足している場合はトランザクションをrevertする必要があります。

具体的には、msg.value が transferFee * amount に満たない場合は、トランザクションを拒否する処理を入れることが重要です。 これにより、手数料不足の取引が通ることを防ぎ、コントラクトの想定外の動作を防止できます。 また、手数料を収集した後にコントラクトからオーナーへ送る時も、意図せず資金が失われることがないよう送金処理を適切に管理することが求められます。

アクセス制御の徹底

トークンの経済的な仕組みをコントロールする重要な変数として、transferFeeと maxSupplyがあります。 これらの値は、トークンの流通やインセンティブ設計に大きな影響を与えるため、不正な変更を防ぐために適切なアクセス制御を行うことが必要です。

この変数を更新できるのはトークンの発行者(オーナー)のみとし、onlyOwner修飾子を使って管理者以外のアカウントが変更できないように制限することが推奨されます。 もし適切に管理されていないと、誰でも手数料を0に設定したり最大供給量を無制限に増やしたりすることができてしまいます。

また、オーナーの権限が集中しすぎるとリスクが高まるため、必要に応じてMulti-Sig を活用し、重要な変更は複数の管理者の承認が必要になるようにするのも有効な方法です。

引用

James Savechives (@jamesavechives), "ERC-7837: Diffusive Tokens [DRAFT]," Ethereum Improvement Proposals, no. 7837, December 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7837.

最後に

今回は「DIFFと呼ばれるtransfer時に送付元アドレスが保有するトークン量を減らさずに、新たにFTをMintするERC20のようなファンジブルトークンの仕組みを提案しているERC7837」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!