Before submitting an EIP I’d like to get some feedback on an idea, please.
Summary: ERC721 “approval” for a specific token is currently limited to a single operator via the approve()
function. Holders wishing to list assets for sale on multiple marketplaces must therefore use setApprovalForAll()
, which goes against the principle of least privilege. I would like to propose a per-token, multiple-operator approval mechanism as an extension to ERC721.
Motivation: a number of phishing scams trick users into signing zero-ETH ask-side orders for fulfilment via marketplace contracts. A user wishing to legitimately sell only a single NFT therefore risks their entire portfolio of the respective contract. This is what happened to Kevin Rose, resulting in the theft of 25 Chromie Squiggles when he had legitimately intended to sell only one. I was on a call with him when it happened so am very familiar with the technical aspects of what happened.
Additional requirements: a solution must require minimal effort for adoption by marketplaces. Introduction of a per-token alternative to isApprovedForAll()
=> setApprovalForAll()
would follow their existing technical flows as outlined after the interface definition in the below proposal.
The Head of Protocol at OpenSea has lent support.
Alternatives considered: the majority of negative feedback I’ve received (to the above-linked tweet) has been that approve()
fulfils this requirement. It does not, however, allow for approval of more than one operator. NFT traders would therefore be limited to selling via a single marketplace.
Proposal:
EDIT: add ApprovalFor
event.
An interface along the lines of:
interface IERC721PerTokenApproval {
/**
* @notice Emitted when an operator is enabled or disabled for a token.
*/
event ApprovalFor(
address indexed _operator,
uint256 indexed _tokenId,
bool _approved
);
/**
* @notice Approves the operator to manage the asset on behalf of its owner.
* @dev Throws if msg.sender is not the current NFT owner.
* @dev Approvals set via this method MUST be cleared upon transfer of the
* token to a new owner.
*/
function setApprovalFor(
address _operator,
uint256 _tokenId,
bool _approved
) external;
/**
* @notice Returns true if any of the following criteria are met:
* 1. _operator was approved via setApprovalFor() on `_tokenId`
* and the token has not since been transferred; OR
* 2. isApprovedForAll(ownerOf(_tokenId), _operator) == true; OR
* 3. getApproved(_tokenId) == _operator.
*/
function isApprovedFor(address _operator, uint256 _tokenId)
external
view
returns (bool);
// ERC-165
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
The requirement to clean up approvals on transfer can be done efficiently with an incrementing nonce, as described here.
Marketplaces wishing to check approvals prior to listing assets would follow this pattern:
if (nft.supportInterface(type(IERC721PerTokenApproval).interfaceId)) {
if (!nft.isApprovedFor(marketplace, tokenId)) {
// initiate user to setApprovalFor(marketplace, tokenId, true)
}
} else {
// fallback to existing setApprovalForAll()
if (!nft.isApprovedForAll(nft.ownerOf(tokenId), marketplace)) {
// initiate user to setApprovalForAll(marketplace, true)
}
}
For further consideration: should setApprovalFor(…,false)
override setApprovalForAll()
? This complicates isApprovedFor()
, which suggests that the 3 positive criteria may be too broad, or that there could be an isExplicitlyApprovedFor()
that only returns true if criterion (1) is met.