Alert Source Discuss
⚠️ Review Standards Track: SRC

SRC-7590: SRC-20 Holder Extension for NFTs

Extension to allow NFTs to receive and transfer SRC-20 tokens.

Authors Steven Pineda (@steven2308), Jan Turk (@ThunderDeliverer)
Created 2024-01-05
Requires SIP-20, SIP-165, SIP-721

Abstract

This proposal suggests an extension to SRC-721 to enable easy exchange of SRC-20 tokens. By enhancing SRC-721, it allows unique tokens to manage and trade SRC-20 fungible tokens bundled in a single NFT. This is achieved by including methods to pull SRC-20 tokens into the NFT contract to a specific NFT, and transferring them out by the owner of such NFT. A transfer out nonce is included to prevent front-running issues.

Motivation

In the ever-evolving landscape of blockchain technology and decentralized ecosystems, interoperability between diverse token standards has become a paramount concern. By enhancing SRC-721 functionality, this proposal empowers non-fungible tokens (NFTs) to engage in complex transactions, facilitating the exchange of fungible tokens, unique assets, and multi-class assets within a single protocol.

This SRC introduces new utilities in the following areas:

  • Expanded use cases
  • Facilitating composite transactions
  • Market liquidity and value creation

Expanded Use Cases

Enabling SRC-721 tokens to handle various token types opens the door to a wide array of innovative use cases. From gaming and digital collectibles to decentralized finance (DeFi) and supply chain management, this extension enhances the potential of NFTs by allowing them to participate in complex, multi-token transactions.

Facilitating Composite Transactions

With this extension, composite transactions involving both fungible and non-fungible assets become easier. This functionality is particularly valuable for applications requiring intricate transactions, such as gaming ecosystems where in-game assets may include a combination of fungible and unique tokens.

Market Liquidity and Value Creation

By allowing SRC-721 tokens to hold and trade different types of tokens, it enhances liquidity for markets in all types of tokens.

Specification


interface ISRC7590 /*is ISRC165, ISRC721*/  {
    /**
     * @notice Used to notify listeners that the token received SRC-20 tokens.
     * @param src20Contract The address of the SRC-20 smart contract
     * @param toTokenId The ID of the token receiving the SRC-20 tokens
     * @param from The address of the account from which the tokens are being transferred
     * @param amount The number of SRC-20 tokens received
     */
    event ReceivedSRC20(
        address indexed src20Contract,
        uint256 indexed toTokenId,
        address indexed from,
        uint256 amount
    );

    /**
     * @notice Used to notify the listeners that the SRC-20 tokens have been transferred.
     * @param src20Contract The address of the SRC-20 smart contract
     * @param fromTokenId The ID of the token from which the SRC-20 tokens have been transferred
     * @param to The address receiving the SRC-20 tokens
     * @param amount The number of SRC-20 tokens transferred
     */
    event TransferredSRC20(
        address indexed src20Contract,
        uint256 indexed fromTokenId,
        address indexed to,
        uint256 amount
    );

    /**
     * @notice Used to retrieve the given token's specific SRC-20 balance
     * @param src20Contract The address of the SRC-20 smart contract
     * @param tokenId The ID of the token being checked for SRC-20 balance
     * @return The amount of the specified SRC-20 tokens owned by a given token
     */
    function balanceOfSRC20(
        address src20Contract,
        uint256 tokenId
    ) external view returns (uint256);

    /**
     * @notice Transfer SRC-20 tokens from a specific token.
     * @dev The balance MUST be transferred from this smart contract.
     * @dev MUST increase the transfer-out-nonce for the tokenId
     * @dev MUST revert if the `msg.sender` is not the owner of the NFT or approved to manage it.
     * @param src20Contract The address of the SRC-20 smart contract
     * @param tokenId The ID of the token to transfer the SRC-20 tokens from
     * @param amount The number of SRC-20 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferHeldSRC20FromToken(
        address src20Contract,
        uint256 tokenId,
        address to,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Transfer SRC-20 tokens to a specific token.
     * @dev The SRC-20 smart contract must have approval for this contract to transfer the SRC-20 tokens.
     * @dev The balance MUST be transferred from the `msg.sender`.
     * @param src20Contract The address of the SRC-20 smart contract
     * @param tokenId The ID of the token to transfer SRC-20 tokens to
     * @param amount The number of SRC-20 tokens to transfer
     * @param data Additional data with no specified format, to allow for custom logic
     */
    function transferSRC20ToToken(
        address src20Contract,
        uint256 tokenId,
        uint256 amount,
        bytes memory data
    ) external;

    /**
     * @notice Nonce increased every time an SRC20 token is transferred out of a token
     * @param tokenId The ID of the token to check the nonce for
     * @return The nonce of the token
     */
    function src20TransferOutNonce(
        uint256 tokenId
    ) external view returns (uint256);
}

Rationale

Pull Mechanism

We propose using a pull mechanism, where the contract transfers the token to itself, instead of receiving it via “safe transfer” for 2 reasons:

  1. Customizability with Hooks. By initiating the process this way, smart contract developers have the flexibility to execute specific actions before and after transferring the tokens.

  2. Lack of transfer with callback: SRC-20 tokens lack a standardized transfer with callback method, such as the “safeTransfer” on SRC-721, which means there is no reliable way to notify the receiver of a successful transfer, nor to know which is the destination token is.

This has the disadvantage of requiring approval of the token to be transferred before actually transferring it into an NFT.

Granular vs Generic

We considered 2 ways of presenting the proposal:

  1. A granular approach where there is an independent interface for each type of held token.
  2. A universal token holder which could also hold and transfer SRC-721 and SRC-1155.

An implementation of the granular version is slightly cheaper in gas, and if you’re using just one or two types, it’s smaller in contract size. The generic version is smaller and has single methods to send or receive, but it also adds some complexity by always requiring Id and amount on transfer methods. Id not being necessary for SRC-20 and amount not being necessary for SRC-721.

We also considered that due to the existence of safe transfer methods on both SRC-721 and SRC-1155, and the commonly used interfaces of ISRC721Receiver and ISRC1155Receiver, there is not much need to declare an additional interface to manage such tokens. However, this is not the case for SRC-20, which does not include a method with a callback to notify the receiver of the transfer.

For the aforementioned reasons, we decided to go with a granular approach.

Backwards Compatibility

No backward compatibility issues found.

Test Cases

Tests are included in src7590.ts.

To run them in terminal, you can use the following commands:

cd ../assets/sip-src7590
npm install
npx hardhat test

Reference Implementation

See SRC7590Mock.sol.

Security Considerations

The same security considerations as with SRC-721 apply: hidden logic may be present in any of the functions, including burn, add resource, accept resource, and more.

Caution is advised when dealing with non-audited contracts.

Implementations MUST use the message sender as from parameter when they are transferring tokens into an NFT. Otherwise, since the current contract needs approval, it could potentially pull the external tokens into a different NFT.

When transferring SRC-20 tokens in or out of an NFT, it could be the case that the amount transferred is not the same as the amount requested. This could happen if the SRC-20 contract has a fee on transfer. This could cause a bug on your Token Holder contract if you do not manage it properly. There are 2 ways to do it, both of which are valid:

  1. Use the ISRC20 interface to check the balance of the contract before and after the transfer, and revert if the balance is not the expected one, hence not supporting tokens with fees on transfer.
  2. Use the ISRC20 interface to check the balance of the contract before and after the transfer, and use the difference to calculate the amount of tokens that were actually transferred.

To prevent a seller from front running the sale of an NFT holding SRC-20 tokens to transfer out such tokens before a sale is executed, marketplaces MUST beware of the src20TransferOutNonce and revert if it has changed since listed.

SRC-20 tokens that are transferred directly to the NFT contract will be lost.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Steven Pineda (@steven2308), Jan Turk (@ThunderDeliverer), "SRC-7590: SRC-20 Holder Extension for NFTs [REVIEW]," Sila Improvement Proposals, no. 7590, January 2024. Available: https://sips.sila.org/SIPS/sip-7590.