This standard is an extension of ERC-721. It defines new validation functionality to avoid wallet draining: every transfer or approve will be locked waiting for validation.
Motivation
The power of the blockchain is at the same time its weakness: giving the user full responsibility for their data.
Many cases of NFT theft currently exist, and current NFT anti-theft schemes, such as transferring NFTs to cold wallets, make NFTs inconvenient to use.
Having a validation step before every transfer and approve would give Smart Contract developers the opportunity to create secure NFT anti-theft schemes.
An implementation example would be a system where a validator address is responsible for validating all Smart Contract transactions.
This address would be connected to a dApp where the user could see the validation requests of his NFTs and accept the correct ones.
Giving this address only the power to validate transactions would make a much more secure system where to steal an NFT the thief would have to have both the user’s address and the validator address simultaneously.
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.
ERC-721 compliant contracts MAY implement this EIP.
All the operations that change the ownership of an NFT, like a transferFrom/safeTransferFrom, SHALL create a TransferValidation pending to be validated and emit a ValidateTransfer, and SHALL NOT transfer the ownership of an NFT.
All the operations that enable an approval to manage an NFT, like an approve/setApprovalForAll, SHALL create an ApprovalValidation pending to be validated and emit a ValidateApproval, and SHALL NOT enable an approval.
When the transfer is called by an approved account and not the owner, it MUST be executed directly without the need for validation. This is in order to adapt to all current marketplaces that require approve to directly move your NFTs.
When validating a TransferValidation or ApprovalValidation the valid field MUST be set to true and MUST NOT be validated again.
The operations that validate a TransferValidation SHALL change the ownership of the NFT or enable the approval.
The operations that validate an ApprovalValidation SHALL enable the approval.
Contract Interface
interfaceIERC6997{structTransferValidation{// The address of the owner.
addressfrom;// The address of the receiver.
addressto;// The token Id.
uint256tokenId;// Whether is a valid transfer.
boolvalid;}structApprovalValidation{// The address of the owner.
addressowner;// The approved address.
addressapprove;// The token Id.
uint256tokenId;// Wether is a total approvement.
boolapproveAll;// Whether is a valid approve.
boolvalid;}/**
* @dev Emitted when a new transfer validation has been requested.
*/eventValidateTransfer(addressindexedfrom,addressto,uint256indexedtokenId,uint256indexedtransferValidationId);/**
* @dev Emitted when a new approval validation has been requested.
*/eventValidateApproval(addressindexedowner,addressapprove,uint256tokenId,boolindexedapproveAll,uint256indexedapprovalValidationId);/**
* @dev Returns true if this contract is a validator ERC721.
*/functionisValidatorContract()externalviewreturns(bool);/**
* @dev Returns the transfer validation struct using the transfer ID.
*
*/functiontransferValidation(uint256transferId)externalviewreturns(TransferValidationmemory);/**
* @dev Returns the approval validation struct using the approval ID.
*
*/functionapprovalValidation(uint256approvalId)externalviewreturns(ApprovalValidationmemory);/**
* @dev Return the total amount of transfer validations created.
*
*/functiontotalTransferValidations()externalviewreturns(uint256);/**
* @dev Return the total amount of transfer validations created.
*
*/functiontotalApprovalValidations()externalviewreturns(uint256);}
The isValidatorContract() function MUST be implemented as public.
The transferValidation(uint256 transferId) function MAY be implemented as public or external.
The approvalValidation(uint256 approveId) function MAY be implemented as public or external.
The totalTransferValidations() function MAY be implemented as pure or view.
The totalApprovalValidations() function MAY be implemented as pure or view.
Rationale
Universality
The standard only defines the validation functions, but not how they should be used. It defines the validations as internal and lets the user decide how to manage them.
An example could be to have an address validator connected to a dApp so that users could manage their validations.
This validator could be used for all NFTs or only for some users.
It could also be used as a wrapped Smart Contract for existing ERC-721, allowing 1/1 conversion with existing NFTs.
Extensibility
This standard only defines the validation function, but does not define the system with which it has to be validated. A third-party protocol can define how it wants to call these functions as it wishes.
Backwards Compatibility
This standard is an extension of ERC-721, compatible with all the operations except transferFrom/safeTransferFrom/approve/setApprovalForAll.
This operations will be overridden to create a validation petition instead of transfer ownership of an NFT or enable an approval.
Reference Implementation
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.0;import"./IERC6997.sol";import"@openzeppelin/contracts/token/ERC721/ERC721.sol";/**
* @dev Implementation of ERC6997
*/contractERC6997isIERC6997,ERC721{// Mapping from transfer ID to transfer validation
mapping(uint256=>TransferValidation)private_transferValidations;// Mapping from approval ID to approval validation
mapping(uint256=>ApprovalValidation)private_approvalValidations;// Total number of transfer validations
uint256private_totalTransferValidations;// Total number of approval validations
uint256private_totalApprovalValidations;/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
*/constructor(stringmemoryname_,stringmemorysymbol_)ERC721(name_,symbol_){}/**
* @dev Returns true if this contract is a validator ERC721.
*/functionisValidatorContract()publicpurereturns(bool){returntrue;}/**
* @dev Returns the transfer validation struct using the transfer ID.
*
*/functiontransferValidation(uint256transferId)publicviewoverridereturns(TransferValidationmemory){require(transferId<_totalTransferValidations,"ERC6997: invalid transfer ID");TransferValidationmemoryv=_transferValidation(transferId);returnv;}/**
* @dev Returns the approval validation struct using the approval ID.
*
*/functionapprovalValidation(uint256approvalId)publicviewoverridereturns(ApprovalValidationmemory){require(approvalId<_totalApprovalValidations,"ERC6997: invalid approval ID");ApprovalValidationmemoryv=_approvalValidation(approvalId);returnv;}/**
* @dev Return the total amount of transfer validations created.
*
*/functiontotalTransferValidations()publicviewoverridereturns(uint256){return_totalTransferValidations;}/**
* @dev Return the total amount of approval validations created.
*
*/functiontotalApprovalValidations()publicviewoverridereturns(uint256){return_totalApprovalValidations;}/**
* @dev Returns the transfer validation of the `transferId`. Does NOT revert if transfer doesn't exist
*/function_transferValidation(uint256transferId)internalviewvirtualreturns(TransferValidationmemory){return_transferValidations[transferId];}/**
* @dev Returns the approval validation of the `approvalId`. Does NOT revert if transfer doesn't exist
*/function_approvalValidation(uint256approvalId)internalviewvirtualreturns(ApprovalValidationmemory){return_approvalValidations[approvalId];}/**
* @dev Validate the transfer using the transfer ID.
*
*/function_validateTransfer(uint256transferId)internalvirtual{TransferValidationmemoryv=transferValidation(transferId);require(!v.valid,"ERC6997: the transfer is already validated");addressfrom=v.from;addressto=v.to;uint256tokenId=v.tokenId;super._transfer(from,to,tokenId);_transferValidations[transferId].valid=true;}/**
* @dev Validate the approval using the approval ID.
*
*/function_validateApproval(uint256approvalId)internalvirtual{ApprovalValidationmemoryv=approvalValidation(approvalId);require(!v.valid,"ERC6997: the approval is already validated");if(!v.approveAll){require(v.owner==ownerOf(v.tokenId),"ERC6997: The token have a new owner");super._approve(v.approve,v.tokenId);}else{super._setApprovalForAll(v.owner,v.approve,true);}_approvalValidations[approvalId].valid=true;}/**
* @dev Create a transfer petition of `tokenId` from `from` to `to`.
*
* Requirements:
*
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
*
* Emits a {TransferValidate} event.
*/function_transfer(addressfrom,addressto,uint256tokenId)internalvirtualoverride{require(ERC721.ownerOf(tokenId)==from,"ERC6997: transfer from incorrect owner");require(to!=address(0),"ERC6997: transfer to the zero address");if(_msgSender()==from){TransferValidationmemoryv;v.from=from;v.to=to;v.tokenId=tokenId;_transferValidations[_totalTransferValidations]=v;emitValidateTransfer(from,to,tokenId,_totalTransferValidations);_totalTransferValidations++;}else{super._transfer(from,to,tokenId);}}/**
* @dev Create an approval petition from `to` to operate on `tokenId`
*
* Emits an {ValidateApproval} event.
*/function_approve(addressto,uint256tokenId)internaloverridevirtual{ApprovalValidationmemoryv;v.owner=ownerOf(tokenId);v.approve=to;v.tokenId=tokenId;_approvalValidations[_totalApprovalValidations]=v;emitValidateApproval(v.owner,to,tokenId,false,_totalApprovalValidations);_totalApprovalValidations++;}/**
* @dev If approved is true create an approval petition from `operator` to operate on
* all of `owner` tokens, if not remove `operator` from operate on all of `owner` tokens
*
* Emits an {ValidateApproval} event.
*/function_setApprovalForAll(addressowner,addressoperator,boolapproved)internaloverridevirtual{require(owner!=operator,"ERC6997: approve to caller");if(approved){ApprovalValidationmemoryv;v.owner=owner;v.approve=operator;v.approveAll=true;_approvalValidations[_totalApprovalValidations]=v;emitValidateApproval(v.owner,operator,0,true,_totalApprovalValidations);_totalApprovalValidations++;}else{super._setApprovalForAll(owner,operator,approved);}}}
Security Considerations
As is defined in the Specification the operations that change the ownership of an NFT or enable an approval to manage the NFT SHALL create a TransferValidation or an ApprovalValidation pending to be validated and SHALL NOT transfer the ownership of an NFT or enable an approval.
With this premise in mind, the operations in charge of validating a TransferValidation or an ApprovalValidation must be protected with the maximum security required by the applied system.
For example, a valid system would be one where there is a validator address in charge of validating the transactions.
To give another example, a system where each user could choose his validator address would also be correct.
In any case, the importance of security resides in the fact that no address can validate a TransferValidation or an ApprovalValidation without the permission of the chosen system.