[ERC6353] ERC20の送金時に一定の割合を別のアドレスに送付する仕組みを理解しよう！

# はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、**ERC20**トークンの送金時に、送金額の一定の割合を別のアドレスに送付する仕組みを提案している**ERC6353**についてまとめていきます！

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

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

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

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

# 概要

**ERC6353**は、**ERC20**トークンの機能を拡張してトークンの送金時に一定の割合を自動的に第三者へ送れるようにする提案です。
例えば、トークンの送金と同時に指定された慈善団体へ寄付を行うことができます。
また、貯蓄プログラムを自動化する仕組みとしても活用できます。

# 動機

ブロックチェーン上にはすでに多くの慈善団体のアドレスが存在しており、トークン保有者の中には送金のたびに自動的に寄付をしたいと考える人もいます。
しかし、現在の**ERC20**にはそのような仕組みの標準がありません。

この仕組みを標準化することで、ユーザーやアプリ開発者にとって利便性が向上します。
ユーザーは簡単に寄付ができてプロジェクト側も寄付を管理しやすくなります。
さらに、トークン保有者が寄付率を比較できるようになれば、透明性が高まり持続可能なブロックチェーン社会の実現につながります。

**ERC6353**は以下のメリットがあります。

- **簡単に寄付ができる**
    - トークンを送るだけで、自動的に寄付が完了するため、手間なく社会貢献ができます。
- **標準化による利便性向上**
    - どのプロジェクトでも統一されたインターフェースを使えるので、開発者が実装しやすくなります。
- **透明性が向上する**
    - ユーザーはどのトークンがどの団体に寄付しているのかを簡単に確認でき、寄付の流れがより明確になります。
- 他の用途にも応用できる
    - 例えば、一定の割合を貯蓄口座に送ることで自動的に貯金する仕組みを作ることも可能です。

# 仕様 

### コントラクトオーナーの役割  

コントラクトのオーナーは、審査を行った上で **whitelistedRate（許可リスト）** に慈善団体のアドレスを登録できます。
また、全体のデフォルトの寄付率を設定することもできます。
なお、アドレスを登録する時には寄付率を空（`null`）にしてはいけません。  

### トークン保有者の選択肢  

トークン保有者は、`_defaultAddress`（デフォルトの寄付先）から寄付先を選択できます。
ただし、このアドレスは空（`null`アドレス）でないことが推奨されます。
寄付先が `null` の場合だと寄付機能が動作しません。  

### 寄付の計算方法  

寄付はパーセンテージベースで計算されますが、具体的な計算方法は異なる場合があります。
この仕様を導入するプロジェクトやアプリは、`charityInfo()`関数を使って各アドレスに割り当てられた寄付率を取得できます。  

### 送金時の処理  

この標準では、`transfer`（通常送金）、`transferFrom`（代理送金）、`approve`（承認）の関数が拡張されており、寄付が自動で処理される仕組みになっています。  

- `transfer`、`transferFrom`が呼ばれると以下の処理が実行されます。
    - 送信者の残高から送金額と寄付額が引かれる。  
    - 受取人の残高に送金額が追加される。  
    - 寄付額が指定された寄付先に送られる。  
    - 2回の `Transfer` イベントが発行される（通常の送金と寄付の2つ）。  

- 残高不足の場合  
    - 送金額と寄付額を合わせた金額が送信者の残高を超えている場合、トランザクションは `revert`（失敗）します。  

## インターフェース

```js
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.4;

///
/// @dev Required interface of an ERC20 Charity compliant contract.
///
interface IERC20charity is IERC165 {
    /// The EIP-165 identifier for this interface is 0x557512b6

    
    /**
     * @dev Emitted when `toAdd` charity address is added to `whitelistedRate`.
     */
    event AddedToWhitelist (address toAdd);

    /**
     * @dev Emitted when `toRemove` charity address is deleted from `whitelistedRate`.
     */
    event RemovedFromWhitelist (address toRemove);

    /**
     * @dev Emitted when `_defaultAddress` charity address is modified and set to `whitelistedAddr`.
     */
    event DonnationAddressChanged (address whitelistedAddr);

    /**
     * @dev Emitted when `_defaultAddress` charity address is modified and set to `whitelistedAddr` 
    * and _donation is set to `rate`.
     */
    event DonnationAddressAndRateChanged (address whitelistedAddr,uint256 rate);

    /**
     * @dev Emitted when `whitelistedRate` for `whitelistedAddr` is modified and set to `rate`.
     */
    event ModifiedCharityRate(address whitelistedAddr,uint256 rate);
    
    /**
    *@notice Called with the charity address to determine if the contract whitelisted the address
    *and if it is the rate assigned.
    *@param addr - the Charity address queried for donnation information.
    *@return whitelisted - true if the contract whitelisted the address to receive donnation
    *@return defaultRate - the rate defined by the contract owner by default , the minimum rate allowed different from 0
    */
    function charityInfo(
        address addr
    ) external view returns (
        bool whitelisted,
        uint256 defaultRate
    );

    /**
    *@notice Add address to whitelist and set rate to the default rate.
    * @dev Requirements:
     *
     * - `toAdd` cannot be the zero address.
     *
     * @param toAdd The address to whitelist.
     */
    function addToWhitelist(address toAdd) external;

    /**
    *@notice Remove the address from the whitelist and set rate to the default rate.
    * @dev Requirements:
     *
     * - `toRemove` cannot be the zero address.
     *
     * @param toRemove The address to remove from whitelist.
     */
    function deleteFromWhitelist(address toRemove) external;

    /**
    *@notice Get all registered charity addresses.
     */
    function getAllWhitelistedAddresses() external ;

    /**
    *@notice Display for a user the rate of the default charity address that will receive donation.
     */
    function getRate() external view returns (uint256);

    /**
    *@notice Set personlised rate for charity address in {whitelistedRate}.
    * @dev Requirements:
     *
     * - `whitelistedAddr` cannot be the zero address.
     * - `rate` cannot be inferior to the default rate.
     *
     * @param whitelistedAddr The address to set as default.
     * @param rate The personalised rate for donation.
     */
    function setSpecificRate(address whitelistedAddr , uint256 rate) external;

    /**
    *@notice Set for a user a default charity address that will receive donation. 
    * The default rate specified in {whitelistedRate} will be applied.
    * @dev Requirements:
     *
     * - `whitelistedAddr` cannot be the zero address.
     *
     * @param whitelistedAddr The address to set as default.
     */
    function setSpecificDefaultAddress(address whitelistedAddr) external;

    /**
    *@notice Set for a user a default charity address that will receive donation. 
    * The rate is specified by the user.
    * @dev Requirements:
     *
     * - `whitelistedAddr` cannot be the zero address.
     * - `rate` cannot be less than to the default rate 
     * or to the rate specified by the owner of this contract in {whitelistedRate}.
     *
     * @param whitelistedAddr The address to set as default.
     * @param rate The personalised rate for donation.
     */
    function setSpecificDefaultAddressAndRate(address whitelistedAddr , uint256 rate) external;

    /**
    *@notice Display for a user the default charity address that will receive donation. 
    * The default rate specified in {whitelistedRate} will be applied.
     */
    function specificDefaultAddress() external view returns (
        address defaultAddress
    );

    /**
    *@notice Delete The Default Address and so deactivate donnations .
     */
    function deleteDefaultAddress() external;
}
```

**IERC20charity**は、**ERC20**トークンに寄付機能を追加するための規格です。
トークンの送金時に、指定した慈善団体へ自動的に一定の割合を寄付できる仕組みを提供します。  

### イベント  

いくつかのイベントが定義されており、これらはコントラクト内での変更を検知するのに役立ちます。

- **AddedToWhitelist**
    - 慈善団体のアドレスが許可リスト（`whitelistedRate`）に追加されたときに発行されます。  
- **RemovedFromWhitelist**
    - 許可リストから慈善団体のアドレスが削除されたときに発行されます。  
- **DonnationAddressChanged**
    - デフォルトの寄付先が変更されたときに発行されます。  
- **DonnationAddressAndRateChanged**
    - デフォルトの寄付先と寄付率が変更されたときに発行されます。  
- **ModifiedCharityRate**
    - 許可リスト内のアドレスの寄付率が変更されたときに発行されます。  

### 関数  

#### `charityInfo`  

指定したアドレスが寄付リストに登録されているかどうか、また登録されている場合の寄付率を確認する関数。 

#### `addToWhitelist`  

新しい慈善団体のアドレスを許可リストに追加してデフォルトの寄付率を設定する関数。  
ゼロアドレス（`0x0`）は登録できません。  

#### `deleteFromWhitelist`  

許可リストから慈善団体のアドレスを削除して寄付率をデフォルト値にリセットする関数。  
ゼロアドレスを削除することはできません。  

#### `getAllWhitelistedAddresses`  

許可リストに登録されているすべての慈善団体のアドレスを取得する関数。  

#### `getRate`  

ユーザーが設定したデフォルトの寄付先の寄付率を取得する関数。  

#### `setSpecificRate`  

特定の慈善団体に対して個別の寄付率を設定する関数。  
ゼロアドレスには設定できません。  
最低でもデフォルトの寄付率以上の値でなければなりません。  

#### `setSpecificDefaultAddress`  

ユーザーがデフォルトの寄付先を設定する関数。  
許可リストに登録されているアドレスのみ設定可能です。  

#### `setSpecificDefaultAddressAndRate`  

ユーザーがデフォルトの寄付先と寄付率を同時に設定する関数。  
ゼロアドレスには設定できません。  
寄付率は最低でもデフォルトの寄付率以上である必要があります。  

#### `specificDefaultAddress`  

ユーザーが設定しているデフォルトの寄付先を取得する関数。  

#### `deleteDefaultAddress`  

設定したデフォルトの寄付先を削除して寄付機能を無効にする関数。

# 補足

## なぜホワイトリストを使うのか？  

**ERC6353**では、慈善団体のアドレスをホワイトリスト（許可リスト）に登録する方式を採用しています。
具体的には、アドレスを配列に保存してマッピング（`whitelistedRate`）を使って各アドレスの有効状態を管理します。
これにより、複数の寄付先を選べるようになり、どのアドレスが寄付を受け取れるのかを透明化できます。  

また、コントラクトのオーナーが単一の寄付先アドレスを設定し、一定の期間ごとに変更することも可能です。
これにより、プロジェクトごとに柔軟な寄付戦略を取ることができます。  

## 送金時の処理と`revert`の仕組み  

送金者の残高が不足している場合（送金額 + 寄付額の合計が足りない場合）、トランザクションは`revert`（失敗）します。
これは、不足したまま送金を許可すると、意図しない動作が発生する可能性があるためです。  

寄付の処理は`transfer`関数の中で行われます。
これにより、新しく専用の寄付用の関数を作る必要がなくなりシンプルな実装が可能になります。
ただし、実装者によっては、寄付額が不足している場合でも送金だけは成功させるような仕様に変更することも考えられます。  

## 寄付の資金管理について  

寄付の処理方法も、プロジェクトごとに選択できます。  

- **直接寄付**

トークンの送金時に、寄付分をそのまま慈善団体に送る。  

- **コントラクトに蓄積して後で引き出し可能にする**  

寄付分をコントラクト内に保持して慈善団体が後で引き出せるようにする。  
その場合、慈善団体が`withdraw`関数や`claim`関数を使って、トークンを請求できる仕組みを作る。  
この方法では、トークン保有者が直接寄付するのではなく、慈善団体が必要に応じて寄付を受け取れる。  

## 寄付額の計算方法  

**ERC6353**では、寄付額を「送金額のパーセンテージ」で計算する方法を採用しています。
この方法だと、少額の送金でも比例した金額が寄付されるためさまざまなケースに柔軟に対応できます。  

ただし、トークンの実装者によっては、次のような異なる方法を採用することも考えられます。  

- 端数を繰り上げて寄付する方式（例：送金額が`99.5`トークンなら`100`トークンを消費し、`0.5`トークンを寄付）  
- 一定額以上の送金時のみ寄付を発生させる方式  

このように、寄付の計算方法は標準的なパーセンテージベース以外にも自由にカスタマイズできます。  

# 互換性

**ERC6353**は**ERC20**の機能拡張であり、**ERC20**標準のインターフェースと機能を維持しながら新しい機能が導入されています。
アカウントの残高が不足している場合に送金が失敗する可能性がある部分で互換性の問題があります。

# 参考実装

```js:ERC20Charity.sol
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/IERC20charity.sol";

/**
 *@title ERC720 charity Token
 *@author Aubay
 *@dev Extension of ERC720 Token that can be partially donated to a charity project
 *
 *This extensions keeps track of donations to charity addresses. The owner can chose the charity adresses listed.
 *Users can active the donation option or not and specify a different pourcentage than the default one donate.
 * A pourcentage af the amount of token transfered will be added and send to a charity address.
 */

abstract contract ERC20Charity is IERC20charity, ERC20, Ownable {
    mapping(address => uint256) public whitelistedRate; //Keep track of the rate for each charity address
    mapping(address => uint256) internal indexOfAddresses;
    mapping(address => mapping(address => uint256)) private _donation; //Keep track of the desired rate to donate for each user
    mapping(address => address) private _defaultAddress; //keep track of each user's default charity address

    address[] whitelistedAddresses; //Addresses whitelisted

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual override(IERC165) returns (bool) {
        return
            interfaceId == type(IERC20charity).interfaceId ||
            interfaceId == type(IERC165).interfaceId;
    }

    /**
     *@dev The default rate of donation can be override
     */
    function _defaultRate() internal pure virtual returns (uint256) {
        return 10; // 0.1%
    }

    /**
     *@dev The denominator to interpret the rate of donation , defaults to 10000 so rate are expressed in basis points, but may be customized by an override.
     * base 10000 , so 10000 =100% , 0 = 0% ,   2000 =20%
     */
    function _feeDenominator() internal pure virtual returns (uint256) {
        return 10000;
    }

    /**
     *@notice Add address to whitelist and set rate to the default rate.
     * @dev Requirements:
     *
     * - `toAdd` cannot be the zero address.
     *
     * @param toAdd The address to whitelist.
     */
    function addToWhitelist(address toAdd) external virtual onlyOwner {
        if (indexOfAddresses[toAdd] == 0) {
            whitelistedRate[toAdd] = _defaultRate();
            whitelistedAddresses.push(toAdd);
            indexOfAddresses[toAdd] = whitelistedAddresses.length;
        }

        emit AddedToWhitelist(toAdd);
    }

    /**
     *@notice Remove the address from the whitelist and set rate to the default rate.
     * @dev Requirements:
     *
     * - `toRemove` cannot be the zero address.
     *
     * @param toRemove The address to remove from whitelist.
     */
    function deleteFromWhitelist(address toRemove) external virtual onlyOwner {
        uint256 index1 = indexOfAddresses[toRemove];
        require(index1 > 0, "Invalid index"); //Indexing starts at 1, 0 is not allowed
        // move the last item into the index being vacated
        address lastValue = whitelistedAddresses[
            whitelistedAddresses.length - 1
        ];
        whitelistedAddresses[index1 - 1] = lastValue; // adjust for 1-based indexing
        indexOfAddresses[lastValue] = index1;
        whitelistedAddresses.pop();
        indexOfAddresses[toRemove] = 0;

        delete whitelistedRate[toRemove]; //whitelistedRate[toRemove] =0;
        emit RemovedFromWhitelist(toRemove);
    }

    /// @notice Get all registered charity addresses
    /// @return List of all registered donations addresses
    function getAllWhitelistedAddresses() external view returns (address[] memory) {
        return whitelistedAddresses;
    }

    /// @notice Display for a user the rate of the default charity address that will receive donation.
    /// @return The default rate of the registered address for the user.
    function getRate() external view returns (uint256) {
        return _donation[msg.sender][_defaultAddress[msg.sender]];
    }

    /**
     *@notice Set for a user a default charity address that will receive donation.
     * The default rate specified in {whitelistedRate} will be applied.
     * @dev Requirements:
     *
     * - `whitelistedAddr` cannot be the zero address.
     *
     * @param whitelistedAddr The address to set as default.
     */
    function setSpecificDefaultAddress(
        address whitelistedAddr
    ) external virtual {
        require(
            whitelistedRate[whitelistedAddr] != 0,
            "ERC20Charity: invalid whitelisted rate"
        );
        _defaultAddress[msg.sender] = whitelistedAddr;
        _donation[msg.sender][whitelistedAddr] = whitelistedRate[
            whitelistedAddr
        ];
        emit DonnationAddressChanged(whitelistedAddr);
    }

    /**
     *@notice Set for a user a default charity address that will receive donation.
     * The rate is specified by the user.
     * @dev Requirements:
     *
     * - `whitelistedAddr` cannot be the zero address.
     * - `rate` cannot be inferior to the default rate
     * or to the rate specified by the owner of this contract in {whitelistedRate}.
     *
     * @param whitelistedAddr The address to set as default.
     * @param rate The personalised rate for donation.
     */
    function setSpecificDefaultAddressAndRate(
        address whitelistedAddr,
        uint256 rate
    ) external virtual {
        require(
            rate <= _feeDenominator(),
            "ERC20Charity: rate must be between 0 and _feeDenominator"
        );
        require(
            rate >= _defaultRate(),
            "ERC20Charity: rate fee must exceed default rate"
        );
        require(
            rate >= whitelistedRate[whitelistedAddr],
            "ERC20Charity: rate fee must exceed the fee set by the owner"
        );
        require(
            whitelistedRate[whitelistedAddr] != 0,
            "ERC20Charity: invalid whitelisted address"
        );
        _defaultAddress[msg.sender] = whitelistedAddr;
        _donation[msg.sender][whitelistedAddr] = rate;
        emit DonnationAddressAndRateChanged(whitelistedAddr, rate);
    }

    /**
     *@notice Set personlised rate for charity address in {whitelistedRate}.
     * @dev Requirements:
     *
     * - `whitelistedAddr` cannot be the zero address.
     * - `rate` cannot be inferior to the default rate.
     *
     * @param whitelistedAddr The address to set as default.
     * @param rate The personalised rate for donation.
     */
    function setSpecificRate(
        address whitelistedAddr,
        uint256 rate
    ) external virtual onlyOwner {
        require(
            rate <= _feeDenominator(),
            "ERC20Charity: rate must be between 0 and _feeDenominator"
        );
        require(
            rate >= _defaultRate(),
            "ERC20Charity: rate fee must exceed default rate"
        );
        require(
            whitelistedRate[whitelistedAddr] != 0,
            "ERC20Charity: invalid whitelisted address"
        );
        whitelistedRate[whitelistedAddr] = rate;
        emit ModifiedCharityRate(whitelistedAddr, rate);
    }

    /**
     *@notice Display for a user the default charity address that will receive donation.
     * The default rate specified in {whitelistedRate} will be applied.
     */
    function specificDefaultAddress() external view virtual returns (address) {
        return _defaultAddress[msg.sender];
    }

    /**
     * inherit IERC20charity
     */
    function charityInfo(
        address charityAddr
    ) external view virtual returns (bool, uint256 rate) {
        rate = whitelistedRate[charityAddr];
        if (rate != 0) {
            return (true, rate);
        } else {
            return (false, rate);
        }
    }

    /**
     *@notice Delete The Default Address and so deactivate donnations .
     */
    function deleteDefaultAddress() external virtual {
        _defaultAddress[msg.sender] = address(0);
        emit DonnationAddressChanged(address(0));
    }

    /**
     *@notice Return the rate to donate.
     * @dev Requirements:
     *
     * - `from` cannot be the zero address
     *
     * @param from The address to get rate of donation.
     */
    function _returnRate(address from) internal virtual returns (uint256 rate) {
        address whitelistedAddr = _defaultAddress[from];
        rate = _donation[from][whitelistedAddr];
        if (
            whitelistedRate[whitelistedAddr] == 0 ||
            _defaultAddress[from] == address(0)
        ) {
            rate = 0;
        }
        return rate;
    }

    /**
     * @dev See {IERC20-transfer}.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - the caller must have a balance of at least `amount`.
     */
    function transfer(
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address owner = _msgSender();

        if (_defaultAddress[msg.sender] != address(0)) {
            address whitelistedAddr = _defaultAddress[msg.sender];
            uint256 rate = _returnRate(msg.sender);
            uint256 donate = (amount * rate) / _feeDenominator();
            _transfer(owner, whitelistedAddr, donate);
        }
        _transfer(owner, to, amount);
        return true;
    }

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);

        if (_defaultAddress[from] != address(0)) {
            address whitelistedAddr = _defaultAddress[from];
            uint256 rate = _returnRate(from);
            uint256 donate = (amount * rate) / _feeDenominator();
            _spendAllowance(from, spender, donate);
            _transfer(from, whitelistedAddr, donate);
        }
        _transfer(from, to, amount);
        return true;
    }

    /**
     * @dev See {IERC20-approve}.
     *
     * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on
     * `transferFrom`. This is semantically equivalent to an infinite approval.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function approve(
        address spender,
        uint256 amount
    ) public virtual override returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        if (_defaultAddress[msg.sender] != address(0)) {
            uint256 rate = _returnRate(msg.sender);
            uint256 donate = (amount * rate) / _feeDenominator();
            _approve(owner, spender, (donate + amount));
        }
        return true;
    }
}
```

# 引用

Aubay <blockchain-team@aubay.com>, BOCA Jeabby (@bjeabby1507), EL MERSHATI Laith (@lth-elm), KEMP Elia (@eliakemp), "ERC-6353: Charity token [DRAFT]," Ethereum Improvement Proposals, no. 6353, May 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6353.

# 最後に

今回は「**ERC20**トークンの送金時に、送金額の一定の割合を別のアドレスに送付する仕組みを提案している**ERC6353**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下の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