# [ERC4931] ERC20トークンを新たな別のトークンに変換する仕組みを理解しよう！

> ERC4931は、ERC20トークンのアップグレードを行う標準的なAPIの実装を提案しています。

- URL: https://cardene.dev/articles/96698ba63fb0f3e5b197
- 公開日: 2025-04-19
- 更新日: 2025-04-19
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、**ERC20**トークンのアップグレードロジックを分離させた仕組みを提案している**ERC4931**についてまとめていきます！

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

https://eips.ethereum.org/EIPS/eip-4931

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

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**ERC4931**は、**ERC20**トークンのアップグレードを行う標準的なAPIの実装を提案しています。
具体的には、あるコントラクトのトークン（「ソーストークン」）を、別のコントラクトのトークン（「デスティネーショントークン」）へ変換する機能を持ったインターフェースを定義しています。

また、変換の時に必要となる基本的な情報（例えば、ソースとデスティネーションの各トークンコントラクトのアドレスや、どのような比率で変換が行われるか）を取得できる補助的なメソッドも提供されます。

## 動機

これまで**ERC20**トークンのアップグレードを行う時は、各トークン発行者が独自の仕組みを用意して、保有者が個別にそのインターフェースを使って古いトークンを新しいものへ交換する必要がありました。

**ERC4931**が提案する標準インターフェースを導入することで、保有者や中央集権型・分散型取引所は、より効率的にアップグレード処理を行えるようになります。
また、アップグレード用スクリプトの再利用が可能となるため、セキュリティ面での確認作業の負担も軽減されます。

さらに、トークン発行者にとっては、トークンのアップグレードを効果的に実装するための明確なガイドラインを提供することになります。

## 仕様

### IEIP4931

```js
interface IEIP4931 {
```

### メソッド

#### `upgradeSource`

```js
/// @dev A getter to determine the contract that is being upgraded from ("source contract")
/// @return The address of the source token contract
function upgradeSource() external view returns(address)
```

アップグレード元となるソーストークンのコントラクトアドレスを返す関数。

- **戻り値**
    - `address` 型のソーストークンのコントラクトアドレス。

#### `upgradeDestination`

```js
/// @dev A getter to determine the contract that is being upgraded to ("destination contract")
/// @return The address of the destination token contract
function upgradeDestination() external view returns(address)
```

アップグレード先となるデスティネーショントークンのコントラクトアドレスを返す関数。

- **戻り値**
    - `address` 型のデスティネーショントークンのコントラクトアドレス。

#### `isUpgradeActive`

```js
/// @dev The method will return true when the contract is serving upgrades and otherwise false
/// @return The status of the upgrade as a boolean
function isUpgradeActive() external view returns(bool)
```

現在アップグレードが有効かどうかを返す関数。

- **戻り値**
    - `bool` 型。
    - `true` の場合はアップグレードが利用可能、`false` の場合は無効。

#### `isDowngradeActive`

```js
/// @dev The method will return true when the contract is serving downgrades and otherwise false
/// @return The status of the downgrade as a boolean
function isDowngradeActive() external view returns(bool)
```

ダウングレードが有効か返すオプション関数。

- **戻り値**
    - `bool` 型。
    - ダウングレードが実装されていない場合、常に `false` を返します。

#### `ratio`

```js
/// @dev A getter for the ratio of destination tokens to source tokens received when conducting an upgrade
/// @return Two uint256, the first represents the numerator while the second represents
/// the denominator of the ratio of destination tokens to source tokens allotted during the upgrade
function ratio() external view returns(uint256, uint256)
```

ソーストークンからデスティネーショントークンへの変換比率を返す関数。

- **戻り値**
    - 2つの `uint256` 値。
    - 最初が分子（`numerator`）、次が分母（`denominator`）で、比率 `(numerator, denominator)` を表します。
    - 例
        - `(3, 1)` は 1 ソーストークンにつき 3 デスティネーショントークンを意味します。

#### `totalUpgraded`

```js
/// @dev A getter for the total amount of source tokens that have been upgraded to destination tokens.
/// The value may not be strictly increasing if the downgrade Optional Ext. is implemented.
/// @return The number of source tokens that have been upgraded to destination tokens
function totalUpgraded() external view returns(uint256)
```

これまでにアップグレードされたソーストークンの総数を返す関数。

- **戻り値**
    - `uint256` 型。
    - ダウングレード機能がある場合は減少する可能性もあります。

#### `computeUpgrade`

```js
/// @dev A method to mock the upgrade call determining the amount of destination tokens received from an upgrade
/// as well as the amount of source tokens that are left over as remainder
/// @param sourceAmount The amount of source tokens that will be upgraded
/// @return destinationAmount A uint256 representing the amount of destination tokens received if upgrade is called
/// @return sourceRemainder A uint256 representing the amount of source tokens left over as remainder if upgrade is called
function computeUpgrade(uint256 sourceAmount) external view
        returns (uint256 destinationAmount, uint256 sourceRemainder)
```

任意のソーストークン量に対し、アップグレード後に受け取れるデスティネーショントークンの量と余り（変換できなかったソーストークン）を計算する関数。

- **引数**
    - `sourceAmount (uint256)`
        - 変換対象のソーストークンの量。

- **戻り値**
    - `destinationAmount (uint256)`
        - アップグレード後に受け取れるデスティネーショントークンの量。
    - `sourceRemainder (uint256)`
        - 変換に使われなかった余りのソーストークン。

#### `computeDowngrade`

```js
/// @dev A method to mock the downgrade call determining the amount of source tokens received from a downgrade
/// as well as the amount of destination tokens that are left over as remainder
/// @param destinationAmount The amount of destination tokens that will be downgraded
/// @return sourceAmount A uint256 representing the amount of source tokens received if downgrade is called
/// @return destinationRemainder A uint256 representing the amount of destination tokens left over as remainder if upgrade is called
function computeDowngrade(uint256 destinationAmount) external view
        returns (uint256 sourceAmount, uint256 destinationRemainder)
```

任意のデスティネーショントークン量に対し、ダウングレード後に受け取れるソーストークンの量と余りを計算するオプション関数。

- **引数**
    - `destinationAmount (uint256)`
        - 変換対象のデスティネーショントークンの量。

- **戻り値**
  - `sourceAmount (uint256)`
      - ダウングレード後に受け取れるソーストークンの量。
  - `destinationRemainder (uint256)`
      - 変換に使われなかった余りのデスティネーショントークン。

#### `upgrade`

```js
/// @dev A method to conduct an upgrade from source token to destination token.
/// The call will fail if upgrade status is not true, if approve has not been called
/// on the source contract, or if sourceAmount is larger than the amount of source tokens at the msg.sender address.
/// If the ratio would cause an amount of tokens to be destroyed by rounding/truncation, the upgrade call will
/// only upgrade the nearest whole amount of source tokens returning the excess to the msg.sender address. 
/// Emits the Upgrade event
/// @param _to The address the destination tokens will be sent to upon completion of the upgrade
/// @param sourceAmount The amount of source tokens that will be upgraded 
function upgrade(address _to, uint256 sourceAmount) external
```

ソーストークンを指定された比率に基づいてデスティネーショントークンに変換し、指定されたアドレスに送付する関数。

`isUpgradeActive()` が `true` を返している状態で、ソーストークンの `approve()` が事前に呼ばれていることが実行条件になります。
小数以下が発生する場合は切り捨てられ、余剰分は `msg.sender` に返されます。
実行が成功すると`Upgrade`イベントが発行されます。

- **引数**
    - `_to (address)`
        - アップグレード後のトークン送り先アドレス。
    - `sourceAmount (uint256)`
        - 変換するソーストークンの量。

#### `downgrade`

```js
/// @dev A method to conduct a downgrade from destination token to source token.
/// The call will fail if downgrade status is not true, if approve has not been called
/// on the destination contract, or if destinationAmount is larger than the amount of destination tokens at the msg.sender address.
/// If the ratio would cause an amount of tokens to be destroyed by rounding/truncation, the downgrade call will only downgrade
/// the nearest whole amount of destination tokens returning the excess to the msg.sender address. 
///  Emits the Downgrade event
/// @param _to The address the source tokens will be sent to upon completion of the downgrade
/// @param destinationAmount The amount of destination tokens that will be downgraded 
function downgrade(address _to, uint256 destinationAmount) external
```

デスティネーショントークンをソーストークンに戻すオプション関数。

`isDowngradeActive()` が `true` を返す状態で、デスティネーショントークンの `approve()` が事前に呼ばれていることが実行条件です。
小数以下は切り捨てられ、余剰分は `msg.sender` に返されます。
実行が成功すると`Downgrade`イベントが発行されます。

- **引数**
    - `_to (address)`
        - ダウングレード後のトークン送り先アドレス。
    - `destinationAmount (uint256)`
        - 変換するデスティネーショントークンの量。

### イベント

#### `Upgrade`

アップグレードが完了した時に発行されるイベント。

- **引数**
    - `_from (address)`
        - アップグレードを実行したアドレス。
    - `_to (address)`
        -デスティネーショントークンが送られたアドレス。
    - `sourceAmount (uint256)`
        - 変換されたソーストークンの量。
    - `destinationAmount (uint256)`
        - 送信されたデスティネーショントークンの量。

#### `Downgrade`

ダウングレードが完了した時に発行されるイベント。

- **引数**
    - `_from (address)`
        - ダウングレードを実行したアドレス。
    - `_to (address)`
        - ソーストークンが送られたアドレス。
    - `sourceAmount (uint256)`
        - 送信されたソーストークンの量。
    - `destinationAmount (uint256)`
        - 変換されたデスティネーショントークンの量。

## 補足

これまで、GolemのGNTからGLMへのアップグレードのように、**ERC20**トークンのアップグレード機能をトークンコントラクト内に直接組み込む事例がいくつか存在しました。
しかし、このような設計は、アップグレードの機能と既存のトークン機能を密結合させてしまうため、望ましくないと考えられています。

**ERC4931**では、アップグレード専用の第三のコントラクトを使うことで、トークンの本来の機能とアップグレード機能を分離することを推奨しています。
これにより、資産保有者や取引所がトークンのアップグレード処理を再利用可能で簡素なスクリプトで実行できるようになり、将来的なアップグレード作業の負担が軽減されます。

また、このインターフェースは汎用的に設計されており、具体的な実装方法は開発者に委ねられています。
これにより、トークン側の仕様がアップグレード処理に干渉することを避けられます。

さらに、安全性と正当性を高めるために、アップグレード時のソーストークンはBurn可能であればBurnし、それ以外のトークンは`0x00`アドレスに送ります。
一方で、ダウングレード機能が実装されている場合は、トークン供給量の増加を防ぐため、ソーストークンはアップグレードコントラクト内にロックされる設計が標準となります。

## 互換性

**ERC4931**は既存の**ERC20**トークンとの互換性を壊すものではなく、後方互換性の問題はありません。

## 参考実装

```js:SourceUpgrade.sol
//SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.9;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./IEIP4931.sol";

contract SourceUpgrade is  IEIP4931 {
	using SafeERC20  for IERC20;

	uint256 constant RATIO_SCALE = 10**18;
    
	IERC20 private source;
	IERC20 private destination;
	bool private upgradeStatus;
	bool private downgradeStatus;
	uint256 private numeratorRatio;
	uint256 private denominatorRatio;
	uint256 private sourceUpgradedTotal;

	mapping(address => uint256) public upgradedBalance;

	constructor(address _source, address _destination, bool _upgradeStatus, bool _downgradeStatus, uint256 _numeratorRatio, uint256 _denominatorRatio) {
		require(_source != _destination, "SourceUpgrade: source and destination addresses are the same");
		require(_source != address(0), "SourceUpgrade: source address cannot be zero address");
		require(_destination != address(0), "SourceUpgrade: destination address cannot be zero address");
		require(_numeratorRatio > 0, "SourceUpgrade: numerator of ratio cannot be zero");
		require(_denominatorRatio > 0, "SourceUpgrade: denominator of ratio cannot be zero");

		source = IERC20(_source);
		destination = IERC20(_destination);
		upgradeStatus = _upgradeStatus;
		downgradeStatus = _downgradeStatus;
		numeratorRatio = _numeratorRatio;
		denominatorRatio = _denominatorRatio;
	}

	/// @dev A getter to determine the contract that is being upgraded from ("source contract")
	/// @return The address of the source token contract
	function upgradeSource() external view returns(address) {
		return address(source);
	}

	/// @dev A getter to determine the contract that is being upgraded to ("destination contract")
	/// @return The address of the destination token contract
	function upgradeDestination() external view returns(address) {
		return address(destination);
	}

	/// @dev The method will return true when the contract is serving upgrades and otherwise false
	/// @return The status of the upgrade as a boolean
	function isUpgradeActive() external view returns(bool) {
		return upgradeStatus;
	}

	/// @dev The method will return true when the contract is serving downgrades and otherwise false
	/// @return The status of the downgrade as a boolean
	function isDowngradeActive() external view returns(bool) {
		return downgradeStatus;
	}

	/// @dev A getter for the ratio of destination tokens to source tokens received when conducting an upgrade
	/// @return Two uint256, the first represents the numerator while the second represents
	/// the denominator of the ratio of destination tokens to source tokens allotted during the upgrade
	function ratio() external view returns(uint256, uint256) {
		return (numeratorRatio, denominatorRatio);
	}

	/// @dev A getter for the total amount of source tokens that have been upgraded to destination tokens.
	/// The value may not be strictly increasing if the downgrade Optional Ext. is implemented.
	/// @return The number of source tokens that have been upgraded to destination tokens
	function totalUpgraded() external view returns(uint256) {
		return sourceUpgradedTotal;
	}

	/// @dev A method to mock the upgrade call determining the amount of destination tokens received from an upgrade
	/// as well as the amount of source tokens that are left over as remainder
	/// @param sourceAmount The amount of source tokens that will be upgraded
	/// @return destinationAmount A uint256 representing the amount of destination tokens received if upgrade is called
	/// @return sourceRemainder A uint256 representing the amount of source tokens left over as remainder if upgrade is called
	function computeUpgrade(uint256 sourceAmount)
		public
		view
		returns (uint256 destinationAmount, uint256 sourceRemainder)
	{
		sourceRemainder = sourceAmount % (numeratorRatio / denominatorRatio);
		uint256 upgradeableAmount = sourceAmount - (sourceRemainder * RATIO_SCALE);
		destinationAmount = upgradeableAmount * (numeratorRatio / denominatorRatio);
	}

	/// @dev A method to mock the downgrade call determining the amount of source tokens received from a downgrade
	/// as well as the amount of destination tokens that are left over as remainder
	/// @param destinationAmount The amount of destination tokens that will be downgraded
	/// @return sourceAmount A uint256 representing the amount of source tokens received if downgrade is called
	/// @return destinationRemainder A uint256 representing the amount of destination tokens left over as remainder if upgrade is called
	function computeDowngrade(uint256 destinationAmount)
		public
		view
		returns (uint256 sourceAmount, uint256 destinationRemainder)
	{
		destinationRemainder = destinationAmount % (denominatorRatio / numeratorRatio);
		uint256 upgradeableAmount = destinationAmount - (destinationRemainder * RATIO_SCALE);
		sourceAmount = upgradeableAmount / (denominatorRatio / numeratorRatio);
	}

	/// @dev A method to conduct an upgrade from source token to destination token.
	/// The call will fail if upgrade status is not true, if approve has not been called
	/// on the source contract, or if sourceAmount is larger than the amount of source tokens at the msg.sender address.
	/// If the ratio would cause an amount of tokens to be destroyed by rounding/truncation, the upgrade call will
	/// only upgrade the nearest whole amount of source tokens returning the excess to the msg.sender address.
	/// Emits the Upgrade event
	/// @param _to The address the destination tokens will be sent to upon completion of the upgrade
	/// @param sourceAmount The amount of source tokens that will be upgraded
	function upgrade(address _to, uint256 sourceAmount) external {
		require(upgradeStatus == true, "SourceUpgrade: upgrade status is not active");
		(uint256 destinationAmount, uint256 sourceRemainder) = computeUpgrade(sourceAmount);
		sourceAmount -= sourceRemainder;
		require(sourceAmount > 0, "SourceUpgrade: disallow conversions of zero value");

		upgradedBalance[msg.sender] += sourceAmount;
		source.safeTransferFrom(
			msg.sender,
			address(this),
			sourceAmount
			);
		destination.safeTransfer(_to, destinationAmount);
		sourceUpgradedTotal += sourceAmount;
		emit Upgrade(msg.sender, _to, sourceAmount, destinationAmount);
	}

	/// @dev A method to conduct a downgrade from destination token to source token.
	/// The call will fail if downgrade status is not true, if approve has not been called
	/// on the destination contract, or if destinationAmount is larger than the amount of destination tokens at the msg.sender address.
	/// If the ratio would cause an amount of tokens to be destroyed by rounding/truncation, the downgrade call will only downgrade
	/// the nearest whole amount of destination tokens returning the excess to the msg.sender address.
	///  Emits the Downgrade event
	/// @param _to The address the source tokens will be sent to upon completion of the downgrade
	/// @param destinationAmount The amount of destination tokens that will be downgraded
	function downgrade(address _to, uint256 destinationAmount) external {
		require(upgradeStatus == true, "SourceUpgrade: upgrade status is not active");
		(uint256 sourceAmount, uint256 destinationRemainder) = computeDowngrade(destinationAmount);
		destinationAmount -= destinationRemainder;
		require(destinationAmount > 0, "SourceUpgrade: disallow conversions of zero value");
		require(upgradedBalance[msg.sender] >= sourceAmount,
			"SourceUpgrade: can not downgrade more than previously upgraded"
			);

		upgradedBalance[msg.sender] -= sourceAmount;
		destination.safeTransferFrom(
			msg.sender,
			address(this),
			destinationAmount
			);
		source.safeTransfer(_to, sourceAmount);
		sourceUpgradedTotal -= sourceAmount;
		emit Downgrade(msg.sender, _to, sourceAmount, destinationAmount);
	}
}
```

## セキュリティ

**ERC4931**における主なセキュリティ上の懸念は、アップグレード対象のソーストークンがアップグレード後に再びアクセス可能になってしまうことです。
これが発生すると、同じトークンが複数回アップグレードされるなどの不正が可能となり、アップグレードの正当性が損なわれるおそれがあります。

そのため、**ERC4931**ではソーストークンの扱いに関して厳格な制約を設けています。
具体的には、ソーストークンがBurn可能な場合は、アップグレード時に必ずBrumすることが求められます。
Burnできないトークンについては、`0x00` アドレス（誰もアクセスできないアドレス）へ送信することで、実質的にトークンを消失させることが求められます。

なお、オプション機能であるダウングレード機能が実装されている場合に限り、ソーストークンをアップグレードコントラクト内にロックして保持することが許容されます。
これは、後にダウングレード処理を可能にするための措置です。
ロックによっても不正な再利用を防ぐ仕組みが整っていれば、セキュリティ上問題はありません。

## 引用

John Peterson (@John-peterson-coinbase), Roberto Bayardo (@roberto-bayardo), David Núñez (@cygnusv), "ERC-4931: Generic Token Upgrade Standard [DRAFT]," Ethereum Improvement Proposals, no. 4931, November 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4931.

## 最後に

今回は「**ERC20**トークンのアップグレードロジックを分離させた仕組みを提案している**ERC4931**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
