ERC-4931: 通用代币升级标准
创建用于升级 ERC20 代币合约的标准接口。
Authors | John Peterson (@John-peterson-coinbase), Roberto Bayardo (@roberto-bayardo), David Núñez (@cygnusv) |
---|---|
Created | 2021-11-02 |
Discussion Link | https://ethereum-magicians.org/t/eip-4931-generic-token-upgrade-standard/8687 |
Requires | EIP-20 |
摘要
以下标准允许为 ERC-20 代币升级实现标准 API。此标准指定了一个接口,该接口支持将代币从一个合约(称为“源代币”)转换为另一个合约(称为“目标代币”)的代币,以及几个辅助方法,以提供有关代币升级的基本信息(即,源和目标代币合约的地址,源将升级到目标的比率等)。
动机
代币合约升级通常要求每个资产持有人使用开发人员提供的定制接口将其旧代币兑换为新代币。此标准接口将允许资产持有人以及中心化和去中心化交易所更有效地进行代币升级,因为代币合约升级脚本本质上是可重用的。标准化将减少验证升级合约功能所涉及的安全开销。它还将为资产发行人提供有关如何有效实施代币升级的明确指导。
规范
本文档中关键字“MUST”,“MUST NOT”,“REQUIRED”,“SHALL”,“SHALL NOT”,“SHOULD”,“SHOULD NOT”,“RECOMMENDED”,“MAY”和“OPTIONAL”应按照 RFC 2119 中的描述进行解释。
请注意:标有(可选扩展)的方法是降级功能的可选扩展的一部分,如果不需要降级功能,则可以不实现。
代币升级接口合约
interface IEIP4931 {
方法
upgradeSource
返回将要升级的原始(源)代币的地址。
/// @dev 用于确定要从哪个合约升级的 getter(“源合约”)
/// @return 源代币合约的地址
function upgradeSource() external view returns(address)
upgradeDestination
返回要升级到的代币合约的地址。
/// @dev 用于确定要升级到哪个合约的 getter(“目标合约”)
/// @return 目标代币合约的地址
function upgradeDestination() external view returns(address)
isUpgradeActive
返回升级功能的当前状态。当升级合约正在运行并且正在提供升级时,状态 MUST 返回 true
。当升级合约当前未提供升级时,它 MUST 返回 false
。
/// @dev 此方法在合约提供升级时返回 true,否则返回 false
/// @return 升级状态,布尔值
function isUpgradeActive() external view returns(bool)
isDowngradeActive
返回降级功能的当前状态。当升级合约正在运行并且正在提供降级时,状态 MUST 返回 true
。当升级合约当前未提供降级时,它 MUST 返回 false
。当未实现降级可选扩展时,此方法将始终返回 false
,表示降级不可用。
/// @dev 此方法在合约提供降级时返回 true,否则返回 false
/// @return 降级状态,布尔值
function isDowngradeActive() external view returns(bool)
ratio
返回目标代币与源代币的比率,表示为 2 元组,升级将使用该比率。例如,(3, 1)
表示对于升级的每个 1 个源代币,升级将提供 3 个目标代币。
/// @dev 用于获取进行升级时收到的目标代币与源代币的比率的 getter
/// @return 两个 uint256,第一个表示分子,第二个表示在升级期间分配的目标代币与源代币的比率的分母
function ratio() external view returns(uint256, uint256)
totalUpgraded
返回已从源升级到目标的代币总数。如果实现了降级可选扩展,则对 downgrade
的调用将减少 totalUpgraded
的返回值,从而使该值可以在调用之间减小。如果未实现降级,则返回值将严格增加。
/// @dev 用于获取已升级为目标代币的源代币总量的 getter。
/// 如果实现了降级可选扩展,则该值可能不会严格增加。
/// @return 已升级为目标代币的源代币数量
function totalUpgraded() external view returns(uint256)
computeUpgrade
根据预定义的转换比率,计算与给定 sourceAmount
的源代币对应的 destinationAmount
的目标代币,以及无法升级的 sourceRemainder
的源代币数量。例如,让我们考虑一个 (3, 2) 的比率,这意味着每 2 个源代币提供 3 个目标代币;那么,对于 5 个代币的源数量,computeUpgrade(5)
必须返回 (6, 1)
,这意味着预期有 6 个目标代币(在本例中,来自 4 个源代币)和 1 个源代币作为余数。
/// @dev 一种模拟升级调用的方法,用于确定从升级收到的目标代币数量
/// 以及作为余数剩余的源代币数量
/// @param sourceAmount 将要升级的源代币数量
/// @return destinationAmount 一个 uint256,表示如果调用升级收到的目标代币数量
/// @return sourceRemainder 一个 uint256,表示如果调用升级剩余的源代币数量
function computeUpgrade(uint256 sourceAmount) external view
returns (uint256 destinationAmount, uint256 sourceRemainder)
computeDowngrade (可选扩展)
根据预定义的转换比率,计算与给定 destinationAmount
的目标代币对应的 sourceAmount
的源代币,以及无法降级的 destinationRemainder
的目标代币数量。例如,让我们考虑一个 (3, 2) 的比率,这意味着每 2 个源代币提供 3 个目标代币;对于 13 个代币的目标数量,computeDowngrade(13)
必须返回 (4, 1)
,这意味着预期有 4 个源代币(在本例中,来自 12 个目标代币)和 1 个目标代币作为余数。
/// @dev 一种模拟降级调用的方法,用于确定从降级收到的源代币数量
/// 以及作为余数剩余的目标代币数量
/// @param destinationAmount 将要降级的目标代币数量
/// @return sourceAmount 一个 uint256,表示如果调用降级收到的源代币数量
/// @return destinationRemainder 一个 uint256,表示如果调用升级剩余的目标代币数量
function computeDowngrade(uint256 destinationAmount) external view
returns (uint256 sourceAmount, uint256 destinationRemainder)
upgrade
以指定的比率将 amount
的源代币升级为目标代币。目标代币将被发送到 _to
地址。该函数 MUST 将源代币锁定在升级合约中或将其销毁。如果实现了降级可选扩展,则必须锁定源代币而不是销毁。如果调用者的地址没有足够的源代币进行升级或者 isUpgradeActive
返回 false
,则该函数 MUST throw
。该函数还 MUST 触发 Upgrade
事件。必须首先在源合约上调用 approve
。
/// @dev 一种从源代币升级到目标代币的方法。
/// 如果升级状态不为 true,如果未在源合约上调用 approve,或者如果 sourceAmount 大于 msg.sender 地址的源代币数量,则调用将失败。
/// 如果比率会导致代币数量因舍入/截断而被销毁,则升级调用将仅升级最接近的源代币的整数数量,从而将多余的数量返回到 msg.sender 地址。
/// 发出 Upgrade 事件
/// @param _to 目标代币在升级完成后将发送到的地址
/// @param sourceAmount 将要升级的源代币数量
function upgrade(address _to, uint256 sourceAmount) external
downgrade (可选扩展)
以指定的比率将 amount
的目标代币降级为源代币。源代币将被发送到 _to
地址。该函数 MUST 将目标代币解包回源代币。如果调用者的地址没有足够的目标代币进行降级或者 isDowngradeActive
返回 false
,则该函数 MUST throw
。该函数还 MUST 触发 Downgrade
事件。必须首先在目标合约上调用 approve
。
/// @dev 一种从目标代币降级为源代币的方法。
/// 如果降级状态不为 true,如果未在目标合约上调用 approve,或者如果 destinationAmount 大于 msg.sender 地址的目标代币数量,则调用将失败。
/// 如果比率会导致代币数量因舍入/截断而被销毁,则降级调用将仅降级最接近的目标代币的整数数量,从而将多余的数量返回到 msg.sender 地址。
/// 发出 Downgrade 事件
/// @param _to 源代币在降级完成后将发送到的地址
/// @param destinationAmount 将要降级的目标代币数量
function downgrade(address _to, uint256 destinationAmount) external
事件
Upgrade
当代币升级时 MUST 触发。
/// @param _from 调用升级的地址
/// @param _to 升级完成后目标代币发送到的地址
/// @param sourceAmount 已升级的源代币数量
/// @param destinationAmount 发送到 _to 地址的目标代币数量
event Upgrade(address indexed _from, address indexed _to, uint256 sourceAmount, uint256 destinationAmount)
Downgrade (可选扩展)
当代币降级时 MUST 触发。
/// @param _from 调用降级的地址
/// @param _to 源代币发送到的地址
/// @param sourceAmount 发送到 _to 地址的源代币数量
/// @param destinationAmount 已降级的目标代币数量
event Downgrade(address indexed _from, address indexed _to, uint256 sourceAmount, uint256 destinationAmount)
}
理由
已经有几个值得注意的 ERC20 升级(例如,Golem:GNT -> GLM),其中升级功能直接写入代币合约中。我们认为这是升级的一种次优方法,因为它将升级与现有代币紧密结合在一起。此 EIP 提倡使用第三方合约来促进代币升级,从而将升级的功能与代币合约的功能分离。标准化升级功能将允许资产持有人和交易所编写简化的可重用脚本来进行升级,从而减少将来进行升级的开销。该接口旨在具有有意广泛性,将升级的许多细节留给实施者,以便代币合约实现不会干扰升级过程。最后,我们希望通过强制执行在升级期间严格处理源代币的方式,为代币升级创造更大的安全感和有效性。这是通过 upgrade
方法的规范来实现的。商定的规范是可燃烧代币应被燃烧。否则,应通过将代币发送到 0x00
地址来有效地销毁代币。当实现降级可选扩展时,约束将被放宽,以便升级合约可以持有源代币。从而避免从人为地夸大任一代币(源代币或目标代币)的供应量而连续调用 upgrade
和 downgrade
。
向后兼容性
没有破坏向后兼容性的问题。以前实现的一些代币升级可能不符合此标准。在这些情况下,资产发行人可能需要沟通他们的升级不符合 EIP-4931。
参考实现
//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 用于确定要从哪个合约升级的 getter(“源合约”)
/// @return 源代币合约的地址
function upgradeSource() external view returns(address) {
return address(source);
}
/// @dev 用于确定要升级到哪个合约的 getter(“目标合约”)
/// @return 目标代币合约的地址
function upgradeDestination() external view returns(address) {
return address(destination);
}
/// @dev 此方法在合约提供升级时返回 true,否则返回 false
/// @return 升级状态,布尔值
function isUpgradeActive() external view returns(bool) {
return upgradeStatus;
}
/// @dev 此方法在合约提供降级时返回 true,否则返回 false
/// @return 降级状态,布尔值
function isDowngradeActive() external view returns(bool) {
return downgradeStatus;
}
/// @dev 用于获取进行升级时收到的目标代币与源代币的比率的 getter
/// @return 两个 uint256,第一个表示分子,第二个表示在升级期间分配的目标代币与源代币的比率的分母
function ratio() external view returns(uint256, uint256) {
return (numeratorRatio, denominatorRatio);
}
/// @dev 用于获取已升级为目标代币的源代币总量的 getter。
/// 如果实现了降级可选扩展,则该值可能不会严格增加。
/// @return 已升级为目标代币的源代币数量
function totalUpgraded() external view returns(uint256) {
return sourceUpgradedTotal;
}
/// @dev 一种模拟升级调用的方法,用于确定从升级收到的目标代币数量
/// 以及作为余数剩余的源代币数量
/// @param sourceAmount 将要升级的源代币数量
/// @return destinationAmount 一个 uint256,表示如果调用升级收到的目标代币数量
/// @return sourceRemainder 一个 uint256,表示如果调用升级剩余的源代币数量
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 一种模拟降级调用的方法,用于确定从降级收到的源代币数量
/// 以及作为余数剩余的目标代币数量
/// @param destinationAmount 将要降级的目标代币数量
/// @return sourceAmount 一个 uint256,表示如果调用降级收到的源代币数量
/// @return destinationRemainder 一个 uint256,表示如果调用升级剩余的目标代币数量
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 一种从源代币升级到目标代币的方法。
/// 如果升级状态不为 true,如果未在源合约上调用 approve,或者如果 sourceAmount 大于 msg.sender 地址的源代币数量,则调用将失败。
/// 如果比率会导致代币数量因舍入/截断而被销毁,则升级调用将仅升级最接近的源代币的整数数量,从而将多余的数量返回到 msg.sender 地址。
/// 发出 Upgrade 事件
/// @param _to 目标代币在升级完成后将发送到的地址
/// @param sourceAmount 将要升级的源代币数量
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 一种从目标代币降级为源代币的方法。
/// 如果降级状态不为 true,如果未在目标合约上调用 approve,或者如果 destinationAmount 大于 msg.sender 地址的目标代币数量,则调用将失败。
/// 如果比率会导致代币数量因舍入/截断而被销毁,则降级调用将仅降级最接近的目标代币的整数数量,从而将多余的数量返回到 msg.sender 地址。
/// 发出 Downgrade 事件
/// @param _to 源代币在降级完成后将发送到的地址
/// @param destinationAmount 将要降级的目标代币数量
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);
}
}
安全注意事项
主要的安全性考虑是确保接口的实现以某种方式处理升级期间的源代币,使其不再可访问。如果没有仔细处理,升级的有效性可能会受到质疑,因为源代币可能会被多次升级。这就是为什么 EIP-4931 将严格执行对可燃烧的源代币使用 burn
。对于不可燃烧的代币,公认的方法是将源代币发送到 0x00
地址。当实现降级可选扩展时,约束将被放宽,以便升级合约可以持有源代币。
版权
版权和相关权利已通过 CC0 放弃。
Citation
Please cite this document as:
John Peterson (@John-peterson-coinbase), Roberto Bayardo (@roberto-bayardo), David Núñez (@cygnusv), "ERC-4931: 通用代币升级标准," Ethereum Improvement Proposals, no. 4931, November 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4931.