A standard interface for NFTs specifically designed for AI agents, where the metadata represents agent capabilities and requires privacy protection. Unlike traditional NFT standards that focus on static metadata, this standard introduces mechanisms for verifiable data ownership and secure transfer. By defining a unified interface for different verification methods (e.g., Trusted Execution Environment (TEE), Zero-Knowledge Proof (ZKP)), it enables secure management of valuable agent metadata such as models, memory, and character definitions, while maintaining confidentiality and verifiability.
Motivation
With the increasing intelligence of AI models, agents have become powerful tools for automating meaningful daily tasks. The integration of agents with blockchain technology has been recognized as a major narrative in the crypto industry, with many projects enabling agent creation for their users. However, a crucial missing piece is the decentralized management of agent ownership.
AI agents possess inherent non-fungible properties that make them natural candidates for NFT representation:
Each agent is unique, with its own model, memory, and character
Agents have private metadata (e.g., neural network models, memory, character definitions) that defines their capabilities
However, current NFT standards like ERC-721 are insufficient for representing AI agents as digital assets. While NFTs can establish ownership of digital items, using them to represent AI agents introduces unique challenges. The key issue lies in the metadata transfer mechanism. Unlike traditional NFTs where metadata is typically static and publicly accessible, an AI agent’s metadata (which constitutes the agent itself):
Has intrinsic value and is often the primary purpose of the transfer
Requires encrypted storage to protect intellectual property
Needs privacy-preserving and verifiable transfer mechanisms when ownership changes
For example, when transferring an agent NFT, we need to ensure:
The actual transfer of encrypted metadata (the agent’s model, memory, character, etc.) is verifiable
The new owner can securely access the metadata that constitutes the agent
The agent’s execution environment can verify ownership and load appropriate metadata
This EIP introduces a standard for NFTs with private metadata that addresses these requirements through privacy-preserving verification mechanisms, enabling secure ownership and transfer of valuable agent data while maintaining confidentiality and verifiability. This standard will serve as a foundation for the emerging agent ecosystem, allowing platforms to provide verifiable agent ownership and secure metadata management in a decentralized manner.
Specification
The EIP defines three key interfaces: the main NFT interface, the metadata interface, and the data verification interface.
Data Verification System
The verification system consists of two core components that work together to ensure secure data operations:
On-chain Verifier (data verification interface)
Implemented as a smart contract
Verifies proofs submitted through contract calls
Returns structured verification results
Can be implemented using different verification mechanisms (TEE/ZKP)
Off-chain Prover
Generates proofs for ownership and availability claims
Works with encrypted data and keys
Implementation varies based on verification mechanism:
TEE-based: Generates proofs within trusted hardware
Proves knowledge of pre-images for claimed dataHashes
Verified on-chain through verifyOwnership()
Transfer Validity Proof
Generated by Prover for data transfers
Proves:
Knowledge of original data (pre-images)
Correct decryption and re-encryption of data
Secure key transmission (using receiver’s public key to encrypt the new key)
Data availability in storage (using receiver’s signature to confirm the data is available in storage)
Verified on-chain through verifyTransferValidity()
The ownership verification is optional because when the minted token is transferred or cloned, the ownership verification is checked again inside the availability verification. It’s better to be safe than sorry, so we recommend doing ownership verification for minting and updates.
Different verification mechanisms have distinct capabilities:
TEE-based Implementation
Prover runs in trusted hardware
Can handle private keys securely
Enables direct data re-encryption
Verifier checks TEE attestations
ZKP-based Implementation
Prover generates cryptographic proofs
Cannot handle multi-party private keys
Re-encryption key known to prover
Requires additional re-encryption when next update, otherwise the new update is still visible to the prover
Data Verification Interface
/// @notice The type of the oracle
/// There are two types of oracles: TEE and ZKP
enumOracleType{TEE,ZKP}/// @notice The access proof which is a signature signed by the receiver (the receiver may delegate the signing privilege to the access assistant)
/// @param oldDataHash The hash of the old data
/// @param newDataHash The hash of the new data
/// @param nonce The nonce of the access proof
/// @param encryptedPubKey The encrypted public key, the receiver's public key which used to encrypt the new data key. `encryptedPubKey` can be empty in `accessProof`, and means that use the receiver's ethereum public key to encrypt the new data key
/// @param proof The proof
structAccessProof{bytes32oldDataHash;bytes32newDataHash;bytesnonce;bytesencryptedPubKey;bytesproof;}/// @notice The ownership proof which is a signature signed by the receiver (the receiver may delegate the signing privilege to the access assistant)
/// @param proofType The type of the proof
/// @param oldDataHash The hash of the old data
/// @param newDataHash The hash of the new data
/// @param sealedKey The sealed key of the new data key
/// @param encryptedPubKey The encrypted public key, the receiver's public key which used to encrypt the new data key
/// @param nonce The nonce
structOwnershipProof{OracleTypeoracleType;// The type of the oracle
bytes32oldDataHash;// The hash of the old data
bytes32newDataHash;// The hash of the new data
bytessealedKey;// The sealed key of the new data key
bytesencryptedPubKey;// The encrypted public key, the receiver's public key which used to encrypt the new data key
bytesnonce;// The nonce
bytesproof;// The proof
}structTransferValidityProof{AccessProofaccessProof;OwnershipProofownershipProof;}structTransferValidityProofOutput{bytes32oldDataHash;bytes32newDataHash;bytessealedKey;bytesencryptedPubKey;byteswantedKey;addressaccessAssistant;bytesaccessProofNonce;bytesownershipProofNonce;}interfaceIERC7857DataVerifier{/// @notice Verify data transfer validity, the _proofs prove:
/// 1. The pre-image of oldDataHashes
/// 2. The oldKey (old data key) can decrypt the pre-image and the new key re-encrypt the plaintexts to new ciphertexts
/// 3. The newKey (new data key) is encrypted using the encryptedPubKey
/// 4. The hashes of new ciphertexts is newDataHashes
/// 5. The newDataHashes identified ciphertexts are available in the storage: need the signature from the receiver or the access assistant signing oldDataHashes, newDataHashes, and encryptedPubKey
/// @param _proofs Proof generated by TEE/ZKP
functionverifyTransferValidity(TransferValidityProof[]calldata_proofs)externalreturns(TransferValidityProofOutput[]memory);}
Metadata Interface
structIntelligentData{stringdataDescription;bytes32dataHash;}interfaceIERC7857Metadata{/// @notice Get the name of the NFT collection
functionname()externalviewreturns(stringmemory);/// @notice Get the symbol of the NFT collection
functionsymbol()externalviewreturns(stringmemory);/// @notice Get the data hash of a token
/// @param _tokenId The token identifier
/// @return The current data hash of the token
functionintelligentDataOf(uint256_tokenId)externalviewreturns(IntelligentData[]memory);}
Main NFT Interface
interfaceIERC7857{/// @notice The event emitted when an address is approved to transfer a token
/// @param _from The address that is approving
/// @param _to The address that is being approved
/// @param _tokenId The token identifier
eventApproval(addressindexed_from,addressindexed_to,uint256indexed_tokenId);/// @notice The event emitted when an address is approved for all
/// @param _owner The owner
/// @param _operator The operator
/// @param _approved The approval
eventApprovalForAll(addressindexed_owner,addressindexed_operator,bool_approved);/// @notice The event emitted when an address is authorized to use a token
/// @param _from The address that is authorizing
/// @param _to The address that is being authorized
/// @param _tokenId The token identifier
eventAuthorization(addressindexed_from,addressindexed_to,uint256indexed_tokenId);/// @notice The event emitted when an address is revoked from using a token
/// @param _from The address that is revoking
/// @param _to The address that is being revoked
/// @param _tokenId The token identifier
eventAuthorizationRevoked(addressindexed_from,addressindexed_to,uint256indexed_tokenId);/// @notice The event emitted when a token is transferred
/// @param _tokenId The token identifier
/// @param _from The address that is transferring
/// @param _to The address that is receiving
eventTransferred(uint256_tokenId,addressindexed_from,addressindexed_to);/// @notice The event emitted when a token is cloned
/// @param _tokenId The token identifier
/// @param _newTokenId The new token identifier
/// @param _from The address that is cloning
/// @param _to The address that is receiving
eventCloned(uint256indexed_tokenId,uint256indexed_newTokenId,address_from,address_to);/// @notice The event emitted when a sealed key is published
/// @param _to The address that is receiving
/// @param _tokenId The token identifier
/// @param _sealedKeys The sealed keys
eventPublishedSealedKey(addressindexed_to,uint256indexed_tokenId,bytes[]_sealedKeys);/// @notice The event emitted when a user is delegated to an assistant
/// @param _user The user
/// @param _assistant The assistant
eventDelegateAccess(addressindexed_user,addressindexed_assistant);/// @notice The verifier interface that this NFT uses
/// @return The address of the verifier contract
functionverifier()externalviewreturns(IERC7857DataVerifier);/// @notice Transfer data with ownership
/// @param _to Address to transfer data to
/// @param _tokenId The token to transfer data for
/// @param _proofs Proofs of data available for _to
functioniTransfer(address_to,uint256_tokenId,TransferValidityProof[]calldata_proofs)external;/// @notice Clone data
/// @param _to Address to clone data to
/// @param _tokenId The token to clone data for
/// @param _proofs Proofs of data available for _to
/// @return _newTokenId The ID of the newly cloned token
functioniClone(address_to,uint256_tokenId,TransferValidityProof[]calldata_proofs)externalreturns(uint256_newTokenId);/// @notice Add authorized user to group
/// @param _tokenId The token to add to group
functionauthorizeUsage(uint256_tokenId,address_user)external;/// @notice Revoke authorization from a user
/// @param _tokenId The token to revoke authorization from
/// @param _user The user to revoke authorization from
functionrevokeAuthorization(uint256_tokenId,address_user)external;/// @notice Approve an address to transfer a token
/// @param _to The address to approve
/// @param _tokenId The token identifier
functionapprove(address_to,uint256_tokenId)external;/// @notice Set approval for all
/// @param _operator The operator
/// @param _approved The approval
functionsetApprovalForAll(address_operator,bool_approved)external;/// @notice Delegate access check to an assistant
/// @param _assistant The assistant
functiondelegateAccess(address_assistant)external;/// @notice Get token owner
/// @param _tokenId The token identifier
/// @return The current owner of the token
functionownerOf(uint256_tokenId)externalviewreturns(address);/// @notice Get the authorized users of a token
/// @param _tokenId The token identifier
/// @return The current authorized users of the token
functionauthorizedUsersOf(uint256_tokenId)externalviewreturns(address[]memory);/// @notice Get the approved address for a token
/// @param _tokenId The token identifier
/// @return The approved address
functiongetApproved(uint256_tokenId)externalviewreturns(address);/// @notice Check if an address is approved for all
/// @param _owner The owner
/// @param _operator The operator
/// @return The approval
functionisApprovedForAll(address_owner,address_operator)externalviewreturns(bool);/// @notice Get the delegate access for a user
/// @param _user The user
/// @return The delegate access
functiongetDelegateAccess(address_user)externalviewreturns(address);}
Rationale
The design choices in this standard are motivated by several key requirements:
Verification Abstraction: The standard separates the verification logic into a dedicated interface (IDataVerifier), allowing different verification mechanisms (TEE, ZKP) to be implemented and used interchangeably. The verifier should support two types of proof:
Ownership Proof Verifies that the prover possesses the original data by demonstrating knowledge of the pre-images that generate the claimed dataHashes
Transfer Validity Proof Verifies secure data integrity and availability by proving: knowledge of the original data (pre-images of oldDataHashes); ability to decrypt with oldKey and re-encrypt with newKey; secure transmission of newKey using recipient’s public key; integrity of the newly encrypted data matching newDataHashes; and data availability confirmed by recipient’s signature on both oldDataHashes and newDataHashes
Data Protection: The standard uses data hashes and encrypted keys to ensure that valuable NFT data remains protected while still being integrity and availability verifiable
Flexible Data Management: Three distinct data operations are supported:
Full transfer, where the data and ownership are transferred to the new owner
Data cloning, where the data is cloned to a new token but the ownership is not transferred
Data usage authorization, where the data is authorized to be used by a specific user, but the ownership is not transferred, and the user still cannot access the data. This need an environment to authenticate the user and process the request from the authorized user secretly, we call it “Sealed Executor”
Sealed Executor: Although the Sealed Executor is not defined and out of the scope of this standard, it is a crucial component for the standard to work. The Sealed Executor is an environment that can authenticate the user and process the request from the authorized user secretly. The Sealed Executor should get authorized group by tokenId, and the verify the signature of the user using the public keys in the authorized group. If the verification is successful, the executor will process the request and return the result to the user, and the sealed executor could be implemented by a trusted party (where permitted), TEE or Fully Homomorphic Encryption (FHE)
Backwards Compatibility
This EIP does not inherit from existing NFT standards to maintain its focus on functional data management. However, implementations can choose to additionally implement ERC-721 if traditional NFT compatibility is desired.
Reference Implementation
Verifier
abstractcontractBaseVerifierisIERC7857DataVerifier{// prevent replay attack
mapping(bytes32=>bool)internalusedProofs;// prevent replay attack
mapping(bytes32=>uint256)internalproofTimestamps;function_checkAndMarkProof(bytes32proofNonce)internal{require(!usedProofs[proofNonce],"Proof already used");usedProofs[proofNonce]=true;proofTimestamps[proofNonce]=block.timestamp;}// clean expired proof records (save gas)
functioncleanExpiredProofs(bytes32[]calldataproofNonces)external{for(uint256i=0;i<proofNonces.length;i++){bytes32nonce=proofNonces[i];if(usedProofs[nonce]&&block.timestamp>proofTimestamps[nonce]+7days){deleteusedProofs[nonce];deleteproofTimestamps[nonce];}}}uint256[50]private__gap;}structAttestationConfig{OracleTypeoracleType;addresscontractAddress;}contractVerifierisBaseVerifier,Initializable,AccessControlUpgradeable,PausableUpgradeable{usingECDSAforbytes32;usingMessageHashUtilsforbytes32;eventAttestationContractUpdated(AttestationConfig[]attestationConfigs);bytes32publicconstantADMIN_ROLE=keccak256("ADMIN_ROLE");bytes32publicconstantPAUSER_ROLE=keccak256("PAUSER_ROLE");mapping(OracleType=>address)publicattestationContract;uint256publicmaxProofAge;stringpublicconstantVERSION="2.0.0";/// @custom:oz-upgrades-unsafe-allow constructor
constructor(){_disableInitializers();}functioninitialize(AttestationConfig[]calldata_attestationConfigs,address_admin)externalinitializer{__AccessControl_init();__Pausable_init();for(uint256i=0;i<_attestationConfigs.length;i++){attestationContract[_attestationConfigs[i].oracleType]=_attestationConfigs[i].contractAddress;}maxProofAge=7days;_grantRole(DEFAULT_ADMIN_ROLE,_admin);_grantRole(ADMIN_ROLE,_admin);_grantRole(PAUSER_ROLE,_admin);emitAttestationContractUpdated(_attestationConfigs);}functionupdateAttestationContract(AttestationConfig[]calldata_attestationConfigs)externalonlyRole(ADMIN_ROLE){for(uint256i=0;i<_attestationConfigs.length;i++){attestationContract[_attestationConfigs[i].oracleType]=_attestationConfigs[i].contractAddress;}emitAttestationContractUpdated(_attestationConfigs);}functionupdateMaxProofAge(uint256_maxProofAge)externalonlyRole(ADMIN_ROLE){maxProofAge=_maxProofAge;}functionpause()externalonlyRole(PAUSER_ROLE){_pause();}functionunpause()externalonlyRole(PAUSER_ROLE){_unpause();}functionhashNonce(bytesmemorynonce)privatepurereturns(bytes32){returnkeccak256(nonce);}functionteeOracleVerify(bytes32messageHash,bytesmemorysignature)internalviewreturns(bool){returnTEEVerifier(attestationContract[OracleType.TEE]).verifyTEESignature(messageHash,signature);}/// @notice Extract and verify signature from the access proof
/// @param accessProof The access proof
/// @return The recovered access assistant address
functionverifyAccessibility(AccessProofmemoryaccessProof)privatepurereturns(address){bytes32messageHash=keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n66",Strings.toHexString(uint256(keccak256(abi.encodePacked(accessProof.oldDataHash,accessProof.newDataHash,accessProof.encryptedPubKey,accessProof.nonce))),32)));addressaccessAssistant=messageHash.recover(accessProof.proof);require(accessAssistant!=address(0),"Invalid access assistant");returnaccessAssistant;}functionverfifyOwnershipProof(OwnershipProofmemoryownershipProof)privateviewreturns(bool){if(ownershipProof.oracleType==OracleType.TEE){bytes32messageHash=keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n66",Strings.toHexString(uint256(keccak256(abi.encodePacked(ownershipProof.oldDataHash,ownershipProof.newDataHash,ownershipProof.sealedKey,ownershipProof.encryptedPubKey,ownershipProof.nonce))),32)));returnteeOracleVerify(messageHash,ownershipProof.proof);}// TODO: add ZKP verification
else{returnfalse;}}/// @notice Process a single transfer validity proof
/// @param proof The proof data
/// @return output The processed proof data as a struct
functionprocessTransferProof(TransferValidityProofcalldataproof)privateviewreturns(TransferValidityProofOutputmemoryoutput){// compare the proof data in access proof and ownership proof
require(proof.accessProof.oldDataHash==proof.ownershipProof.oldDataHash,"Invalid oldDataHashes");output.oldDataHash=proof.accessProof.oldDataHash;require(proof.accessProof.newDataHash==proof.ownershipProof.newDataHash,"Invalid newDataHashes");output.newDataHash=proof.accessProof.newDataHash;output.wantedKey=proof.accessProof.encryptedPubKey;output.accessProofNonce=proof.accessProof.nonce;output.encryptedPubKey=proof.ownershipProof.encryptedPubKey;output.sealedKey=proof.ownershipProof.sealedKey;output.ownershipProofNonce=proof.ownershipProof.nonce;// verify the access assistant
output.accessAssistant=verifyAccessibility(proof.accessProof);boolisOwn=verfifyOwnershipProof(proof.ownershipProof);require(isOwn,"Invalid ownership proof");returnoutput;}/// @notice Verify data transfer validity, the _proof prove:
/// 1. The pre-image of oldDataHashes
/// 2. The oldKey can decrypt the pre-image and the new key re-encrypt the plaintexts to new ciphertexts
/// 3. The newKey is encrypted with the receiver's pubKey to get the sealedKey
/// 4. The hashes of new ciphertexts is newDataHashes (key to note: TEE could support a private key of the receiver)
/// 5. The newDataHashes identified ciphertexts are available in the storage: need the signature from the receiver signing oldDataHashes and newDataHashes
/// @param proofs Proof generated by TEE/ZKP oracle
functionverifyTransferValidity(TransferValidityProof[]calldataproofs)publicvirtualoverridewhenNotPausedreturns(TransferValidityProofOutput[]memory){TransferValidityProofOutput[]memoryoutputs=newTransferValidityProofOutput[](proofs.length);for(uint256i=0;i<proofs.length;i++){TransferValidityProofOutputmemoryoutput=processTransferProof(proofs[i]);outputs[i]=output;bytes32accessProofNonce=hashNonce(output.accessProofNonce);_checkAndMarkProof(accessProofNonce);bytes32ownershipProofNonce=hashNonce(output.ownershipProofNonce);_checkAndMarkProof(ownershipProofNonce);}returnoutputs;}uint256[50]private__gap;}
Main NFT
contractAgentNFTisAccessControlEnumerableUpgradeable,IERC7857,IERC7857Metadata{eventUpdated(uint256indexed_tokenId,IntelligentData[]_oldDatas,IntelligentData[]_newDatas);eventMinted(uint256indexed_tokenId,addressindexed_creator,addressindexed_owner);structTokenData{addressowner;address[]authorizedUsers;addressapprovedUser;IntelligentData[]iDatas;}/// @custom:storage-location erc7201:agent.storage.AgentNFT
structAgentNFTStorage{// Token data
mapping(uint256=>TokenData)tokens;mapping(addressowner=>mapping(addressoperator=>bool))operatorApprovals;mapping(addressuser=>addressaccessAssistant)accessAssistants;uint256nextTokenId;// Contract metadata
stringname;stringsymbol;stringstorageInfo;// Core components
IERC7857DataVerifierverifier;}bytes32publicconstantADMIN_ROLE=keccak256("ADMIN_ROLE");bytes32publicconstantPAUSER_ROLE=keccak256("PAUSER_ROLE");stringpublicconstantVERSION="2.0.0";// keccak256(abi.encode(uint(keccak256("agent.storage.AgentNFT")) - 1)) & ~bytes32(uint(0xff))
bytes32privateconstantAGENT_NFT_STORAGE_LOCATION=0x4aa80aaafbe0e5fe3fe1aa97f3c1f8c65d61f96ef1aab2b448154f4e07594600;function_getAgentStorage()privatepurereturns(AgentNFTStoragestorage$){assembly{$.slot:=AGENT_NFT_STORAGE_LOCATION}}/// @custom:oz-upgrades-unsafe-allow constructor
constructor(){_disableInitializers();}functioninitialize(stringmemoryname_,stringmemorysymbol_,stringmemorystorageInfo_,addressverifierAddr,addressadmin_)publicvirtualinitializer{require(verifierAddr!=address(0),"Zero address");__AccessControlEnumerable_init();_grantRole(DEFAULT_ADMIN_ROLE,admin_);_grantRole(ADMIN_ROLE,admin_);_grantRole(PAUSER_ROLE,admin_);AgentNFTStoragestorage$=_getAgentStorage();$.name=name_;$.symbol=symbol_;$.storageInfo=storageInfo_;$.verifier=IERC7857DataVerifier(verifierAddr);}// Basic getters
functionname()publicviewvirtualreturns(stringmemory){return_getAgentStorage().name;}functionsymbol()publicviewvirtualreturns(stringmemory){return_getAgentStorage().symbol;}functionverifier()publicviewvirtualreturns(IERC7857DataVerifier){return_getAgentStorage().verifier;}// Admin functions
functionupdateVerifier(addressnewVerifier)publicvirtualonlyRole(ADMIN_ROLE){require(newVerifier!=address(0),"Zero address");_getAgentStorage().verifier=IERC7857DataVerifier(newVerifier);}functionupdate(uint256tokenId,IntelligentData[]calldatanewDatas)publicvirtual{AgentNFTStoragestorage$=_getAgentStorage();TokenDatastoragetoken=$.tokens[tokenId];require(token.owner==msg.sender,"Not owner");require(newDatas.length>0,"Empty data array");IntelligentData[]memoryoldDatas=newIntelligentData[](token.iDatas.length);for(uinti=0;i<token.iDatas.length;i++){oldDatas[i]=token.iDatas[i];}deletetoken.iDatas;for(uinti=0;i<newDatas.length;i++){token.iDatas.push(newDatas[i]);}emitUpdated(tokenId,oldDatas,newDatas);}functionmint(IntelligentData[]calldataiDatas,addressto)publicpayablevirtualreturns(uint256tokenId){require(to!=address(0),"Zero address");require(iDatas.length>0,"Empty data array");AgentNFTStoragestorage$=_getAgentStorage();tokenId=$.nextTokenId++;TokenDatastoragenewToken=$.tokens[tokenId];newToken.owner=to;newToken.approvedUser=address(0);for(uinti=0;i<iDatas.length;i++){newToken.iDatas.push(iDatas[i]);}emitMinted(tokenId,msg.sender,to);}function_proofCheck(addressfrom,addressto,uint256tokenId,TransferValidityProof[]calldataproofs)internalreturns(bytes[]memorysealedKeys,IntelligentData[]memorynewDatas){AgentNFTStoragestorage$=_getAgentStorage();require(to!=address(0),"Zero address");require($.tokens[tokenId].owner==from,"Not owner");require(proofs.length>0,"Empty proofs array");TransferValidityProofOutput[]memoryproofOutput=$.verifier.verifyTransferValidity(proofs);require(proofOutput.length==$.tokens[tokenId].iDatas.length,"Proof count mismatch");sealedKeys=newbytes[](proofOutput.length);newDatas=newIntelligentData[](proofOutput.length);for(uinti=0;i<proofOutput.length;i++){// require the initial data hash is the same as the old data hash
require(proofOutput[i].oldDataHash==$.tokens[tokenId].iDatas[i].dataHash,"Old data hash mismatch");// only the receiver itself or the access assistant can sign the access proof
require(proofOutput[i].accessAssistant==$.accessAssistants[to]||proofOutput[i].accessAssistant==to,"Access assistant mismatch");bytesmemorywantedKey=proofOutput[i].wantedKey;bytesmemoryencryptedPubKey=proofOutput[i].encryptedPubKey;if(wantedKey.length==0){// if the wanted key is empty, the default wanted receiver is receiver itself
addressdefaultWantedReceiver=Utils.pubKeyToAddress(encryptedPubKey);require(defaultWantedReceiver==to,"Default wanted receiver mismatch");}else{// if the wanted key is not empty, the data is private
require(Utils.bytesEqual(encryptedPubKey,wantedKey),"encryptedPubKey mismatch");}sealedKeys[i]=proofOutput[i].sealedKey;newDatas[i]=IntelligentData({dataDescription:$.tokens[tokenId].iDatas[i].dataDescription,dataHash:proofOutput[i].newDataHash});}return(sealedKeys,newDatas);}function_transfer(addressfrom,addressto,uint256tokenId,TransferValidityProof[]calldataproofs)internal{AgentNFTStoragestorage$=_getAgentStorage();(bytes[]memorysealedKeys,IntelligentData[]memorynewDatas)=_proofCheck(from,to,tokenId,proofs);TokenDatastoragetoken=$.tokens[tokenId];token.owner=to;token.approvedUser=address(0);deletetoken.iDatas;for(uinti=0;i<newDatas.length;i++){token.iDatas.push(newDatas[i]);}emitTransferred(tokenId,from,to);emitPublishedSealedKey(to,tokenId,sealedKeys);}functioniTransfer(addressto,uint256tokenId,TransferValidityProof[]calldataproofs)publicvirtual{require(_isApprovedOrOwner(msg.sender,tokenId),"Not authorized");_transfer(ownerOf(tokenId),to,tokenId,proofs);}functiontransferFrom(addressfrom,addressto,uint256tokenId)publicvirtual{TokenDatastoragetoken=_getAgentStorage().tokens[tokenId];require(_isApprovedOrOwner(msg.sender,tokenId),"Not authorized");require(to!=address(0),"Zero address");require(token.owner==from,"Not owner");token.owner=to;token.approvedUser=address(0);emitTransferred(tokenId,from,to);}functioniTransferFrom(addressfrom,addressto,uint256tokenId,TransferValidityProof[]calldataproofs)publicvirtual{require(_isApprovedOrOwner(msg.sender,tokenId),"Not authorized");_transfer(from,to,tokenId,proofs);}function_clone(addressfrom,addressto,uint256tokenId,TransferValidityProof[]calldataproofs)internalreturns(uint256){AgentNFTStoragestorage$=_getAgentStorage();(bytes[]memorysealedKeys,IntelligentData[]memorynewDatas)=_proofCheck(from,to,tokenId,proofs);uint256newTokenId=$.nextTokenId++;TokenDatastoragenewToken=$.tokens[newTokenId];newToken.owner=to;newToken.approvedUser=address(0);for(uinti=0;i<newDatas.length;i++){newToken.iDatas.push(newDatas[i]);}emitCloned(tokenId,newTokenId,from,to);emitPublishedSealedKey(to,newTokenId,sealedKeys);returnnewTokenId;}functioniClone(addressto,uint256tokenId,TransferValidityProof[]calldataproofs)publicvirtualreturns(uint256){require(_isApprovedOrOwner(msg.sender,tokenId),"Not authorized");return_clone(ownerOf(tokenId),to,tokenId,proofs);}functioniCloneFrom(addressfrom,addressto,uint256tokenId,TransferValidityProof[]calldataproofs)publicvirtualreturns(uint256){require(_isApprovedOrOwner(msg.sender,tokenId),"Not authorized");return_clone(from,to,tokenId,proofs);}functionauthorizeUsage(uint256tokenId,addressto)publicvirtual{require(to!=address(0),"Zero address");AgentNFTStoragestorage$=_getAgentStorage();require($.tokens[tokenId].owner==msg.sender,"Not owner");address[]storageauthorizedUsers=$.tokens[tokenId].authorizedUsers;for(uinti=0;i<authorizedUsers.length;i++){require(authorizedUsers[i]!=to,"Already authorized");}authorizedUsers.push(to);emitAuthorization(msg.sender,to,tokenId);}functionownerOf(uint256tokenId)publicviewvirtualreturns(address){AgentNFTStoragestorage$=_getAgentStorage();addressowner=$.tokens[tokenId].owner;require(owner!=address(0),"Token does not exist");returnowner;}functionauthorizedUsersOf(uint256tokenId)publicviewvirtualreturns(address[]memory){AgentNFTStoragestorage$=_getAgentStorage();require(_exists(tokenId),"Token does not exist");return$.tokens[tokenId].authorizedUsers;}functionstorageInfo(uint256tokenId)publicviewvirtualreturns(stringmemory){require(_exists(tokenId),"Token does not exist");return_getAgentStorage().storageInfo;}function_exists(uint256tokenId)internalviewreturns(bool){return_getAgentStorage().tokens[tokenId].owner!=address(0);}functionintelligentDataOf(uint256tokenId)publicviewvirtualreturns(IntelligentData[]memory){AgentNFTStoragestorage$=_getAgentStorage();require(_exists(tokenId),"Token does not exist");return$.tokens[tokenId].iDatas;}functionapprove(addressto,uint256tokenId)publicvirtual{addressowner=ownerOf(tokenId);require(to!=owner,"Approval to current owner");require(msg.sender==owner||isApprovedForAll(owner,msg.sender),"Not authorized");_getAgentStorage().tokens[tokenId].approvedUser=to;emitApproval(owner,to,tokenId);}functionsetApprovalForAll(addressoperator,boolapproved)publicvirtual{require(operator!=msg.sender,"Approve to caller");_getAgentStorage().operatorApprovals[msg.sender][operator]=approved;emitApprovalForAll(msg.sender,operator,approved);}functiongetApproved(uint256tokenId)publicviewvirtualreturns(address){require(_exists(tokenId),"Token does not exist");return_getAgentStorage().tokens[tokenId].approvedUser;}functionisApprovedForAll(addressowner,addressoperator)publicviewvirtualreturns(bool){return_getAgentStorage().operatorApprovals[owner][operator];}functiondelegateAccess(addressassistant)publicvirtual{require(assistant!=address(0),"Zero address");_getAgentStorage().accessAssistants[msg.sender]=assistant;emitDelegateAccess(msg.sender,assistant);}functiongetDelegateAccess(addressuser)publicviewvirtualreturns(address){return_getAgentStorage().accessAssistants[user];}function_isApprovedOrOwner(addressspender,uint256tokenId)internalviewreturns(bool){require(_exists(tokenId),"Token does not exist");addressowner=ownerOf(tokenId);return(spender==owner||getApproved(tokenId)==spender||isApprovedForAll(owner,spender));}functionbatchAuthorizeUsage(uint256tokenId,address[]calldatausers)publicvirtual{require(users.length>0,"Empty users array");AgentNFTStoragestorage$=_getAgentStorage();require($.tokens[tokenId].owner==msg.sender,"Not owner");for(uinti=0;i<users.length;i++){require(users[i]!=address(0),"Zero address in users");$.tokens[tokenId].authorizedUsers.push(users[i]);emitAuthorization(msg.sender,users[i],tokenId);}}functionrevokeAuthorization(uint256tokenId,addressuser)publicvirtual{AgentNFTStoragestorage$=_getAgentStorage();require($.tokens[tokenId].owner==msg.sender,"Not owner");require(user!=address(0),"Zero address");address[]storageauthorizedUsers=$.tokens[tokenId].authorizedUsers;boolfound=false;for(uinti=0;i<authorizedUsers.length;i++){if(authorizedUsers[i]==user){authorizedUsers[i]=authorizedUsers[authorizedUsers.length-1];authorizedUsers.pop();found=true;break;}}require(found,"User not authorized");emitAuthorizationRevoked(msg.sender,user,tokenId);}}
Security Considerations
Proof Verification
Implementations must carefully verify all assertions in the proof
Replay attacks must be prevented
Different verification systems have their own security considerations, and distinct capabilities regarding key management: TEE can securely handle private keys from multi-parties, enabling direct data re-encryption. However, ZKP, due to its cryptographic nature, cannot process private keys from multi-parties. As a result, the re-encryption key is also from the prover (i.e., the sender), so tokens acquired through transfer or cloning must undergo re-encryption during their next update, otherwise the new update is still visible to the previous owner. This distinction in key handling capabilities affects how data transformations are managed during later usage
Data Privacy
Only hashes and sealed keys are stored on-chain, actual functional data must be stored and transmitted securely off-chain
Key management is crucial for secure data access
TEE verification system could support private key of the receiver, but ZKP verification system could not. So when using ZKP, the token transferred or cloned from other should be re-encrypted when next update, otherwise the new update is still visible to the previous owner
Access Control and State Management
Operations restricted to token owners only
All data operations must maintain integrity and availability
Critical state changes (sealed keys, ownership, permissions) must be atomic and verifiable
Sealed Executor
Although out of scope for this standard, the Sealed Executor is crucial for secure operation
The Sealed Executor authenticates users and processes requests in a secure environment by verifying user signatures against authorized public keys for each tokenId
The Sealed Executor can be implemented through a trusted party (where permitted), TEE or FHE
Ensuring secure request processing and result delivery
Ming Wu (@sparkmiw), Jason Zeng (@zenghbo), Wei Wu (@Wilbert957), Michael Heinrich (@michaelomg), "ERC-7857: AI Agents NFT with Private Metadata [DRAFT]," Ethereum Improvement Proposals, no. 7857, January 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7857.