はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、時間経過とともににallowance
しているERC20トークンの額を増加させていく仕組みを提案しているERC5827についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
概要
ERC5827は、ERC20の承認メカニズムに「renewable allowance」を追加する提案です。
具体的には、recoveryRate
というパラメータを導入し、トークンの許可額が時間の経過とともに回復する仕組みになっています。
例えば、1秒ごとに一定量のトークンが元の最大許可額に向かって回復していくイメージです。
動機
ERC20トークンでは、所有者が特定のアカウント(spender
)に対して、一定量のトークンを使う許可(allowance
)を与えることができます。
しかし、この仕組みは定期的な支払い(例:サブスクリプション、給与、積立投資など)には適していません。
現状では、多くのDAppsがこの問題を回避するために、ユーザーに「必要以上に大きな額」または「無制限のapprove
」を求めています。
しかし、これにはセキュリティリスクが伴います。
悪意のあるDAppが、ユーザーの許可範囲内で資金を一気に引き出してしまう可能性があるからです。
多くのユーザーは、この許可を与えることのリスクを十分に理解していない場合もあります。
なぜrenewable allowanceが必要なのか
renewable allowanceを導入することで、クレジットカードの利用限度額やデビットカードのチャージ残高のような仕組みをブロックチェーン上で実現できます。
例えば、アカウントの所有者は以下のようなルールを設定できます。
- 「1日あたり〇〇トークンまで使える」
- 「一定期間ごとに使える額が自動で回復する」
これにより、不正利用のリスクを抑えつつ、定期的な支払いをスムーズに行えるようになります。
仕様
ERC5827では、ERC20の承認(allowance
)に「自動回復」の仕組みを追加するための仕様を定義しています。
IERC5827という新しいインターフェースを導入し、承認されたトークンの利用可能額が時間とともに回復するようにします。
また、既存のERC-20トークンでもこの仕組みを使えるようにするためのプロキシコントラクトや、一定時間後に自動的に失効する有効期限付きバージョン(IERC5827Expirable)についても規定されています。
pragma solidity ^0.8.0;
interface IERC5827 /* is ERC20, ERC165 */ {
/*
* Note: the ERC-165 identifier for this interface is 0x93cd7af6.
* 0x93cd7af6 ===
* bytes4(keccak256('approveRenewable(address,uint256,uint256)')) ^
* bytes4(keccak256('renewableAllowance(address,address)')) ^
* bytes4(keccak256('approve(address,uint256)') ^
* bytes4(keccak256('transferFrom(address,address,uint256)') ^
* bytes4(keccak256('allowance(address,address)') ^
*/
/**
* @notice Thrown when the available allowance is less than the transfer amount.
* @param available allowance available; 0 if unset
*/
error InsufficientRenewableAllowance(uint256 available);
/**
* @notice Emitted when any allowance is set.
* @dev MUST be emitted even if a non-renewable allowance is set; if so, the
* @dev `_recoveryRate` MUST be 0.
* @param _owner owner of token
* @param _spender allowed spender of token
* @param _value initial and maximum allowance granted to spender
* @param _recoveryRate recovery amount per second
*/
event RenewableApproval(
address indexed _owner,
address indexed _spender,
uint256 _value,
uint256 _recoveryRate
);
/**
* @notice Grants an allowance of `_value` to `_spender` initially, which recovers over time
* @notice at a rate of `_recoveryRate` up to a limit of `_value`.
* @dev SHOULD cause `allowance(address _owner, address _spender)` to return `_value`,
* @dev SHOULD throw when `_recoveryRate` is larger than `_value`, and MUST emit a
* @dev `RenewableApproval` event.
* @param _spender allowed spender of token
* @param _value initial and maximum allowance granted to spender
* @param _recoveryRate recovery amount per second
*/
function approveRenewable(
address _spender,
uint256 _value,
uint256 _recoveryRate
) external returns (bool success);
/**
* @notice Returns approved max amount and recovery rate of allowance granted to `_spender`
* @notice by `_owner`.
* @dev `amount` MUST also be the initial approval amount when a non-renewable allowance
* @dev has been granted, e.g. with `approve(address _spender, uint256 _value)`.
* @param _owner owner of token
* @param _spender allowed spender of token
* @return amount initial and maximum allowance granted to spender
* @return recoveryRate recovery amount per second
*/
function renewableAllowance(address _owner, address _spender)
external
view
returns (uint256 amount, uint256 recoveryRate);
/// Overridden ERC-20 functions
/**
* @notice Grants a (non-increasing) allowance of _value to _spender and clears any existing
* @notice renewable allowance.
* @dev MUST clear set `_recoveryRate` to 0 on the corresponding renewable allowance, if
* @dev any.
* @param _spender allowed spender of token
* @param _value allowance granted to spender
*/
function approve(address _spender, uint256 _value)
external
returns (bool success);
/**
* @notice Moves `amount` tokens from `from` to `to` using the caller's allowance.
* @dev When deducting `amount` from the caller's allowance, the allowance amount used
* @dev SHOULD include the amount recovered since the last transfer, but MUST NOT exceed
* @dev the maximum allowed amount returned by `renewableAllowance(address _owner, address
* @dev _spender)`.
* @dev SHOULD also throw `InsufficientRenewableAllowance` when the allowance is
* @dev insufficient.
* @param from token owner address
* @param to token recipient
* @param amount amount of token to transfer
*/
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
/**
* @notice Returns amount currently spendable by `_spender`.
* @dev The amount returned MUST be as of `block.timestamp`, if a renewable allowance
* @dev for the `_owner` and `_spender` is present.
* @param _owner owner of token
* @param _spender allowed spender of token
* @return remaining allowance at the current point in time
*/
function allowance(address _owner, address _spender)
external
view
returns (uint256 remaining);
}
主要機能
自動回復型allowance
(IERC5827)
IERC5827では、トークンの承認(approve
)にrecoveryRate
というパラメータを追加し、指定された額まで毎秒回復する仕組みを提供します。
approveRenewable(address _spender, uint256 _value, uint256 _recoveryRate)
- 指定したアカウント(
_spender
)に対し、最大_value
のallowance
を付与し、毎秒_recoveryRate
ずつ回復するようにします。 -
_recoveryRate
は_value
を超えてはいけません。 - 実行時には
RenewableApproval
イベントが発行されます。
renewableAllowance(address _owner, address _spender)
-
_owner
が_spender
に与えた**最大allowance
と回復速度(recoveryRate
)**を取得できます。
この仕組みにより、ユーザーは無制限の承認を与えるリスクを避けつつ、定期的な支払い(例:サブスク料金や積立投資)を安全に行うことができます。
既存のERC20トークンとの互換性
既存のERC20の approve(address _spender, uint256 _value)
も引き続き使えます。
ただし、この場合、回復速度(recoveryRate
)は0になります。
また、allowance()
と transferFrom()
も、回復型allowance
に対応できるように改修が必要です。
プロキシコントラクト(IERC5827Proxy)
interface IERC5827Proxy /* is IERC5827 */ {
/*
* Note: the ERC-165 identifier for this interface is 0xc55dae63.
* 0xc55dae63 ===
* bytes4(keccak256('baseToken()')
*/
/**
* @notice Get the underlying base token being proxied.
* @return baseToken address of the base token
*/
function baseToken() external view returns (address);
}
既存のERC20トークンでも、直接コードを変更せずにこの機能を利用できるようにするための仕組みです。
-
baseToken()
- もともとのERC20トークンのアドレスを取得するための関数です。
-
transfer()
- 直接の
transfer
は行いますが、元のERC20トークンがすでにTransfer
イベントを発行しているため、新たにTransfer
イベントを発行しない 仕様になっています。
- 直接の
このプロキシを使えばERC20の仕様を変えずに、新しい機能だけを追加できます。
有効期限付きallowance
(IERC5827Expirable)
interface IERC5827Expirable /* is IERC5827 */ {
/*
* Note: the ERC-165 identifier for this interface is 0x46c5b619.
* 0x46c5b619 ===
* bytes4(keccak256('approveRenewable(address,uint256,uint256,uint64)')) ^
* bytes4(keccak256('renewableAllowance(address,address)')) ^
*/
/**
* @notice Grants an allowance of `_value` to `_spender` initially, which recovers over time
* @notice at a rate of `_recoveryRate` up to a limit of `_value` and expires at
* @notice `_expiration`.
* @dev SHOULD throw when `_recoveryRate` is larger than `_value`, and MUST emit
* @dev `RenewableApproval` event.
* @param _spender allowed spender of token
* @param _value initial allowance granted to spender
* @param _recoveryRate recovery amount per second
* @param _expiration Unix time (in seconds) at which the allowance expires
*/
function approveRenewable(
address _spender,
uint256 _value,
uint256 _recoveryRate,
uint64 _expiration
) external returns (bool success);
/**
* @notice Returns approved max amount, recovery rate, and expiration timestamp.
* @return amount initial and maximum allowance granted to spender
* @return recoveryRate recovery amount per second
* @return expiration Unix time (in seconds) at which the allowance expires
*/
function renewableAllowance(address _owner, address _spender)
external
view
returns (uint256 amount, uint256 recoveryRate, uint64 expiration);
}
時間が経過すると自動的にallowance
が失効するバージョンです。
approveRenewable(address _spender, uint256 _value, uint256 _recoveryRate, uint64 _expiration)
-
_spender
に_value
のallowance
を与え、_recoveryRate
で回復させつつ、_expiration
の時刻を超えたら失効するように設定します。
renewableAllowance(address _owner, address _spender)
- 最大
allowance
、回復速度に加えて失効時刻(expiration
)も取得できるようになっています。
この仕組みを使えば、例えば「1ヶ月だけ有効な支払い枠」などを設定でき、不要な承認を放置するリスクを減らすことができます。
補足
renewable allowanceは、一定時間ごとにリセットする方法でも実装できます。
しかし、継続的に回復する recoveryRate
を採用することでより柔軟に使えるようになります。
この方法により、決まった時間ごとのリセットに縛られずにトークンの利用可能額が常にスムーズに回復するので、シンプルなロジックで管理しやすくなります。
互換性
ERC5827を使うために、新しくERC20トークンを作り直す必要はありません。
すでに発行されているERC20トークンでも、プロキシコントラクト(IERC5827Proxy)を利用すれば、renewable allowanceの機能を後から追加できます。
参考実装
最小限の実装コードが提供されています。
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.17;
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
import "./IERC5827.sol";
contract ERC5827 is ERC20, IERC5827 {
struct RenewableAllowance {
uint256 amount;
uint192 recoveryRate;
uint64 lastUpdated;
}
// owner => spender => renewableAllowance
mapping(address => mapping(address => RenewableAllowance))
private rAllowance;
constructor(
string memory name_,
string memory symbol_
) ERC20(name_, symbol_) {}
function approve(
address _spender,
uint256 _value
) public override(ERC20, IERC5827) returns (bool success) {
address owner = _msgSender();
_approve(owner, _spender, _value, 0);
return true;
}
function approveRenewable(
address _spender,
uint256 _value,
uint256 _recoveryRate
) public override returns (bool success) {
address owner = _msgSender();
_approve(owner, _spender, _value, _recoveryRate);
return true;
}
function _approve(
address _owner,
address _spender,
uint256 _value,
uint256 _recoveryRate
) internal virtual {
require(
_recoveryRate <= _value,
"recoveryRate must be less than or equal to value"
);
rAllowance[_owner][_spender] = RenewableAllowance({
amount: _value,
recoveryRate: uint192(_recoveryRate),
lastUpdated: uint64(block.timestamp)
});
_approve(_owner, _spender, _value);
emit RenewableApproval(_owner, _spender, _value, _recoveryRate);
}
/// @notice fetch amounts spendable by _spender
/// @return remaining allowance at the current point in time
function allowance(
address _owner,
address _spender
) public view override(ERC20, IERC5827) returns (uint256 remaining) {
return _remainingAllowance(_owner, _spender);
}
/// @dev returns the sum of two uint256 values, saturating at 2**256 - 1
function saturatingAdd(
uint256 a,
uint256 b
) internal pure returns (uint256) {
unchecked {
uint256 c = a + b;
if (c < a) return type(uint256).max;
return c;
}
}
function _remainingAllowance(
address _owner,
address _spender
) private view returns (uint256) {
RenewableAllowance memory a = rAllowance[_owner][_spender];
uint256 remaining = super.allowance(_owner, _spender);
uint256 recovered = uint256(a.recoveryRate) *
uint64(block.timestamp - a.lastUpdated);
uint256 remainingAllowance = saturatingAdd(remaining, recovered);
return remainingAllowance > a.amount ? a.amount : remainingAllowance;
}
/// @notice fetch approved max amount and recovery rate
/// @return amount initial and maximum allowance given to spender
/// @return recoveryRate recovery amount per second
function renewableAllowance(
address _owner,
address _spender
) public view returns (uint256 amount, uint256 recoveryRate) {
RenewableAllowance memory a = rAllowance[_owner][_spender];
return (a.amount, uint256(a.recoveryRate));
}
/// @notice transfers base token with renewable allowance logic applied
/// @param from owner of base token
/// @param to recipient of base token
/// @param amount amount to transfer
function transferFrom(
address from,
address to,
uint256 amount
) public override(ERC20, IERC5827) returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual override {
(uint256 maxAllowance, ) = renewableAllowance(owner, spender);
if (maxAllowance != type(uint256).max) {
uint256 currentAllowance = _remainingAllowance(owner, spender);
if (currentAllowance < amount) {
revert InsufficientRenewableAllowance({
available: currentAllowance
});
}
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
rAllowance[owner][spender].lastUpdated = uint64(block.timestamp);
}
}
function supportsInterface(
bytes4 interfaceId
) public view virtual returns (bool) {
return interfaceId == type(IERC5827).interfaceId;
}
}
セキュリティ
ERC5827では、無制限のallowace
を許可するERC20よりもより厳格なルールを導入しています。
しかし、recoveryRate
が大きく設定されると、短時間で大量のトークンを送ることが可能になるため注意が必要です。
また、ERC5827に対応していないアプリは、allowance()
の返り値や Approval
イベントに記録された値を最大利用可能額だと誤解する可能性があります。
しかし、実際にはトークンのallowance
は時間とともに回復するため、これらのアプリでは誤った判断をする恐れがあります。
アプリ開発者は、ERC5827の特性を理解して誤った残高計算をしないように設計することが重要です。
引用
zlace (@zlace0x), zhongfu (@zhongfu), edison0xyz (@edison0xyz), "ERC-5827: Auto-renewable allowance extension [DRAFT]," Ethereum Improvement Proposals, no. 5827, October 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5827.
最後に
今回は「時間経過とともににallowance
しているERC20トークンの額を増加させていく仕組みを提案しているERC5827」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!