はじめに

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

今回は、既存のERC20トークンをそのまま保持したまま別の実装へアップグレードできる仕組みを提案しているERC6864についてまとめていきます!

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

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC20は、Ethereum上で最も広く使われているトークン規格ですが、一度デプロイしたスマートコントラクトは基本的に変更できません。 ERC6864は、この問題に対して「既存のERC20トークンをアップグレード/ダウングレードしつつ、ユーザの残高を保ったまま移行できるインターフェース」を定義します。

ERC20については以下の記事を参考にしてください。

ここでいう「インターフェース」は、スマートコントラクトが外部に公開する関数の仕様(関数名・引数・戻り値など)のセットのことです。ERC6864は、既存のERC-20規格にこのインターフェースを拡張として追加する形になっています。

この拡張のポイントは次以下です。

  • 既存のERC20トークンコントラクトを、新しいトークンコントラクトに「アップグレード」したり、逆に以前のコントラクトに「ダウングレード」したりできます。
  • ユーザの残高はアップグレード/ダウングレードの前後で維持され、ユーザ視点では「トークンが新しいコントラクトに移った」ように扱えます。
  • 他のスマートコントラクトから見ると、「ERC20トークンである」という性質は変わらないため、基本的には参照するコントラクトアドレスさえ差し替えれば、既存のロジックをそのまま使えます。

つまり、ERC6864は「ERC20としての互換性を保ちつつ、アドレスを差し替えるだけで新しい実装に乗り換えられる」ようにするための標準インターフェースです。

動機

スマートコントラクトの世界では、「一度デプロイしたコントラクトは変更できない(イミュータブル)」ことと、「ERC20のようなトークン規格は、機能を最小限に絞る」という考え方が重要視されています。 これは、分散型アプリケーションにおいて、予期しない変更を防ぎ、透明性と信頼性を高めるためです。

一方で、実用的な観点からは、ERC20トークンを後からアップグレードしたくなる場面が少なからずあります。 ERC6864は、そうした現実的なニーズを満たすために提案されています。

アップグレードが必要になる場面

ERC6864が想定している、アップグレードしたくなる典型的な理由は以下です。

  • バグ修正や制約の解消 デプロイ後にバグが見つかったり、設計上の制約で運用が難しくなったりすることがあります。 このような場合、本来であればトークンを新しい実装に載せ替えたいですが、通常のERC20にはその手段がありません。

  • 新しい機能や標準への対応 時間とともに、新しいトークン標準や拡張機能(例: メタトランザクション、ガスレス転送、拡張的な許可管理など)が出てきます。 既存のトークンに対してこれらの機能を後付けしたい場合も、アップグレードができると便利です。

  • 規制変更への対応 各国の規制やコンプライアンス要件は変化します。KYC(本人確認)やトランザクション制限など、法的要件に合わせて仕様を変更したい場面でも、トークンコントラクトをアップグレードできると柔軟に対応できます。

既存の一般的な解決策:プロキシパターン

現在よく使われている一般的な解決策として、「プロキシパターン」があります。 プロキシパターンは、「プロキシ用のスマートコントラクト(窓口)を1つ用意し、その裏側に実装用スマートコントラクトを差し替える」という構成です。 ユーザや他のコントラクトは常にプロキシのアドレスを呼び出し、プロキシが内部で実装コントラクトに処理を委譲します。

このときよく使われるのが delegatecall というEVMの命令です。 delegatecall は「呼び出し先のコードを、呼び出し元のストレージ(状態)を共有したまま実行する」仕組みで、これにより「ストレージを持つのはプロキシ、ロジックは実装コントラクト」という分離が実現できます。

ただし、この方法にはいくつか課題があります。

  • 最初からプロキシ前提で設計・デプロイしておく必要がある プロキシパターンを使うには、「プロキシ用コントラクトと実装用コントラクト」の構成を前提に設計し、最初からその構造でデプロイしておく必要があります。 つまり、すでに単体のERC20としてデプロイ済みのトークンには後から適用できません。

  • アップグレードがサイレントで、ユーザが拒否できない 実装コントラクトを差し替える作業は、一般にコントラクトの管理者が一方的に行えます。 ユーザの残高はそのままですが、「いつ、どんなロジックに変わったか」をユーザが細かく把握するのは難しく、ユーザに明示的な「同意」や「拒否」の選択肢がありません。 結果として、ユーザは知らないうちに新しい仕様のトークンを使わされることになります。

ERC20専用拡張としてのアプローチ

ERC6864は、「何でもアップグレードできる汎用仕組み」ではなく、「ERC20トークンに特化した拡張」として設計されています。 この限定により、以下のような利点が生まれます。

  • 既存のERC20にも適用できる ERC20トークンのインターフェースに、この拡張インターフェースを追加すればよく、事前にプロキシ構成でデプロイしておく必要がありません。 既存のトークンでも、対応した新コントラクトと組み合わせることで、アップグレード/ダウングレードを扱えます。

  • 実装と保守がシンプル ERC20に特化しているため、一般的なプロキシパターンよりも設計が単純で、実装や監査、保守がしやすくなります。

  • アップグレードを「戻す」ことや「ネスト」することが可能 ユーザが新しいトークンにアップグレードした後でも、必要に応じて前のトークンに戻す(ダウングレードする)ことができます。 また、複数回のアップグレードを段階的に重ねる(ネストする)構成もサポートしやすくなります。

  • ユーザごとに明示的な「二重の確認」ができる アップグレードはユーザが自分の意思で実行することを前提としており、すべてのユーザが任意に「参加(opt-in)」できます。 これにより、管理者が一方的に実装を差し替えるのではなく、ユーザごとに明示的な同意を取る流れにできます。

ERC4931との比較

ERC6864と似た課題に取り組んでいる提案として、ERC4931があります。 ERC4931は、「ブリッジコントラクト」と呼ばれる第三のコントラクトを用意し、このブリッジを通じてアップグレード/ダウングレードを行う設計になっています。

ERC4931については以下の記事を参考にしてください。

ブリッジコントラクトとは、元のトークンコントラクトと新しいトークンコントラクトの間に入り、両者とのやり取りを仲介する専用のスマートコントラクトのことです。 この仕組みにより、「トークンのロジック」と「アップグレードのロジック」を分離できるというメリットがあります。

ただし、ERC4931には以下のような制約があります。

  • アップグレード先のコントラクトでトークンを事前に発行しておき、ブリッジコントラクトがそのトークンを保有している必要がある つまり、ユーザがアップグレード操作を行う前の段階で、「アップグレード先のトークンをあらかじめ用意してブリッジに持たせておく」という前提が必要になります。 ERC6864は、アップグレード実行時に残高をその場で反映する形を想定しており、この点で「オンデマンド(just-in-time)」な設計になっています。

  • transfer と同時にアップグレードするような処理や、「見通し(see-through)」の機能をサポートしにくい ERC6864では、「トークンを送るときに同時にアップグレードも行う」といった高度なユースケースや、複数段のアップグレードをまたいで残高や動きを追跡するような機能(see-through機能)を想定しています。 ERC4931のアーキテクチャでは、このような柔軟な振る舞いを実現するのが難しくなる、というのがERC6864側の問題意識です。

まとめると、ERC6864は、プロキシパターンのような汎用的な仕組みではなく、「既存・将来のERC20トークンに対して、シンプルかつユーザが明示的に参加できる形でアップグレード/ダウングレードを標準化する」ことを狙った提案になっています。

仕様

概要

ERC6864は、既存のERC20トークンをアップグレード/ダウングレード可能にするためのインターフェースです。 ユーザが所有する既存トークンを、アップグレード先のトークンへ移行したり、逆に戻したりする操作を標準化します。

さらに ERC6864SeeThrough は、元のERC20トークンとアップグレード後のトークンを「合算して扱うための閲覧用インターフェース」を提供する拡張です。 これは任意実装であり、利用者に統合された残高・総供給量などを見せたい場面で利用できます。

ERC6864

javascript
pragma solidity ^0.8.0;

/**
    @title Upgradable Fungible Token
    @dev See https://eips.ethereum.org/EIPS/eip-6864
 */
interface IERC6864 is IERC20 {
    /**
      @dev MUST be emitted when tokens are upgraded
      @param from Previous owner of base ERC-20 tokens
      @param to New owner of ERC-6864 tokens
      @param amount The amount that is upgraded
    */
    event Upgrade(address indexed from, address indexed to, uint256 amount);

    /**
      @dev MUST be emitted when tokens are downgraded
      @param from Previous owner of ERC-6864 tokens
      @param to New owner of base ERC-20 tokens
      @param amount The amount that is downgraded
    */
    event Downgrade(address indexed from, address indexed to, uint256 amount);

    /**
      @notice Upgrade `amount` of base ERC-20 tokens owned by `msg.sender` to ERC-6864 tokens under `to`
      @dev `msg.sender` must directly own sufficient base ERC-20 tokens
      MUST revert if `to` is the zero address
      MUST revert if `msg.sender` does not directly own `amount` or more of base ERC-20 tokens
      @param to The address to receive ERC-6864 tokens
      @param amount The amount of base ERC-20 tokens to upgrade
    */
    function upgrade(address to, uint256 amount) external;

    /**
      @notice Downgrade `amount` of ERC-6864 tokens owned by `from` to base ERC-20 tokens under `to`
      @dev `msg.sender` must either directly own or be approved to spend sufficient ERC-6864 tokens for `from`
      MUST revert if `to` is the zero address
      MUST revert if `from` does not directly own `amount` or more of ERC-6864 tokens
      MUST revret if `msg.sender` is not `from` and is not approved to spend `amount` or more of ERC-6864 tokens for `from`
      @param from The address to release ERC-6864 tokens
      @param to The address to receive base ERC-20 tokens
      @param amount The amount of ERC-6864 tokens to downgrade
    */
    function downgrade(address from, address to, uint256 amount) external;

    /**
      @notice Get the base ERC-20 smart contract address
      @return The address of the base ERC-20 smart contract
    */
    function baseToken() external view returns (address);
}

イベント

Upgrade
javascript
event Upgrade(address indexed from, address indexed to, uint256 amount);

トークンをアップグレードしたときに発行されるイベント。 元のERC20トークンをアップグレード先のERC6864トークンへ移行した時に通知されます。 from が元のトークン保有者、to がアップグレード後のトークンを受け取るアドレスを示します。 移行した数量は amount に記録されます。

パラメータ

  • from
    • 元のERC20トークンを所有していたアドレス。
  • to
    • アップグレード後のERC6864トークンを受け取るアドレス。
  • amount
    • アップグレードされたトークン数量。
Downgrade
javascript
event Downgrade(address indexed from, address indexed to, uint256 amount);

トークンをダウングレードした時に発行されるイベント。 ERC6864トークンを元のERC20トークンへ戻す操作を行った際に発行されます。 from がERC6864トークンの所有者、to が元のERC20トークンを受け取るアドレスです。 操作された数量が amount として記録されます。

パラメータ

  • from
    • ERC6864トークンを保有していたアドレス。
  • to
    • 元のERC20トークンを受け取るアドレス。
  • amount
    • ダウングレードされたトークン数量。

関数

upgrade
javascript
function upgrade(address to, uint256 amount) external;

元のERC20トークンを、アップグレード先のERC6864トークンへ変換する関数。 コントラクトを呼び出したアドレスの所有する元ERC20トークンを、to アドレス宛にERC6864として発行します。 to がゼロアドレスの場合は処理が中断されます。 呼び出し元が指定数量以上の元ERC20トークンを直接保有していない場合も中断されます。

引数

  • to
    • アップグレード後のERC6864トークンを受け取るアドレス。
  • amount
    • アップグレードする元ERC20トークン数量。
downgrade
javascript
function downgrade(address from, address to, uint256 amount) external;

ERC6864トークンを元のERC20トークンへ戻す関数。

詳細

from が所有するERC6864トークンのうち amount を、to に対して元のERC20トークンに変換して送付します。 to がゼロアドレスの場合は中断されます。 from が十分なERC6864トークンを保有していない場合も中断されます。 また、呼び出し元が from ではない場合は、指定数量を扱うための許可が与えられている必要があります。

引数

  • from
    • ERC6864トークンを差し出すアドレス。
  • to
    • 変換後の元ERC20トークンを受け取るアドレス。
  • amount
    • ダウングレードする数量。
baseToken
javascript
function baseToken() external view returns (address);

元となるERC20トークンコントラクトのアドレスを取得する関数。 このERC6864コントラクトがどのERC20トークンを基礎としているかを確認するために利用します。 アップグレード元・ダウングレード先のトークンを正しく関連付ける際に重要な情報を返します。

戻り値

  • address
    • 元ERC20トークンのコントラクトアドレス。

ERC6864SeeThrough

javascript
pragma solidity ^0.8.0;

interface IERC6864SeeThrough is IERC6864 {
  /**
    @notice Get the combined total token supply between this ERC-6864 and base ERC-20 smart contracts
    @return The combined total token supply
  */
  function combinedTotalSupply() external view returns (uint256);

  /**
    @notice Get the combined token balance of `account` between this ERC-6864 and base ERC-20 smart contracts
    @param account The address that owns the tokens
    @return The combined token balance
  */
  function combinedBalanceOf(address account) external view returns (uint256);

  /**
    @notice Get the combined allowance that `spender` is allowed to spend for `owner` between this ERC-6864 and base ERC-20 smart contracts
    @param owner The address that owns the tokens
    @param spender The address that is approve to spend the tokens
    @return The combined spending allowance
  */
  function combinedAllowance(address owner, address spender) external view returns (uint256);
}

関数

combinedTotalSupply
javascript
function combinedTotalSupply() external view returns (uint256);

元ERC20とERC6864の合算総供給量を取得する関数。 アップグレード前後のトークンを合わせた総供給量を一度に確認できます。 ユーザ側が「実質的に流通しているトークンの合計」を知りたい場面で利用されます。

戻り値

  • uint256
    • 両コントラクトの総供給量の合算値。
combinedBalanceOf
javascript
function combinedBalanceOf(address account) external view returns (uint256);

元ERC20とERC6864を合算したアカウントの残高を取得する関数。 account がどちらのコントラクトに残高を持っていても、その合計値を返します。 アップグレード途中のユーザや、両方で残高を持つケースで役立ちます。

引数

  • account
    • 残高を確認したいアドレス。

戻り値

  • uint256
    • 両コントラクトの残高合計。
combinedAllowance
javascript
function combinedAllowance(address owner, address spender) external view returns (uint256);

元ERC20とERC6864の合算許可量(allowance)を取得する関数。 spender が owner の保有する元ERC20とERC6864の両方について、どれだけ使用を許可されているかを合計して返します。 トークンが分散して存在する場合でも、単一値として扱えるようになります。

引数

  • owner
    • トークンの所有者。
  • spender
    • 使用許可を受けるアドレス。

戻り値

  • uint256
    • 合算された許可数量。

補足

ERC20規格を拡張する理由

ERC6864は、既存のERC20規格を活かしながらアップグレード機能を提供することを目的としています。 ユーザが保持している残高に影響を与えないためには、元のトークンが持つデータ構造や関数をそのまま利用できるほうが合理的です。

ERC20標準が広く普及していることを踏まえると、ERC20の仕組みに沿った拡張として実装するほうが、他のコントラクトやウォレット、ツールとの互換性を最大限保てます。 新しい仕組みを一から作るよりも、既存の機能を利用しつつ拡張するほうが実装コストが低く、既存エコシステムとの連携も容易です。

つまり、ERC6864は「既存ERC20に寄り添った形でアップグレード機能を追加する」という設計思想を採用しています。

ダウングレードをサポートする意義

ERC6864は、アップグレードだけでなくダウングレードの操作もサポートしています。 これにより、同じ元ERC20を基礎とした複数のERC6864実装間を自由に移動できるようになります。 アップグレード先に不具合が見つかった場合や、ユーザが別の実装を選択したい場合にも柔軟に対応できます。

アップグレードという仕組みは便利である一方、一方向の操作しかできないとユーザに不便を強いる可能性があります。 ダウングレードが可能であれば、安全性や透明性が高まり、ユーザ自身の判断でトークンの状態を調整できます。

また、運用者側にとっても、万が一新しい実装に制約や問題が見つかった際に元の状態へ戻す手段を提供できるため、アップグレードに伴うリスクを大幅に低減できます。

オプションのシースルー拡張

ERC6864SeeThroughとして定義される拡張は、元ERC20とアップグレード後のERC6864を合算して扱える補助インターフェースです。 これにより、総供給量や残高、許可量などを「両コントラクトを合わせた値」として簡単に参照できます。

この拡張の機能は便利であるものの、以下の理由で「必須」ではなく「任意(OPTIONAL)」として定められています。

  • 実装自体が簡単で、必要であれば他の公開関数を組み合わせるだけでも同様の結果を計算できるため。
  • 全てのコントラクトが必ずしも合算情報を必要とするわけではないため。

つまり、実装者が特定の用途に応じて採用すればよい補助的な仕様として提供されています。

互換性

ERC6864は、基本的に元のERC20規格と高い互換性を保っています。 ユーザがERC20トークンとして扱っている操作(transfer、balance など)はそのまま利用できます。

ただし、いくつか注意点があります。

一部のスマートコントラクトは、ユーザが持つ「元ERC20+ERC6864の合計残高」を使って transfer を実装する場合があります。 これは、ユーザがアップグレード/ダウングレードの状態を意識せずにトークンを扱えるようにするためです。 つまり、ユーザがどちらのコントラクトにトークンを保持していても不足が起きにくくなり、操作上の摩擦が少なくなります。

このような設計を採用する場合、次の項目を組み合わせて扱うことが推奨されます。

  • totalSupply
  • balanceOf

これらの関数を「元ERC20+ERC6864の合計値」として返すことで、より自然で一貫した振る舞いを提供できます。 特に、ウォレットや他のDeFiコントラクトがトークン情報を取得する際に、統合された残高や供給量を基準に処理できるようになります。

参考実装

js
import {IERC20, ERC20} from "@openzeppelin-contracts/token/ERC20/ERC20.sol";

contract ERC6864 is IERC6864, ERC20 {
  IERC20 private immutable s_baseToken;

    constructor(string memory name, string memory symbol, address baseToken_) ERC20(name, symbol) {
        s_baseToken = IERC20(baseToken_);
    }

    function baseToken() public view virtual override returns (address) {
        return address(s_baseToken);
    }

    function upgrade(address to, uint256 amount) public virtual override {
        address from = _msgSender();

        s_baseToken.transferFrom(from, address(this), amount);
        _mint(to, amount);

        emit Upgrade(from, to, amount);
    }

    function downgrade(address from, address to, uint256 amount) public virtual override {
        address spender = _msgSender();

        if (from != spender) {
            _spendAllowance(from, spender, amount);
        }
        _burn(from, amount);
        s_baseToken.transfer(to, amount);

        emit Downgrade(from, to, amount);
    }

    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address from = _msgSender();
        uint256 balance = balanceOf(from);

        if (balance < amount) {
            upgrade(from, amount - balance);
        }

        _transfer(from, to, amount);
        return true;
    }

    function totalSupply() public view virtual override returns (uint256) {
        return return super.totalSupply() + s_baseToken.totalSupply() - s_baseToken.balanceOf(address(this));
    }

    function balanceOf(address account) public view virtual override returns (uint256) {
        return super.balanceOf(account) + s_baseToken.balanceOf(account);
    }
}

セキュリティ

アップグレード時のapproveとユーザの責任

ERC6864でベースとなるERC20トークンをアップグレードする場合、ユーザはまず元のERC20コントラクトに対して、**ERC6864コントラクトが自分のトークンを引き出せるように approve を行う必要があります。

つまり、アップグレードの前段階として「このERC6864コントラクトに対して、自分のトークンを一定量まで使ってよい」という許可を与えることになります。 そのため、ユーザには次のような責任が生じます。

1つ目は、「そのERC6864コントラクトが本当に安全で信頼できるかどうか」を自分で確認することです。 実装に問題があれば、許可したトークンを意図しない形で使われる可能性があります。

2つ目は、「どのくらいの数量を許可するか」が妥当かどうかを確認することです。 必要以上に大きな数量を許可すると、その分リスクも大きくなります。

これは、一般的なERC20の approve 操作と同じ性質のリスクであり、ERC6864だから特別に新しい種類のリスクが増えているわけではありません。

変換ロジックとフロントランニング/サンドイッチ攻撃のリスク

ERC6864のコントラクトは、アップグレード/ダウングレードの際に使う「変換ロジック」を自由に設計できます。 変換レートは、1対1の固定レートでもよく、数量に応じて直線的に変化するレートでもよく、さらに非線形(カーブ)に基づくレートでもかまわないとされています。

特に「非線形な変換関数」を使う場合、以下のような点で注意が必要です。

アップグレードやダウングレードの操作は、他のユーザのトランザクション状況によって結果が変わる可能性があります。 非線形な変換カーブでは、直前のトランザクションが変換レートに影響しやすく、そのためフロントランニング(先回りして取引する攻撃)やサンドイッチ攻撃(前後からトランザクションを挟み込む攻撃)の対象になりやすくなります。 こうした性質は、非線形カーブを使う自動マーケットメイカー(AMM)と同じです。 AMMで見られるような「他者の取引によってレートが動き、その隙を突かれる」というリスクが、同様にアップグレード/ダウングレード操作にも現れます。

まとめると、変換ロジックの設計次第では、ERC6864上のアップグレード/ダウングレードが、AMMと同じタイプの攻撃リスクを持つ可能性があるということになります。

無制限アローワンスと自動アップグレードによる確認回数の減少

ERC6864のコントラクトは、ユーザに対して「無制限の allowance(事実上上限なしの approve)」を求めることがあります。 さらに、参照実装のように、「トークン送付のタイミングで自動的にアップグレードを行う」といった挙動を組み込むこともできます。

本来、ユーザがアップグレードを行う流れは、以下の2段階になっており、これが「二重の確認」に相当します。ユーザは、許可と実行の2つの場面で意図を再確認できます。

  1. approve でトークン利用を許可する。
  2. upgrade を呼び出して実際にアップグレードする。

しかし、無制限 allowance +自動アップグレードのような設計を取ると、ユーザが明示的に毎回アップグレード操作を意識する機会が減ります。 結果として、ユーザが「本当にアップグレードしたいかどうか」を確認する回数が少なくなり、安全側に倒した設計からは離れていきます。 ここでは、「approve が二重確認の役割を持っているが、その機会が減る」という点がセキュリティ上の懸念として挙げられています。

複数実装・ネストによるトークン構造の複雑化

同じベースERC20トークンに対して、複数のIERC6864実装を適用することができます。 さらに、あるERC6864トークンを別のERC6864でラップするような「ネスト」構造を取ることも可能です。

このようにレイヤーを重ねていくと、トークンの状態や流れが複雑になります。 具体的には、ダッシュボードやブロックチェーンエクスプローラ、分析ツールなどが、トークンの残高や総供給量を正しく把握できなくなる可能性があります。 どのレイヤーのトークンを「本体」と見なすのか、どこまでを合算してレポートするのか、といった点で扱いが分かれ、表示される数値がツールごとに異なってしまうことがあります。

つまり、複数のIERC6864実装やネスト構造を多用すると、トークンエコシステム全体としては「見た目上の一貫性」が下がり、既存の可視化ツールが誤った値やバラついた値を表示するリスクがある、ということになります。

引用

Jeff Huang (@jeffishjeff), "ERC-6864: Upgradable Fungible Token [DRAFT]," Ethereum Improvement Proposals, no. 6864, April 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6864.

最後に

今回は「既存のERC20トークンをそのまま保持したまま別の実装へアップグレードできる仕組みを提案しているERC6864」についてまとめてきました! いかがだったでしょうか?

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

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