From a5299b00e8f79adb2be5e02d6dffb41452908818 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 3 Mar 2023 18:56:20 +0000 Subject: [PATCH] Add EIP-6464: Multi-operator, per-token ERC721 approval (#6464) * EIP Motivation and Specification for "Multi-operator, per-token ERC721 approval." * Add remaining authors' names * Interface overhaul with explanation in EIP markdown. * Add `Abstract` and `@dev` note about interplay with `setApprovalForAll()`. * Partial response to cxkoda review * Full response to cxkoda review * Explicitly include OZ `IERC{165,721}.sol` instead of using a submodule * Minor typo and wording changes to draft. * Prune authors to active participants as they can always be re-added later * Revert `.gitignore` so as to not change it in the EIP draft (#7) * Set presumptive EIP number and fix linter complaints (#5) * Rename files and directories to include EIP number * Address linter (`EIP Walidator`) issues * Add presumed EIP number based on PR: 6464 * Revert `.gitignore` to get through initial draft acceptance * Do the last commit properly ;) * Add ERC category * Fix linter issues (#8) * Update EIP- references to ERC- * Remove OpenZeppelin interfaces and replace with original EIP versions; change 6464 license to CC0 * Add `solidity` language tag to code block --- EIPS/eip-6464.md | 198 +++++++++++++++++++++++++ assets/eip-6464/contracts/IERC165.sol | 12 ++ assets/eip-6464/contracts/IERC6464.sol | 101 +++++++++++++ assets/eip-6464/contracts/IERC721.sol | 102 +++++++++++++ 4 files changed, 413 insertions(+) create mode 100644 EIPS/eip-6464.md create mode 100644 assets/eip-6464/contracts/IERC165.sol create mode 100644 assets/eip-6464/contracts/IERC6464.sol create mode 100644 assets/eip-6464/contracts/IERC721.sol diff --git a/EIPS/eip-6464.md b/EIPS/eip-6464.md new file mode 100644 index 0000000000000..0b75fecc27a63 --- /dev/null +++ b/EIPS/eip-6464.md @@ -0,0 +1,198 @@ +--- +eip: 6464 +title: Multi-operator, per-token ERC-721 approvals. +description: Extends ERC-721 to allow token owners to approve multiple operators to control their assets on a per-token basis. +author: Cristian Espinoza (@crisgarner), Simon Fremaux (@dievardump), David Huber (@cxkoda), and Arran Schlosberg (@aschlosberg) +discussions-to: https://ethereum-magicians.org/t/fine-grained-erc721-approval-for-multiple-operators/12796 +status: Draft +type: Standards Track +category: ERC +created: 2023-02-02 +requires: 165, 721 +--- + +## Abstract + +[ERC-721](./eip-721.md) did not foresee the approval of multiple operators to manage a specific token on behalf of its owner. This lead to the establishment of `setApprovalForAll()` as the predominant way to authorise operators, which affords the approved address control over all assets and creates an unnecessarily broad security risk that has already been exploited in a multitude of phishing attacks. The presented EIP extends ERC-721 by introducing a fine-grained, on-chain approval mechanism that allows owners to authorise multiple, specific operators on a per-token basis; this removes unnecessary access permissions and shrinks the surface for exploits to a minimum. The provided reference implementation further enables cheap revocation of all approvals on a per-owner or per-token basis. + +## Motivation + +The NFT standard defined in ERC-721 allows token owners to "approve" arbitrary addresses to control their tokens—the approved addresses are known as "operators". Two types of approval were defined: + +1. `approve(address,uint256)` provides a mechanism for only a single operator to be approved for a given `tokenId`; and +2. `setApprovalForAll(address,bool)` toggles whether an operator is approved for *every* token owned by `msg.sender`. + +With the introduction of multiple NFT marketplaces, the ability to approve multiple operators for a particular token is necessary if sellers wish to allow each marketplace to transfer a token upon sale. There is, however, no mechanism for achieving this without using `setApprovalForAll()`. This is in conflict with the principle of least privilege and creates an attack vector that is exploited by phishing for malicious (i.e. zero-cost) sell-side signatures that are executed by legitimate marketplace contracts. + +This EIP therefore defines a fine-grained approach for approving multiple operators but scoped to specific token(s). + +### Goals + +1. Ease of adoption for marketplaces; requires minimal changes to existing workflows. +2. Ease of adoption for off-chain approval-indexing services. +3. Simple revocation of approvals; i.e. not requiring one per grant. + +### Non-goals + +1. Security measures for protecting NFTs other than through limiting the scope of operator approvals. +2. Compatibility with [ERC-1155](./eip-1155.md) semi-fungible tokens. However we note that the mechanisms described herein are also applicable to ERC-1155 token *types* without requiring approval for all other types. + +## 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. + +To comply with this EIP, a contract MUST implement `IERC6464` (defined herein) and the `ERC165` and `ERC721` interfaces; see [ERC-165](./eip-165.md) and ERC-721 respectively. + +```solidity +/** + * @notice Extends ERC-721 to include per-token approval for multiple operators. + * @dev Off-chain indexers of approvals SHOULD assume that an operator is approved if either of `ERC721.Approval(…)` or + * `ERC721.ApprovalForAll(…, true)` events are witnessed without the corresponding revocation(s), even if an + * `ExplicitApprovalFor(…, false)` is emitted. + * @dev TODO: the ERC-165 identifier for this interface is TBD. + */ +interface IERC6464 is ERC721 { + /** + * @notice Emitted when approval is explicitly granted or revoked for a token. + */ + event ExplicitApprovalFor( + address indexed operator, + uint256 indexed tokenId, + bool approved + ); + + /** + * @notice Emitted when all explicit approvals, as granted by either `setExplicitApprovalFor()` function, are + * revoked for all tokens. + * @dev MUST be emitted upon calls to `revokeAllExplicitApprovals()`. + */ + event AllExplicitApprovalsRevoked(address indexed owner); + + /** + * @notice Emitted when all explicit approvals, as granted by either `setExplicitApprovalFor()` function, are + * revoked for the specific token. + * @param owner MUST be `ownerOf(tokenId)` as per ERC721; in the case of revocation due to transfer, this MUST be + * the `from` address expected to be emitted in the respective `ERC721.Transfer()` event. + */ + event AllExplicitApprovalsRevoked( + address indexed owner, + uint256 indexed tokenId + ); + + /** + * @notice Approves the operator to manage the asset on behalf of its owner. + * @dev Throws if `msg.sender` is not the current NFT owner, or an authorised operator of the current owner. + * @dev Approvals set via this method MUST be revoked upon transfer of the token to a new owner; equivalent to + * calling `revokeAllExplicitApprovals(tokenId)`, including associated events. + * @dev MUST emit `ApprovalFor(operator, tokenId, approved)`. + * @dev MUST NOT have an effect on any standard ERC721 approval setters / getters. + */ + function setExplicitApproval( + address operator, + uint256 tokenId, + bool approved + ) external; + + /** + * @notice Approves the operator to manage the token(s) on behalf of their owner. + * @dev MUST be equivalent to calling `setExplicitApprovalFor(operator, tokenId, approved)` for each `tokenId` in + * the array. + */ + function setExplicitApproval( + address operator, + uint256[] memory tokenIds, + bool approved + ) external; + + /** + * @notice Revokes all explicit approvals granted by `msg.sender`. + * @dev MUST emit `AllExplicitApprovalsRevoked(msg.sender)`. + */ + function revokeAllExplicitApprovals() external; + + /** + * @notice Revokes all excplicit approvals granted for the specified token. + * @dev Throws if `msg.sender` is not the current NFT owner, or an authorised operator of the current owner. + * @dev MUST emit `AllExplicitApprovalsRevoked(msg.sender, tokenId)`. + */ + function revokeAllExplicitApprovals(uint256 tokenId) external; + + /** + * @notice Query whether an address is an approved operator for a token. + */ + function isExplicitlyApprovedFor(address operator, uint256 tokenId) + external + view + returns (bool); +} + +interface IERC6464AnyApproval is ERC721 { + /** + * @notice Returns true if any of the following criteria are met: + * 1. `isExplicitlyApprovedFor(operator, tokenId) == true`; OR + * 2. `isApprovedForAll(ownerOf(tokenId), operator) == true`; OR + * 3. `getApproved(tokenId) == operator`. + * @dev The criteria MUST be extended if other mechanism(s) for approving operators are introduced. The criteria + * MUST include all approval approaches. + */ + function isApprovedFor(address operator, uint256 tokenId) + external + view + returns (bool); +} +``` + +## Rationale + +### Draft notes to be expanded upon + +1. Approvals granted via the newly introduced methods are called *explicit* as a means of easily distinguishing them from those granted via the standard `ERC721.approve()` and `ERC721.setApprovalForAll()` functions. However they follow the same intent: authorising operators to act on the owner's behalf. +2. Abstracting `isApprovedFor()` into `IERC6464AnyApproval` interface, as against keeping it in `IERC6464` allows for modularity of plain `IERC6464` implementations while also standardising the interface for checking approvals when interfacing with specific implementations and any future approval EIPs. +3. Inclusion of an indexed owner address in `AllExplicitApprovalsRevoked(address,uint256)` assists off-chain indexing of existing approvals. +4. Re `IERC6464AnyApproval`: With an increasing number of approval mechanisms it becomes cumbersome for marketplaces to integrate with them since they have to query multiple interfaces to check if they are approved to manage tokens. This provides a streamlined interface, intended to simplify data ingestion for them. + + + +## Backwards Compatibility + +This extension was written to allow for the smallest change possible to the original ERC-721 spec while still providing a mechanism to grant, revoke and track approvals of multiple operators on a per-token basis. + +Extended contracts remain fully compatible with all existing platforms. + +**Note** the `Security Considerations` sub-section on `Other risks` regarding interplay of approval types. + +## Reference Implementation + +TODO: add internal link to assets directory when the implementation is in place. + +An efficient mechanism for broad revocation of approvals via incrementing nonces is included. + +## Security Considerations + +### Threat model + +### Mitigations + +### Other risks + +TODO: Interplay with `setApprovalForAll()`. + + + +Needs discussion. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-6464/contracts/IERC165.sol b/assets/eip-6464/contracts/IERC165.sol new file mode 100644 index 0000000000000..9f87593359b93 --- /dev/null +++ b/assets/eip-6464/contracts/IERC165.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +interface ERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceID The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} diff --git a/assets/eip-6464/contracts/IERC6464.sol b/assets/eip-6464/contracts/IERC6464.sol new file mode 100644 index 0000000000000..da0a125001133 --- /dev/null +++ b/assets/eip-6464/contracts/IERC6464.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC721.sol"; + +/** + * @notice Extends ERC-721 to include per-token approval for multiple operators. + * @dev Off-chain indexers of approvals SHOULD assume that an operator is approved if either of `ERC721.Approval(…)` or + * `ERC721.ApprovalForAll(…, true)` events are witnessed without the corresponding revocation(s), even if an + * `ExplicitApprovalFor(…, false)` is emitted. + * @dev TODO: the ERC-165 identifier for this interface is TBD. + */ +interface IERC6464 is ERC721 { + /** + * @notice Emitted when approval is explicitly granted or revoked for a token. + */ + event ExplicitApprovalFor( + address indexed operator, + uint256 indexed tokenId, + bool approved + ); + + /** + * @notice Emitted when all explicit approvals, as granted by either `setExplicitApprovalFor()` function, are + * revoked for all tokens. + * @dev MUST be emitted upon calls to `revokeAllExplicitApprovals()`. + */ + event AllExplicitApprovalsRevoked(address indexed owner); + + /** + * @notice Emitted when all explicit approvals, as granted by either `setExplicitApprovalFor()` function, are + * revoked for the specific token. + * @param owner MUST be `ownerOf(tokenId)` as per ERC721; in the case of revocation due to transfer, this MUST be + * the `from` address expected to be emitted in the respective `ERC721.Transfer()` event. + */ + event AllExplicitApprovalsRevoked( + address indexed owner, + uint256 indexed tokenId + ); + + /** + * @notice Approves the operator to manage the asset on behalf of its owner. + * @dev Throws if `msg.sender` is not the current NFT owner, or an authorised operator of the current owner. + * @dev Approvals set via this method MUST be revoked upon transfer of the token to a new owner; equivalent to + * calling `revokeAllExplicitApprovals(tokenId)`, including associated events. + * @dev MUST emit `ApprovalFor(operator, tokenId, approved)`. + * @dev MUST NOT have an effect on any standard ERC721 approval setters / getters. + */ + function setExplicitApproval( + address operator, + uint256 tokenId, + bool approved + ) external; + + /** + * @notice Approves the operator to manage the token(s) on behalf of their owner. + * @dev MUST be equivalent to calling `setExplicitApprovalFor(operator, tokenId, approved)` for each `tokenId` in + * the array. + */ + function setExplicitApproval( + address operator, + uint256[] memory tokenIds, + bool approved + ) external; + + /** + * @notice Revokes all explicit approvals granted by `msg.sender`. + * @dev MUST emit `AllExplicitApprovalsRevoked(msg.sender)`. + */ + function revokeAllExplicitApprovals() external; + + /** + * @notice Revokes all excplicit approvals granted for the specified token. + * @dev Throws if `msg.sender` is not the current NFT owner, or an authorised operator of the current owner. + * @dev MUST emit `AllExplicitApprovalsRevoked(msg.sender, tokenId)`. + */ + function revokeAllExplicitApprovals(uint256 tokenId) external; + + /** + * @notice Query whether an address is an approved operator for a token. + */ + function isExplicitlyApprovedFor(address operator, uint256 tokenId) + external + view + returns (bool); +} + +interface IERC6464AnyApproval is ERC721 { + /** + * @notice Returns true if any of the following criteria are met: + * 1. `isExplicitlyApprovedFor(operator, tokenId) == true`; OR + * 2. `isApprovedForAll(ownerOf(tokenId), operator) == true`; OR + * 3. `getApproved(tokenId) == operator`. + * @dev The criteria MUST be extended if other mechanism(s) for approving operators are introduced. The criteria + * MUST include all approval approaches. + */ + function isApprovedFor(address operator, uint256 tokenId) + external + view + returns (bool); +} diff --git a/assets/eip-6464/contracts/IERC721.sol b/assets/eip-6464/contracts/IERC721.sol new file mode 100644 index 0000000000000..cca04190cabfa --- /dev/null +++ b/assets/eip-6464/contracts/IERC721.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC165.sol"; + +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// Note: the ERC-165 identifier for this interface is 0x80ac58cd. +interface ERC721 is ERC165 { + /// @dev This emits when ownership of any NFT changes by any mechanism. + /// This event emits when NFTs are created (`from` == 0) and destroyed + /// (`to` == 0). Exception: during contract creation, any number of NFTs + /// may be created and assigned without emitting Transfer. At the time of + /// any transfer, the approved address for that NFT (if any) is reset to none. + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + /// @dev This emits when the approved address for an NFT is changed or + /// reaffirmed. The zero address indicates there is no approved address. + /// When a Transfer event emits, this also indicates that the approved + /// address for that NFT (if any) is reset to none. + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + + /// @dev This emits when an operator is enabled or disabled for an owner. + /// The operator can manage all NFTs of the owner. + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param _owner An address for whom to query the balance + /// @return The number of NFTs owned by `_owner`, possibly zero + function balanceOf(address _owner) external view returns (uint256); + + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param _tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + function ownerOf(uint256 _tokenId) external view returns (address); + + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. When transfer is complete, this function + /// checks if `_to` is a smart contract (code size > 0). If so, it calls + /// `onERC721Received` on `_to` and throws if the return value is not + /// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + /// @param data Additional data with no specified format, sent in call to `_to` + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) external payable; + + /// @notice Transfers the ownership of an NFT from one address to another address + /// @dev This works identically to the other function with an extra data parameter, + /// except this function just sets data to "". + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; + + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner, an authorized + /// operator, or the approved address for this NFT. Throws if `_from` is + /// not the current owner. Throws if `_to` is the zero address. Throws if + /// `_tokenId` is not a valid NFT. + /// @param _from The current owner of the NFT + /// @param _to The new owner + /// @param _tokenId The NFT to transfer + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; + + /// @notice Change or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param _approved The new approved NFT controller + /// @param _tokenId The NFT to approve + function approve(address _approved, uint256 _tokenId) external payable; + + /// @notice Enable or disable approval for a third party ("operator") to manage + /// all of `msg.sender`'s assets + /// @dev Emits the ApprovalForAll event. The contract MUST allow + /// multiple operators per owner. + /// @param _operator Address to add to the set of authorized operators + /// @param _approved True if the operator is approved, false to revoke approval + function setApprovalForAll(address _operator, bool _approved) external; + + /// @notice Get the approved address for a single NFT + /// @dev Throws if `_tokenId` is not a valid NFT. + /// @param _tokenId The NFT to find the approved address for + /// @return The approved address for this NFT, or the zero address if there is none + function getApproved(uint256 _tokenId) external view returns (address); + + /// @notice Query if an address is an authorized operator for another address + /// @param _owner The address that owns the NFTs + /// @param _operator The address that acts on behalf of the owner + /// @return True if `_operator` is an approved operator for `_owner`, false otherwise + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} \ No newline at end of file