Refundable Tokens
Abstract
This ERC adds refund functionality for initial token offerings to ERC-20, ERC-721, and ERC-1155. Funds are held in escrow until a predetermined time before they are claimable. Until that predetermined time passes, users can receive a refund for tokens they have purchased.
Motivation
The NFT and token spaces lack accountability. For the health of the ecosystem as a whole, better mechanisms to prevent rugpulls from happening are needed. Offering refunds provides greater protection for buyers and increases legitimacy for creators.
A standard interface for this particular use case allows for certain benefits:
- Greater Compliance with EU “Distance Selling Regulations,” which require a 14-day refund period for goods (such as tokens) purchased online
- Interoperability with various NFT-related applications, such as portfolio browsers, and marketplaces
- NFT marketplaces could place a badge indicating that the NFT is still refundable on listings, and offer to refund NFTs instead of listing them on the marketplace
- DExes could offer to refund tokens if doing so would give a higher yield
- Better wallet confirmation dialogs
- Wallets can better inform the user of the action that is being taken (tokens being refunded), similar to how transfers often have their own unique dialog
- DAOs can better display the functionality of smart proposals that include refunding tokens
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.
All implementations MUST use and follow the directions of ERC-165.
ERC-20 Refund Extension
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.17;
import "ERC20.sol";
import "ERC165.sol";
/// @notice Refundable ERC-20 tokens
/// @dev The ERC-165 identifier of this interface is `0xf0ca2917`
interface ERC20Refund is ERC20, ERC165 {
/// @notice Emitted when a token is refunded
/// @dev Emitted by `refund`
/// @param _from The account whose assets are refunded
/// @param _amount The amount of token (in terms of the indivisible unit) that was refunded
event Refund(
address indexed _from,
uint256 indexed _amount
);
/// @notice Emitted when a token is refunded
/// @dev Emitted by `refundFrom`
/// @param _sender The account that sent the refund
/// @param _from The account whose assets are refunded
/// @param _amount The amount of token (in terms of the indivisible unit) that was refunded
event RefundFrom(
address indexed _sender,
address indexed _from,
uint256 indexed _amount
);
/// @notice As long as the refund is active, refunds the user
/// @dev Make sure to check that the user has the token, and be aware of potential re-entrancy vectors
/// @param amount The `amount` to refund
function refund(uint256 amount) external;
/// @notice As long as the refund is active and the sender has sufficient approval, refund the tokens and send the ether to the sender
/// @dev Make sure to check that the user has the token, and be aware of potential re-entrancy vectors
/// The ether goes to msg.sender.
/// @param from The user from which to refund the assets
/// @param amount The `amount` to refund
function refundFrom(address from, uint256 amount) external;
/// @notice Gets the refund price
/// @return _wei The amount of ether (in wei) that would be refunded for a single token unit (10**decimals indivisible units)
function refundOf() external view returns (uint256 _wei);
/// @notice Gets the first block for which the refund is not active
/// @return block The first block where the token cannot be refunded
function refundDeadlineOf() external view returns (uint256 block);
}
ERC-721 Refund Extension
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.17;
import "ERC721.sol";
import "ERC165.sol";
/// @notice Refundable ERC-721 tokens
/// @dev The ERC-165 identifier of this interface is `0xe97f3c83`
interface ERC721Refund is ERC721 /* , ERC165 */ {
/// @notice Emitted when a token is refunded
/// @dev Emitted by `refund`
/// @param _from The account whose assets are refunded
/// @param _tokenId The `tokenId` that was refunded
event Refund(
address indexed _from,
uint256 indexed _tokenId
);
/// @notice Emitted when a token is refunded
/// @dev Emitted by `refundFrom`
/// @param _sender The account that sent the refund
/// @param _from The account whose assets are refunded
/// @param _tokenId The `tokenId` that was refunded
event RefundFrom(
address indexed _sender,
address indexed _from,
uint256 indexed _tokenId
);
/// @notice As long as the refund is active for the given `tokenId`, refunds the user
/// @dev Make sure to check that the user has the token, and be aware of potential re-entrancy vectors
/// @param tokenId The `tokenId` to refund
function refund(uint256 tokenId) external;
/// @notice As long as the refund is active and the sender has sufficient approval, refund the token and send the ether to the sender
/// @dev Make sure to check that the user has the token, and be aware of potential re-entrancy vectors
/// The ether goes to msg.sender.
/// @param from The user from which to refund the token
/// @param tokenId The `tokenId` to refund
function refundFrom(address from, uint256 tokenId) external;
/// @notice Gets the refund price of the specific `tokenId`
/// @param tokenId The `tokenId` to query
/// @return _wei The amount of ether (in wei) that would be refunded
function refundOf(uint256 tokenId) external view returns (uint256 _wei);
/// @notice Gets the first block for which the refund is not active for a given `tokenId`
/// @param tokenId The `tokenId` to query
/// @return block The first block where token cannot be refunded
function refundDeadlineOf(uint256 tokenId) external view returns (uint256 block);
}
Optional ERC-721 Batch Refund Extension
// SPDX-License-Identifier: CC0-1.0;
import "ERC721Refund.sol";
/// @notice Batch Refundable ERC-721 tokens
/// @dev The ERC-165 identifier of this interface is ``
contract ERC721BatchRefund is ERC721Refund {
/// @notice Emitted when one or more tokens are batch refunded
/// @dev Emitted by `refundBatch`
/// @param _from The account whose assets are refunded
/// @param _tokenId The `tokenIds` that were refunded
event RefundBatch(
address indexed _from,
uint256[] _tokenIds // This may or may not be indexed
);
/// @notice Emitted when one or more tokens are batch refunded
/// @dev Emitted by `refundFromBatch`
/// @param _sender The account that sent the refund
/// @param _from The account whose assets are refunded
/// @param _tokenId The `tokenId` that was refunded
event RefundFromBatch(
address indexed _sender,
address indexed _from,
uint256 indexed _tokenId
);
/// @notice As long as the refund is active for the given `tokenIds`, refunds the user
/// @dev Make sure to check that the user has the tokens, and be aware of potential re-entrancy vectors
/// These must either succeed or fail together; there are no partial refunds.
/// @param tokenIds The `tokenId`s to refund
function refundBatch(uint256[] tokenIds) external;
/// @notice As long as the refund is active for the given `tokenIds` and the sender has sufficient approval, refund the tokens and send the ether to the sender
/// @dev Make sure to check that the user has the tokens, and be aware of potential re-entrancy vectors
/// The ether goes to msg.sender.
/// These must either succeed or fail together; there are no partial refunds.
/// @param from The user from which to refund the token
/// @param tokenIds The `tokenId`s to refund
function refundFromBatch(address from, uint256[] tokenIds) external;
}
ERC-1155 Refund Extension
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.17;
import "ERC1155.sol";
import "ERC165.sol";
/// @notice Refundable ERC-1155 tokens
/// @dev The ERC-165 identifier of this interface is `0x94029f5c`
interface ERC1155Refund is ERC1155 /* , ERC165 */ {
/// @notice Emitted when a token is refunded
/// @dev Emitted by `refund`
/// @param _from The account that requested a refund
/// @param _tokenId The `tokenId` that was refunded
/// @param _amount The amount of `tokenId` that was refunded
event Refund(
address indexed _from,
uint256 indexed _tokenId,
uint256 _amount
);
/// @notice Emitted when a token is refunded
/// @dev Emitted by `refundFrom`
/// @param _sender The account that sent the refund
/// @param _from The account whose assets are refunded
/// @param _tokenId The `tokenId` that was refunded
/// @param _amount The amount of `tokenId` that was refunded
event RefundFrom(
address indexed _sender,
address indexed _from,
uint256 indexed _tokenId
);
/// @notice As long as the refund is active for the given `tokenId`, refunds the user
/// @dev Make sure to check that the user has enough tokens, and be aware of potential re-entrancy vectors
/// @param tokenId The `tokenId` to refund
/// @param amount The amount of `tokenId` to refund
function refund(uint256 tokenId, uint256 amount) external;
/// @notice As long as the refund is active and the sender has sufficient approval, refund the tokens and send the ether to the sender
/// @dev Make sure to check that the user has enough tokens, and be aware of potential re-entrancy vectors
/// The ether goes to msg.sender.
/// @param from The user from which to refund the token
/// @param tokenId The `tokenId` to refund
/// @param amount The amount of `tokenId` to refund
function refundFrom(address from, uint256 tokenId, uint256 amount) external;
/// @notice Gets the refund price of the specific `tokenId`
/// @param tokenId The `tokenId` to query
/// @return _wei The amount of ether (in wei) that would be refunded for a single token
function refundOf(uint256 tokenId) external view returns (uint256 _wei);
/// @notice Gets the first block for which the refund is not active for a given `tokenId`
/// @param tokenId The `tokenId` to query
/// @return block The first block where the token cannot be refunded
function refundDeadlineOf(uint256 tokenId) external view returns (uint256 block);
}
Optional ERC-1155 Batch Refund Extension
// SPDX-License-Identifier: CC0-1.0;
import "ERC1155Refund.sol";
/// @notice Batch Refundable ERC-1155 tokens
/// @dev The ERC-165 identifier of this interface is ``
contract ERC1155BatchRefund is ERC1155Refund {
/// @notice Emitted when one or more tokens are batch refunded
/// @dev Emitted by `refundBatch`
/// @param _from The account that requested a refund
/// @param _tokenIds The `tokenIds` that were refunded
/// @param _amounts The amount of each `tokenId` that was refunded
event RefundBatch(
address indexed _from,
uint256[] _tokenIds, // This may or may not be indexed
uint256[] _amounts
);
/// @notice Emitted when one or more tokens are batch refunded
/// @dev Emitted by `refundFromBatch`
/// @param _sender The account that sent the refund
/// @param _from The account whose assets are refunded
/// @param _tokenIds The `tokenIds` that was refunded
/// @param _amounts The amount of each `tokenId` that was refunded
event RefundFromBatch(
address indexed _sender,
address indexed _from,
uint256[] _tokenId, // This may or may not be indexed
uint256[] _amounts
);
/// @notice As long as the refund is active for the given `tokenIds`, refunds the user
/// @dev Make sure to check that the user has enough tokens, and be aware of potential re-entrancy vectors
/// These must either succeed or fail together; there are no partial refunds.
/// @param tokenIds The `tokenId`s to refund
/// @param amounts The amount of each `tokenId` to refund
function refundBatch(uint256[] tokenIds, uint256[] amounts) external;
/// @notice As long as the refund is active for the given `tokenIds` and the sender has sufficient approval, refund the tokens and send the ether to the sender
/// @dev Make sure to check that the user has the tokens, and be aware of potential re-entrancy vectors
/// The ether goes to msg.sender.
/// These must either succeed or fail together; there are no partial refunds.
/// @param from The user from which to refund the token
/// @param tokenIds The `tokenId`s to refund
/// @param amounts The amount of each `tokenId` to refund
function refundFromBatch(address from, uint256[] tokenIds, uint256[] amounts external;
}
Rationale
refundDeadlineOf
uses blocks instead of timestamps, as timestamps are less reliable than block numbers.
The function names of refund
, refundOf
, and refundDeadlineOf
were chosen to fit the naming style of ERC-20, ERC-721, and ERC-1155.
ERC-165 is required as introspection by DApps would be made significantly harder if it were not.
Custom ERC-20 tokens are not supported, as it needlessly increases complexity, and the refundFrom
function allows for this functionality when combined with a DEx.
Batch refunds are optional, as account abstraction would make atomic operations like these significantly easier. However, they might still reduce gas costs if properly implemented.
Backwards Compatibility
No backward compatibility issues were found.
Security Considerations
There is a potential re-entrancy risk with the refund
function. Make sure to perform the ether transfer after the tokens are destroyed (i.e. obey the checks, effects, interactions pattern).
Copyright
Copyright and related rights waived via CC0.