A fungible SRC-20 token contract and non-fungible SRC-721 token contract can be interlinked, allowing actions performed on one contract to be reflected on the other. This proposal defines how the relationship between the two token contracts can be queried. It also enables accounts to configure whether SRC-721 mints and transfers should be skipped during SRC-20 to SRC-721 synchronization.
Motivation
The SRC-20 fungible and SRC-721 non-fungible token standards offer sufficient flexibility for a co-joined, dual nature token pair. Transfers on the SRC-20 token can automatically trigger transfers on the SRC-721 token, and vice-versa. This enables applications such as native SRC-721 fractionalization, wherein acquiring SRC-20 tokens leads to the automatic issuance of SRC-721 tokens, proportional to the SRC-20 balance.
Dual nature token pairs maintain full compliance with both SRC-20 and SRC-721 token standards. This proposal aims to enhance the functionality of dual nature token pairs.
To facilitate querying the relationship between the tokens, extension interfaces are proposed for the SRC-20 and SRC-721 tokens respectively. This enables various quality of life improvements such as allowing decentralized exchanges and NFT marketplaces to display the relationship between the tokens.
Additionally, users can configure whether they want to skip SRC-721 mints and transfers during SRC-20 to SRC-721 synchronization.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Overview
A dual nature token pair comprises of an SRC-20 contract and an SRC-721 contract.
For convention, the SRC-20 contract is designated as the base contract, and the SRC-721 contract is designated as the mirror contract.
SRC-20 Extension Interface
The SRC-20 contract MUST implement the following interface.
interfaceISRC7631Base{/// @dev Returns the address of the mirror SRC-721 contract.
///
/// This method MAY revert or return the zero address
/// to denote that a mirror SRC-721 contract has not been linked.
///
/// If a non-zero address is returned, the returned address MUST
/// implement `ISRC7631Mirror` and its `baseSRC20()` method MUST
/// return the address of this contract.
///
/// Once a non-zero address has been returned, this method
/// MUST NOT revert and the returned value MUST NOT change.
functionmirrorSRC721()externalviewreturns(address);}
The SRC-20 contract MAY implement the following interface.
interfaceISRC7631BaseNFTSkippable{/// @dev Implementations SHOULD emit this event when the skip NFT status
/// of `owner` is updated to `status`.
///
/// The purpose of this event is to signal to indexers that the
/// skip NFT status has been changed.
///
/// For simplicity of implementation,
/// this event MAY be emitted even if the status is unchanged.
eventSkipNFTSet(addressindexedowner,boolstatus);/// @dev Returns true if SRC-721 mints and transfers to `owner` SHOULD be
/// skipped during SRC-20 to SRC-721 synchronization.
/// Otherwise, returns false.
///
/// This method MAY revert
/// (e.g. contract not initialized, method not supported).
///
/// If this method reverts:
/// - Interacting code SHOULD interpret `setSkipNFT` functionality as
/// unavailable and hide any functionality to call `setSkipNFT`.
/// - The skip NFT status for `owner` SHOULD be interpreted as undefined.
///
/// Once a true or false value has been returned for a given `owner`,
/// this method MUST NOT revert for the given `owner`.
functiongetSkipNFT(addressowner)externalviewreturns(bool);/// @dev Sets the caller's skip NFT status.
///
/// This method MAY revert
/// (e.g. insufficient permissions, method not supported).
///
/// It is RECOMMENDED to keep this method permissionless.
///
/// Emits a {SkipNFTSet} event.
functionsetSkipNFT(boolstatus)external;}
SRC-721 Extension Interface
The SRC-721 contract MUST implement the following interface.
interfaceISRC7631Mirror{/// @dev Returns the address of the base SRC-20 contract.
///
/// This method MAY revert or return the zero address
/// to denote that a base SRC-20 contract has not been linked.
///
/// If a non-zero address is returned, the returned address MUST
/// implement `ISRC7631Base` and its `mirrorSRC721()` method MUST
/// return the address of this contract.
///
/// Once a non-zero address has been returned, this method
/// MUST NOT revert and the returned value MUST NOT change.
functionbaseSRC20()externalviewreturns(address);}
Rationale
Implementation Detection
The mirrorSRC721 and baseSRC20 methods returning non-zero addresses signal that the SRC-20 and SRC-721 contracts implement the required interfaces respectively. As such, SRC-165 is not required.
The getSkipNFT and setSkipNFT methods MAY revert. As contracts compiled with Solidity or Vyper inherently revert on calls to undefined methods, a typical ISRC7631Base implementation lacking explicit getSkipNFT and setSkipNFT definitions still complies with ISRC7631BaseNFTSkippable.
NFT Skipping
The skip NFT methods allow accounts to avoid having SRC-721 tokens automatically minted to it whenever there is an SRC-20 transfer.
They are helpful in the following situations:
Loading vesting contracts with large amounts SRC-20 tokens to be vested to many users.
Loading candy machine contracts with large amounts of SRC-20 tokens to sell SRC-721 tokens to customers.
Transferring large amounts of SRC-20 tokens in / out of a liquidity pool.
Transferring large amounts of SRC-20 tokens between admin accounts.
Including the skip NFT methods in the standard will:
Enable applications to conveniently display the option for users to skip NFTs.
Enable applications to transfer any amount of SRC-20 tokens without the O(n) gas costs associated with minting multiple SRC-721 tokens, which can surpass the block gas limit.
These methods are recommended even on SVM chains with low gas costs, because bulk automatic SRC-721 transfers can still surpass the block gas limit.
A useful pattern is to make getSkipNFT return true by default if owner is a smart contract.
The choice of getSkipNFT returning a boolean value is for simplicity. If more complex behavior is needed, developers may add in extra methods of their own.
Implementation Conventions
The SRC-20 contract is designated as the base contract for convention, as a typical implementation can conveniently derive SRC-721 balances from the SRC-20 balances. This does not prohibit one from implementing most of the logic in the SRC-721 contract if required.
This proposal does not cover the token synchronization logic. This is to leave flexibility for various implementation patterns and novel use cases (e.g. automatically rebased tokens).
Linking Mechanism
The linking process is omitted for flexibility purposes. Developers can use any desired mechanism (e.g. linking in constructor, initializer, or via custom admin-only public methods on the two contracts). The only restriction is that the pairing must be immutable once established (to simplify indexing logic).
Backwards Compatibility
No backward compatibility issues found.
Security Considerations
Synchronization Access Guards
External methods for synchronization logic must be guarded such that only the other contract is authorized to call them.
Rare NFT Sniping
For dual nature collections that offer SRC-721 tokens with differing rarity levels, the SRC-721 metadata should be revealed in a way that is not easily gameable with metadata scraping and SRC-20 token transfers. A recommendation is to require that an SRC-721 token is held by the same account for some time before revealing its metadata.
Out-of-gas Denial of Service
Transferring SRC-20 tokens can automatically initiate the minting, transferring, or burning of multiple SRC-721 tokens. This can incur O(n) gas costs instead of the typical O(1) gas costs for SRC-20 tokens transfers. Logic for selecting SRC-721 token IDs can also incur additional gas costs. Synchronization logic must consider SRC-721 related gas costs to prevent out-of-gas denial of service issues.