The following standard allows for the implementation of a standard API for ERC-20 token upgrades. This standard specifies an interface that supports the conversion of tokens from one contract (called the “source token”) to those from another (called the “destination token”), as well as several helper methods to provide basic information about the token upgrade (i.e. the address of the source and destination token contracts, the ratio that source will be upgraded to destination, etc.).
Motivation
Token contract upgrades typically require each asset holder to exchange their old tokens for new ones using a bespoke interface provided by the developers. This standard interface will allow asset holders as well as centralized and decentralized exchanges to conduct token upgrades more efficiently since token contract upgrade scripts will be essentially reusable. Standardization will reduce the security overhead involved in verifying the functionality of the upgrade contracts. It will also provide asset issuers clear guidance on how to effectively implement a token upgrade.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Please Note: Methods marked with (Optional Ext.) are a part of the optional extension for downgrade functionality and may remain unimplemented if downgrade functionality is not required.
Token Upgrade Interface Contract
interfaceIEIP4931{
Methods
upgradeSource
Returns the address of the original (source) token that will be upgraded.
/// @dev A getter to determine the contract that is being upgraded from ("source contract")
/// @return The address of the source token contract
functionupgradeSource()externalviewreturns(address)
upgradeDestination
Returns the address of the token contract that is being upgraded to.
/// @dev A getter to determine the contract that is being upgraded to ("destination contract")
/// @return The address of the destination token contract
functionupgradeDestination()externalviewreturns(address)
isUpgradeActive
Returns the current status of the upgrade functionality. Status MUST return true when the upgrade contract is functional and serving upgrades. It MUST return false when the upgrade contract is not currently serving upgrades.
/// @dev The method will return true when the contract is serving upgrades and otherwise false
/// @return The status of the upgrade as a boolean
functionisUpgradeActive()externalviewreturns(bool)
isDowngradeActive
Returns the current status of the downgrade functionality. Status MUST return true when the upgrade contract is functional and serving downgrades. It MUST return false when the upgrade contract is not currently serving downgrades. When the downgrade Optional Ext. is not implemented, this method will always return false to signify downgrades are not available.
/// @dev The method will return true when the contract is serving downgrades and otherwise false
/// @return The status of the downgrade as a boolean
functionisDowngradeActive()externalviewreturns(bool)
ratio
Returns the ratio of destination token to source token, expressed as a 2-tuple, that the upgrade will use. E.g. (3, 1) means the upgrade will provide 3 destination tokens for every 1 source token being upgraded.
/// @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
functionratio()externalviewreturns(uint256,uint256)
totalUpgraded
Returns the total number of tokens that have been upgraded from source to destination. If the downgrade Optional Ext. is implemented, calls to downgrade will reduce the totalUpgraded return value making it possible for the value to decrease between calls. The return value will be strictly increasing if downgrades are not implemented.
/// @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
functiontotalUpgraded()externalviewreturns(uint256)
computeUpgrade
Computes the destinationAmount of destination tokens that correspond to a given sourceAmount of source tokens, according to the predefined conversion ratio, as well as the sourceRemainder amount of source tokens that can’t be upgraded. For example, let’s consider a (3, 2) ratio, which means that 3 destination tokens are provided for every 2 source tokens; then, for a source amount of 5 tokens, computeUpgrade(5) must return (6, 1), meaning that 6 destination tokens are expected (in this case, from 4 source tokens) and 1 source token is left as remainder.
/// @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
functioncomputeUpgrade(uint256sourceAmount)externalviewreturns(uint256destinationAmount,uint256sourceRemainder)
computeDowngrade (Optional Ext.)
Computes the sourceAmount of source tokens that correspond to a given destinationAmount of destination tokens, according to the predefined conversion ratio, as well as the destinationRemainder amount of destination tokens that can’t be downgraded. For example, let’s consider a (3, 2) ratio, which means that 3 destination tokens are provided for every 2 source tokens; for a destination amount of 13 tokens, computeDowngrade(13) must return (4, 1), meaning that 4 source tokens are expected (in this case, from 12 destination tokens) and 1 destination token is left as remainder.
/// @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
functioncomputeDowngrade(uint256destinationAmount)externalviewreturns(uint256sourceAmount,uint256destinationRemainder)
upgrade
Upgrades the amount of source token to the destination token in the specified ratio. The destination tokens will be sent to the _to address. The function MUST lock the source tokens in the upgrade contract or burn them. If the downgrade Optional Ext. is implemented, the source tokens MUST be locked instead of burning. The function MUST throw if the caller’s address does not have enough source token to upgrade or if isUpgradeActive is returning false. The function MUST also fire the Upgrade event. approve MUST be called first on the source contract.
/// @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
functionupgrade(address_to,uint256sourceAmount)external
downgrade (Optional Ext.)
Downgrades the amount of destination token to the source token in the specified ratio. The source tokens will be sent to the _to address. The function MUST unwrap the destination tokens back to the source tokens. The function MUST throw if the caller’s address does not have enough destination token to downgrade or if isDowngradeActive is returning false. The function MUST also fire the Downgrade event. approve MUST be called first on the destination contract.
/// @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
functiondowngrade(address_to,uint256destinationAmount)external
Events
Upgrade
MUST trigger when tokens are upgraded.
/// @param _from Address that called upgrade
/// @param _to Address that destination tokens were sent to upon completion of the upgrade
/// @param sourceAmount Amount of source tokens that were upgraded
/// @param destinationAmount Amount of destination tokens sent to the _to address
eventUpgrade(addressindexed_from,addressindexed_to,uint256sourceAmount,uint256destinationAmount)
Downgrade (Optional Ext.)
MUST trigger when tokens are downgraded.
/// @param _from Address that called downgrade
/// @param _to Address that source tokens were sent to upon completion of the downgrade
/// @param sourceAmount Amount of source tokens sent to the _to address
/// @param destinationAmount Amount of destination tokens that were downgraded
eventDowngrade(addressindexed_from,addressindexed_to,uint256sourceAmount,uint256destinationAmount)}
Rationale
There have been several notable ERC20 upgrades (Ex. Golem: GNT -> GLM) where the upgrade functionality is written directly into the token contracts. We view this as a suboptimal approach to upgrades since it tightly couples the upgrade with the existing tokens. This EIP promotes the use of a third contract to facilitate the token upgrade to decouple the functionality of the upgrade from the functionality of the token contracts. Standardizing the upgrade functionality will allow asset holders and exchanges to write simplified reusable scripts to conduct upgrades which will reduce the overhead of conducting upgrades in the future. The interface aims to be intentionally broad leaving much of the specifics of the upgrade to the implementer, so that the token contract implementations do not interfere with the upgrade process. Finally, we hope to create a greater sense of security and validity for token upgrades by enforcing strict means of disposing of the source tokens during the upgrade. This is achieved by the specification of the upgrade method. The agreed upon norm is that burnable tokens shall be burned. Otherwise, tokens shall be effectively burned by being sent to the 0x00 address. When downgrade Optional Ext. is implemented, the default is instead to lock source tokens in the upgrade contract to avoid a series of consecutive calls to upgrade and downgrade from artificially inflating the supply of either token (source or destination).
Backwards Compatibility
There are no breaking backwards compatibility issues. There are previously implemented token upgrades that likely do not adhere to this standard. In these cases, it may be relevant for the asset issuers to communicate that their upgrade is not EIP-4931 compliant.
Reference Implementation
//SPDX-License-Identifier: Apache-2.0
pragmasolidity0.8.9;import"@openzeppelin/contracts/token/ERC20/IERC20.sol";import"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";import"./IEIP4931.sol";contractSourceUpgradeisIEIP4931{usingSafeERC20forIERC20;uint256constantRATIO_SCALE=10**18;IERC20privatesource;IERC20privatedestination;boolprivateupgradeStatus;boolprivatedowngradeStatus;uint256privatenumeratorRatio;uint256privatedenominatorRatio;uint256privatesourceUpgradedTotal;mapping(address=>uint256)publicupgradedBalance;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
functionupgradeSource()externalviewreturns(address){returnaddress(source);}/// @dev A getter to determine the contract that is being upgraded to ("destination contract")
/// @return The address of the destination token contract
functionupgradeDestination()externalviewreturns(address){returnaddress(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
functionisUpgradeActive()externalviewreturns(bool){returnupgradeStatus;}/// @dev The method will return true when the contract is serving downgrades and otherwise false
/// @return The status of the downgrade as a boolean
functionisDowngradeActive()externalviewreturns(bool){returndowngradeStatus;}/// @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
functionratio()externalviewreturns(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
functiontotalUpgraded()externalviewreturns(uint256){returnsourceUpgradedTotal;}/// @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
functioncomputeUpgrade(uint256sourceAmount)publicviewreturns(uint256destinationAmount,uint256sourceRemainder){sourceRemainder=sourceAmount%(numeratorRatio/denominatorRatio);uint256upgradeableAmount=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
functioncomputeDowngrade(uint256destinationAmount)publicviewreturns(uint256sourceAmount,uint256destinationRemainder){destinationRemainder=destinationAmount%(denominatorRatio/numeratorRatio);uint256upgradeableAmount=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
functionupgrade(address_to,uint256sourceAmount)external{require(upgradeStatus==true,"SourceUpgrade: upgrade status is not active");(uint256destinationAmount,uint256sourceRemainder)=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;emitUpgrade(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
functiondowngrade(address_to,uint256destinationAmount)external{require(upgradeStatus==true,"SourceUpgrade: upgrade status is not active");(uint256sourceAmount,uint256destinationRemainder)=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;emitDowngrade(msg.sender,_to,sourceAmount,destinationAmount);}}
Security Considerations
The main security consideration is ensuring the implementation of the interface handles the source tokens during the upgrade in such a way that they are no longer accessible. Without careful handling, the validity of the upgrade may come into question since source tokens could potentially be upgraded multiple times. This is why EIP-4931 will strictly enforce the use of burn for source tokens that are burnable. For non-burnable tokens, the accepted method is to send the source tokens to the 0x00 address. When the downgrade Optional Ext. is implemented, the constraint will be relaxed, so that the source tokens can be held by the upgrade contract.