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

Extending ERC1155 with approvals by amount #5216

Merged
merged 8 commits into from
Sep 1, 2022
112 changes: 112 additions & 0 deletions EIPS/eip-5216.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
eip: 5216
title: EIP-1155 Approval By Amount Extension
description: Extension for EIP-1155 secure approvals
author: Iván Mañús (@ivanmmurciaua), Juan Carlos Cantó (@EscuelaCryptoES)
discussions-to: https://ethereum-magicians.org/t/eip-erc1155-approval-by-amount/9898
status: Draft
type: Standards Track
category: ERC
created: 2022-07-11
requires: 20, 165, 1155
---

## Abstract

This specification defines standard functions for approving amounts of an [EIP-1155](./eip-1155.md). An implementation allows approve by id, different amounts both batch and individual. The proposal depends on and extends the existing [EIP-1155](./eip-1155.md).

## Motivation

[EIP-1155](./eip-1155.md) has created a scenario where multiple token management and transactions occur on a daily basis. Although it can be used as [EIP-721](./eip-721.md), the most common way to use it is to create collections through its `ids` in order to have an indefinite `amount` of tokens of one type. These tokens have been implemented in a wide variety of projects, but the most important are marketplaces.
The nature of tokens is trading, transfer and use, so it is important that your peer-to-peer transactions are as secure as possible.

The way the [EIP-1155](./eip-1155.md) standard approves tokens is with the `setApprovalForAll(address operator, bool approved)` function, which approves ALL tokens of an `id`. However, this is a standard that combines ideas from the [EIP-20](./eip-20.md) and [EIP-721](./eip-721.md) standards, and it is important to create a trust mechanism, where an owner can allow a third party, such as a marketplace, to approve a limited number of tokens of an `id` and not all at once.

In turn, it is also necessary to subtract the amount of tokens approved once they have been successfully transferred to avoid the scenario that we have now, where ALL tokens of an `id` are approved without revoking that approval at any time. In our honest opinion, this is quite dangerous because if you don't revoke that approval with `setApprovalForAll(operatorAddress, false)`, you let that the operator do whateaver he wants whenever he wants with all your tokens.

By having a way to approve and revoke as the [EIP-20](./eip-20.md) standard works, i.e. by amounts we reduce that security problem:

- By `approve` function, we allow an operator to use an amount of tokens per id.
- Through the function `allowance` you can see the amount of tokens that an approved account has per id.

Both following the [EIP-20](./eip-20.md) standard name patterns.


## Specification

The keywords “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.

Every contract compliant to the `ERC1155ApprovalByAmount` extension MUST implement the `IERC1155ApprovalByAmount` interface. The **approval by amount extension** is OPTIONAL for [EIP-1155](./eip-1155.md) contracts.

### Interface implementation

```solidity
/**
* @title ERC-1155 Approval By Amount Extension
* Note: the ERC-165 identifier for this interface is 0x1be07d74
ivanmmurciaua marked this conversation as resolved.
Show resolved Hide resolved
*/
interface IERC1155ApprovalByAmount is IERC1155 {

/**
* @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
* `id` and with an amount: `amount`.
*/
event ApprovalByAmount(address indexed account, address indexed operator, uint256 id, uint256 amount);

/**
* @notice Grants permission to `operator` to transfer the caller's tokens, according to `id`, and an amount: `amount`.
* Emits an {ApprovalByAmount} event.
*
* Requirements:
* - `operator` cannot be the caller.
*/
function approve(address operator, uint256 id, uint256 amount) external;

/**
* @notice Returns the amount allocated to `operator` approved to transfer ``account``'s tokens, according to `id`.
*/
function allowance(address account, address operator, uint256 id) external view returns (uint256);

}
```

The `_allowances` mapping MAY be implemented as `internal` or `private`.

The `approve(address operator, uint256 id, uint256 amount)` function MAY be implemented as `public` or `external` and it is RECOMMENDED implements `private` `_approve(address owner, address operator, uint256 id, uint256 amount)` function, which is responsible for performing the approval.

The `allowance(address account, address operator, uint256 id)` function MAY be implemented as `public` or `external` for visibility and MUST be implemented as `view`.

Every contract implementing the `ERC1155ApprovalByAmount` extension MUST implement `_checkApprovalForBatch(address from, address to, uint256[] memory ids, uint256[] memory amounts)` function in some way. This function is necessary to manage
batch transfers subtracting the amount of tokens transferred.

In same way, `safe(Batch)TrasferFrom` overrode function from `ERC1155` standard it is also important to change its implementation:

- In `safeTransferFrom` MUST be added to require an extra condition: `|| allowance(from, _msgSender(), id)`.
- In `safeTransferFrom` MUST be subtracted from `_allowances` mapping the transferred amount of tokens: `_allowances[from][_msgSender()][id] -= amount`.
- In `safeBatchTransferFrom` MUST be added to require an extra condition that checks if the `allowance` of all `ids` have the approved `amounts` (See `_checkApprovalForBatch` function reference implementation)

The `ApprovalByAmount` event MUST be emitted when a certain number of tokens are approved.

The `supportsInterface` method MUST return `true` when called with `0x1be07d74`.

## Rationale

The chosen name resonates with the purpose of its existence. Users can approve their tokens by id and amounts to `operators`.

The [EIP-20](./eip-20.md) standard name patterns, were used to simplify the understanding of how the interface works due to similarity with how [EIP-20](./eip-20.md) works with approvals.

## Backwards Compatibility

This standard is compatible with [EIP-1155](./eip-1155.md).

## Reference Implementation

The reference implementation can be found [here](../assets/eip-5216/ERC1155ApprovalByAmount.sol).

## Security Considerations

Implementors of the `ERC1155ApprovalByAmount` standard must consider thoroughly the amount of tokens they give permission to `operators`. They should also revoke the rest.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
146 changes: 146 additions & 0 deletions assets/eip-5216/ERC1155ApprovalByAmount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.15;

import "IERC1155.sol";
Pandapip1 marked this conversation as resolved.
Show resolved Hide resolved
import "ERC1155.sol";

/**
* @title ERC-1155 Approval By Amount Extension
* Note: the ERC-165 identifier for this interface is 0x1be07d74
*/
interface IERC1155ApprovalByAmount is IERC1155 {

/**
* @notice Emitted when `account` grants or revokes permission to `operator` to transfer their tokens, according to
* `id` and with an amount: `amount`.
*/
event ApprovalByAmount(address indexed account, address indexed operator, uint256 id, uint256 amount);

/**
* @notice Grants permission to `operator` to transfer the caller's tokens, according to `id`, and an amount: `amount`.
* Emits an {ApprovalByAmount} event.
*
* Requirements:
* - `operator` cannot be the caller.
*/
function approve(address operator, uint256 id, uint256 amount) external;

/**
* @notice Returns the amount allocated to `operator` approved to transfer ``account``'s tokens, according to `id`.
*/
function allowance(address account, address operator, uint256 id) external view returns (uint256);

}

/**
* @dev Extension of {ERC1155} that allows you to approve your tokens by amount and id.
*/
abstract contract ERC1155ApprovalByAmount is ERC1155, IERC1155ApprovalByAmount {

// Mapping from account to operator approvals by id and amount.
mapping(address => mapping(address => mapping(uint256 => uint256))) internal _allowances;

/**
* @dev See {IERC1155ApprovalByAmount}
*/
function approve(address operator, uint256 id, uint256 amount) public virtual {
_approve(_msgSender(), operator, id, amount);
}

/**
* @dev See {IERC1155ApprovalByAmount}
*/
function allowance(address account, address operator, uint256 id) public view virtual returns (uint256) {
return _allowances[account][operator][id];
}

/**
* @dev safeTransferFrom implementation for using ApprovalByAmount extension
*/
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) public override(IERC1155, ERC1155) {
require(
from == _msgSender() || isApprovedForAll(from, _msgSender()) || allowance(from, _msgSender(), id) >= amount,
"ERC1155: caller is not owner nor approved nor approved for amount"
);
unchecked {
_allowances[from][_msgSender()][id] -= amount;
}
_safeTransferFrom(from, to, id, amount, data);
}

/**
* @dev safeBatchTransferFrom implementation for using ApprovalByAmount extension
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public virtual override(IERC1155, ERC1155) {
require(
from == _msgSender() || isApprovedForAll(from, _msgSender()) || _checkApprovalForBatch(from, _msgSender(), ids, amounts),
"ERC1155: transfer caller is not owner nor approved nor approved for some amount"
);
_safeBatchTransferFrom(from, to, ids, amounts, data);
}

/**
* @dev Checks if all ids and amounts are permissioned for `to`.
*
* Requirements:
* - `ids` and `amounts` length should be equal.
*/
function _checkApprovalForBatch(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts
) internal virtual returns (bool) {
uint256 idsLength = ids.length;
uint256 amountsLength = amounts.length;

require(idsLength == amountsLength, "ERC1155ApprovalByAmount: ids and amounts length mismatch");
for (uint256 i = 0; i < idsLength;) {
require(allowance(from, to, ids[i]) >= amounts[i], "ERC1155ApprovalByAmount: operator is not approved for that id or amount");
unchecked {
_allowances[from][to][ids[i]] -= amounts[i];
++i;
}
}
return true;
}

/**
* @dev Approve `operator` to operate on all of `owner` tokens by id and amount.
* Emits a {ApprovalByAmount} event.
*/
function _approve(
address owner,
address operator,
uint256 id,
uint256 amount
) internal virtual {
require(owner != operator, "ERC1155ApprovalByAmount: setting approval status for self");
_allowances[owner][operator][id] = amount;
emit ApprovalByAmount(owner, operator, id, amount);
}
}

contract ExampleToken is ERC1155ApprovalByAmount {
constructor() ERC1155("") {}

function mint(address account, uint256 id, uint256 amount, bytes memory data) public {
_mint(account, id, amount, data);
}

function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) public {
_mintBatch(to, ids, amounts, data);
}
}