Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ERC: ERC-20 Holder Extension for NFTs #187

Merged
merged 13 commits into from
Jan 9, 2024
178 changes: 178 additions & 0 deletions ERCS/erc-7590.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
eip: 7590
title: ERC-20 Holder Extension for NFTs
description: Extension to allow NFTs to receive and transfer ERC-20 tokens.
author: Steven Pineda (@steven2308), Jan Turk (@ThunderDeliverer)
discussions-to: https://ethereum-magicians.org/t/token-holder-extension-for-nfts/16260
status: Draft
type: Standards Track
category: ERC
created: 2024-01-05
requires: 20, 165, 721
---

## Abstract

This proposal suggests an extension to [ERC-721](./eip-721.md) to enable easy exchange of [ERC-20](./eip-20.md) tokens. By enhancing [ERC-721](./eip-721.md), it allows unique tokens to manage and trade [ERC-20](./eip-20.md) fungible tokens bundled in a single NFT.
steven2308 marked this conversation as resolved.
Show resolved Hide resolved

## Motivation

In the ever-evolving landscape of blockchain technology and decentralized ecosystems, interoperability between diverse token standards has become a paramount concern. By enhancing [ERC-721](./eip-721.md) 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 ERC introduces new utilities in the following areas:
- Expanded use cases
- Facilitating composite transactions
- Market liquidity and value creation

### Expanded Use Cases

Enabling [ERC-721](./eip-721.md) 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 [ERC-721](./eip-721.md) tokens to hold and trade different types of tokens, it enhances liquidity for markets in all types of tokens.

## Specification

```solidity

interface IERC7590 /*is IERC165, IERC721*/ {
/**
* @notice Used to notify listeners that the token received ERC-20 tokens.
* @param erc20Contract The address of the ERC-20 smart contract
* @param toTokenId The ID of the token receiving the ERC-20 tokens
* @param from The address of the account from which the tokens are being transferred
* @param amount The number of ERC-20 tokens received
*/
event ReceivedERC20(
address indexed erc20Contract,
uint256 indexed toTokenId,
address indexed from,
uint256 amount
);

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

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

/**
* @notice Transfer ERC-20 tokens from a specific token.
* @dev The balance MUST be transferred from this smart contract.
* @dev Implementers should validate that the `msg.sender` is either the token owner or approved to manage it before calling this.
* @dev Must increase the transfer-out-nonce for the tokenId
* @param erc20Contract The address of the ERC-20 smart contract
* @param tokenId The ID of the token to transfer the ERC-20 tokens from
* @param amount The number of ERC-20 tokens to transfer
* @param data Additional data with no specified format, to allow for custom logic
*/
function transferHeldERC20FromToken(
address erc20Contract,
uint256 tokenId,
address to,
uint256 amount,
bytes memory data
) external;

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

/**
* @notice Nonce increased every time an ERC20 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 erc20TransferOutNonce(
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: [ERC-20](./eip-20.md) tokens lack a standardized transfer with callback method, such as the "safeTransfer" on [ERC-721](./eip-721.md), 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 [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md).

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 [ERC-20](./eip-20.md) and amount not being necessary for [ERC-721](./eip-721.md).

We also considered that due to the existence of safe transfer methods on both [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md), and the commonly used interfaces of `IERC721Receiver` and `IERC1155Receiver`, there is not much need to declare an additional interface to manage such tokens. However, this is not the case for [ERC-20](./eip-20.md), 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

Will be added.

## Reference Implementation

Will be added.

## Security Considerations

The same security considerations as with [ERC-721](./eip-721.md) 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.

To prevent a seller from front running the sale of an NFT holding [ERC-20](./eip-20.md) tokens to transfer out such tokens before a sale is executed, marketplaces must beware of the `erc20TransferOutNonce` and revert if it has changed since listed.

steven2308 marked this conversation as resolved.
Show resolved Hide resolved
## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
188 changes: 188 additions & 0 deletions assets/erc-7590/contracts/AbstractERC7590.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// SPDX-License-Identifier: Apache-2.0
steven2308 marked this conversation as resolved.
Show resolved Hide resolved

pragma solidity ^0.8.21;

import "./IERC7590.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

error InvalidValue();
error InvalidAddress();
error InsufficientBalance();

abstract contract AbstractERC7590 is IERC7590 {
mapping(uint256 tokenId => mapping(address erc20Address => uint256 balance))
private _balances;
mapping(uint256 tokenHolderId => uint256 nonce)
private _erc20TransferOutNonce;

/**
* @inheritdoc IERC7590
*/
function balanceOfERC20(
address erc20Contract,
uint256 tokenId
) external view returns (uint256) {
return _balances[tokenId][erc20Contract];
}

/**
* @notice Transfer ERC-20 tokens from a specific token
* @dev The balance MUST be transferred from this smart contract.
* @dev Implementers should validate that the `msg.sender` is either the token owner or approved to manage it before calling this.
* @param erc20Contract The ERC-20 contract
* @param tokenId The token to transfer from
* @param amount The number of ERC-20 tokens to transfer
* @param data Additional data with no specified format, to allow for custom logic
*/
function _transferHeldERC20FromToken(
address erc20Contract,
uint256 tokenId,
address to,
uint256 amount,
bytes memory data
) internal {
if (amount == 0) {
revert InvalidValue();
}
if (to == address(0) || erc20Contract == address(0)) {
revert InvalidAddress();
}
if (_balances[tokenId][erc20Contract] < amount) {
revert InsufficientBalance();
}
_beforeTransferHeldERC20FromToken(
erc20Contract,
tokenId,
to,
amount,
data
);
_balances[tokenId][erc20Contract] -= amount;
_erc20TransferOutNonce[tokenId]++;

IERC20(erc20Contract).transfer(to, amount);

emit TransferredERC20(erc20Contract, tokenId, to, amount);
_afterTransferHeldERC20FromToken(
erc20Contract,
tokenId,
to,
amount,
data
);
}

/**
* @inheritdoc IERC7590
*/
function transferERC20ToToken(
address erc20Contract,
uint256 tokenId,
uint256 amount,
bytes memory data
) external {
if (amount == 0) {
revert InvalidValue();
}
if (erc20Contract == address(0)) {
revert InvalidAddress();
}
_beforeTransferERC20ToToken(
erc20Contract,
tokenId,
msg.sender,
amount,
data
);
IERC20(erc20Contract).transferFrom(msg.sender, address(this), amount);
_balances[tokenId][erc20Contract] += amount;

emit ReceivedERC20(erc20Contract, tokenId, msg.sender, amount);
_afterTransferERC20ToToken(
erc20Contract,
tokenId,
msg.sender,
amount,
data
);
}

/**
* @inheritdoc IERC7590
*/
function erc20TransferOutNonce(
uint256 tokenId
) external view returns (uint256) {
return _erc20TransferOutNonce[tokenId];
}

/**
* @notice Hook that is called before any transfer of ERC-20 tokens from a token
* @param erc20Contract The ERC-20 contract
* @param tokenId The token to transfer from
* @param to The address to send the ERC-20 tokens to
* @param amount The number of ERC-20 tokens to transfer
* @param data Additional data with no specified format, to allow for custom logic
*/
function _beforeTransferHeldERC20FromToken(
address erc20Contract,
uint256 tokenId,
address to,
uint256 amount,
bytes memory data
) internal virtual {}

/**
* @notice Hook that is called after any transfer of ERC-20 tokens from a token
* @param erc20Contract The ERC-20 contract
* @param tokenId The token to transfer from
* @param to The address to send the ERC-20 tokens to
* @param amount The number of ERC-20 tokens to transfer
* @param data Additional data with no specified format, to allow for custom logic
*/
function _afterTransferHeldERC20FromToken(
address erc20Contract,
uint256 tokenId,
address to,
uint256 amount,
bytes memory data
) internal virtual {}

/**
* @notice Hook that is called before any transfer of ERC-20 tokens to a token
* @param erc20Contract The ERC-20 contract
* @param tokenId The token to transfer from
* @param from The address to send the ERC-20 tokens from
* @param amount The number of ERC-20 tokens to transfer
* @param data Additional data with no specified format, to allow for custom logic
*/
function _beforeTransferERC20ToToken(
address erc20Contract,
uint256 tokenId,
address from,
uint256 amount,
bytes memory data
) internal virtual {}

/**
* @notice Hook that is called after any transfer of ERC-20 tokens to a token
* @param erc20Contract The ERC-20 contract
* @param tokenId The token to transfer from
* @param from The address to send the ERC-20 tokens from
* @param amount The number of ERC-20 tokens to transfer
* @param data Additional data with no specified format, to allow for custom logic
*/
function _afterTransferERC20ToToken(
address erc20Contract,
uint256 tokenId,
address from,
uint256 amount,
bytes memory data
) internal virtual {}

function supportsInterface(
bytes4 interfaceId
) public view virtual override returns (bool) {
return type(IERC7590).interfaceId == interfaceId;
}
}
12 changes: 12 additions & 0 deletions assets/erc-7590/contracts/ERC20Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20Mock is ERC20 {
constructor() ERC20("Test Token", "TEST") {}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
Loading
Loading