Abstract

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:

  1. Each agent is unique, with its own model, memory, and character
  2. Agents embody clear ownership rights, representing significant computational investment and intellectual property
  3. 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):

  1. Has intrinsic value and is often the primary purpose of the transfer
  2. Requires encrypted storage to protect intellectual property
  3. Needs privacy-preserving and verifiable transfer mechanisms when ownership changes

For example, when transferring an agent NFT, we need to ensure:

  1. The actual transfer of encrypted metadata (the agent’s model, memory, character, etc.) is verifiable
  2. The new owner can securely access the metadata that constitutes the agent
  3. 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:

  1. 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)
  2. 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
      • ZKP-based: Creates cryptographic zero-knowledge proofs

The system supports two types of proofs:

  1. Ownership Proof
    • Generated by Prover with access to original data
    • Proves knowledge of pre-images for claimed dataHashes
    • Verified on-chain through verifyOwnership()
  2. 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
enum OracleType {
    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
struct AccessProof {
    bytes32 oldDataHash;
    bytes32 newDataHash;
    bytes nonce;
    bytes encryptedPubKey;
    bytes proof;
}

/// @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
struct OwnershipProof {
    OracleType oracleType; // The type of the oracle
    bytes32 oldDataHash; // The hash of the old data
    bytes32 newDataHash; // The hash of the new data
    bytes sealedKey; // The sealed key of the new data key
    bytes encryptedPubKey; // The encrypted public key, the receiver's public key which used to encrypt the new data key
    bytes nonce; // The nonce
    bytes proof; // The proof
}

struct TransferValidityProof {
    AccessProof accessProof;
    OwnershipProof ownershipProof;
}

struct TransferValidityProofOutput {
    bytes32 oldDataHash;
    bytes32 newDataHash;
    bytes sealedKey;
    bytes encryptedPubKey;
    bytes wantedKey;
    address accessAssistant;
    bytes accessProofNonce;
    bytes ownershipProofNonce;
}

interface IERC7857DataVerifier {
    /// @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
    function verifyTransferValidity(
        TransferValidityProof[] calldata _proofs
    ) external returns (TransferValidityProofOutput[] memory);
}

Metadata Interface

struct IntelligentData {
    string dataDescription;
    bytes32 dataHash;
}

interface IERC7857Metadata {
    /// @notice Get the name of the NFT collection
    function name() external view returns (string memory);

    /// @notice Get the symbol of the NFT collection
    function symbol() external view returns (string memory);

    /// @notice Get the data hash of a token
    /// @param _tokenId The token identifier
    /// @return The current data hash of the token
    function intelligentDataOf(uint256 _tokenId) external view returns (IntelligentData[] memory);
}

Main NFT Interface

interface IERC7857 {
    /// @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
    event Approval(
        address indexed _from,
        address indexed _to,
        uint256 indexed _tokenId
    );

    /// @notice The event emitted when an address is approved for all
    /// @param _owner The owner
    /// @param _operator The operator
    /// @param _approved The approval
    event ApprovalForAll(
        address indexed _owner,
        address indexed _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
    event Authorization(
        address indexed _from,
        address indexed _to,
        uint256 indexed _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
    event AuthorizationRevoked(
        address indexed _from,
        address indexed _to,
        uint256 indexed _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
    event Transferred(
        uint256 _tokenId,
        address indexed _from,
        address indexed _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
    event Cloned(
        uint256 indexed _tokenId,
        uint256 indexed _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
    event PublishedSealedKey(
        address indexed _to,
        uint256 indexed _tokenId,
        bytes[] _sealedKeys
    );

    /// @notice The event emitted when a user is delegated to an assistant
    /// @param _user The user
    /// @param _assistant The assistant
    event DelegateAccess(address indexed _user, address indexed _assistant);

    /// @notice The verifier interface that this NFT uses
    /// @return The address of the verifier contract
    function verifier() external view returns (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
    function iTransfer(
        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
    function iClone(
        address _to,
        uint256 _tokenId,
        TransferValidityProof[] calldata _proofs
    ) external returns (uint256 _newTokenId);

    /// @notice Add authorized user to group
    /// @param _tokenId The token to add to group
    function authorizeUsage(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
    function revokeAuthorization(uint256 _tokenId, address _user) external;

    /// @notice Approve an address to transfer a token
    /// @param _to The address to approve
    /// @param _tokenId The token identifier
    function approve(address _to, uint256 _tokenId) external;

    /// @notice Set approval for all
    /// @param _operator The operator
    /// @param _approved The approval
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice Delegate access check to an assistant
    /// @param _assistant The assistant
    function delegateAccess(address _assistant) external;

    /// @notice Get token owner
    /// @param _tokenId The token identifier
    /// @return The current owner of the token
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice Get the authorized users of a token
    /// @param _tokenId The token identifier
    /// @return The current authorized users of the token
    function authorizedUsersOf(
        uint256 _tokenId
    ) external view returns (address[] memory);

    /// @notice Get the approved address for a token
    /// @param _tokenId The token identifier
    /// @return The approved address
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice Check if an address is approved for all
    /// @param _owner The owner
    /// @param _operator The operator
    /// @return The approval
    function isApprovedForAll(
        address _owner,
        address _operator
    ) external view returns (bool);

    /// @notice Get the delegate access for a user
    /// @param _user The user
    /// @return The delegate access
    function getDelegateAccess(address _user) external view returns (address);
}

Rationale

The design choices in this standard are motivated by several key requirements:

  1. 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
  2. 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

  3. 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”
  4. 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

abstract contract BaseVerifier is IERC7857DataVerifier {
    // prevent replay attack
    mapping(bytes32 => bool) internal usedProofs;
    
    // prevent replay attack
    mapping(bytes32 => uint256) internal proofTimestamps;
    
    function _checkAndMarkProof(bytes32 proofNonce) internal {
        require(!usedProofs[proofNonce], "Proof already used");
        usedProofs[proofNonce] = true;
        proofTimestamps[proofNonce] = block.timestamp;
    }
    
    // clean expired proof records (save gas)
    function cleanExpiredProofs(bytes32[] calldata proofNonces) external {
        for (uint256 i = 0; i < proofNonces.length; i++) {
            bytes32 nonce = proofNonces[i];
            if (usedProofs[nonce] && 
                block.timestamp > proofTimestamps[nonce] + 7 days) {
                delete usedProofs[nonce];
                delete proofTimestamps[nonce];
            }
        }
    }

    uint256[50] private __gap;
}

struct AttestationConfig {
    OracleType oracleType;
    address contractAddress;
}

contract Verifier is
    BaseVerifier,
    Initializable,
    AccessControlUpgradeable,
    PausableUpgradeable
{
    using ECDSA for bytes32;
    using MessageHashUtils for bytes32;

    event AttestationContractUpdated(AttestationConfig[] attestationConfigs);

    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    mapping(OracleType => address) public attestationContract;

    uint256 public maxProofAge;

    string public constant VERSION = "2.0.0";

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(
        AttestationConfig[] calldata _attestationConfigs,
        address _admin
    ) external initializer {
        __AccessControl_init();
        __Pausable_init();

        for (uint256 i = 0; i < _attestationConfigs.length; i++) {
            attestationContract[
                _attestationConfigs[i].oracleType
            ] = _attestationConfigs[i].contractAddress;
        }
        maxProofAge = 7 days;

        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
        _grantRole(ADMIN_ROLE, _admin);
        _grantRole(PAUSER_ROLE, _admin);

        emit AttestationContractUpdated(_attestationConfigs);
    }

    function updateAttestationContract(
        AttestationConfig[] calldata _attestationConfigs
    ) external onlyRole(ADMIN_ROLE) {
        for (uint256 i = 0; i < _attestationConfigs.length; i++) {
            attestationContract[
                _attestationConfigs[i].oracleType
            ] = _attestationConfigs[i].contractAddress;
        }

        emit AttestationContractUpdated(_attestationConfigs);
    }

    function updateMaxProofAge(
        uint256 _maxProofAge
    ) external onlyRole(ADMIN_ROLE) {
        maxProofAge = _maxProofAge;
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    function hashNonce(bytes memory nonce) private pure returns (bytes32) {
        return keccak256(nonce);
    }

    function teeOracleVerify(
        bytes32 messageHash,
        bytes memory signature
    ) internal view returns (bool) {
        return
            TEEVerifier(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
    function verifyAccessibility(
        AccessProof memory accessProof
    ) private pure returns (address) {
        bytes32 messageHash = keccak256(
            abi.encodePacked(
                "\x19Ethereum Signed Message:\n66",
                Strings.toHexString(
                    uint256(
                        keccak256(
                            abi.encodePacked(
                                accessProof.oldDataHash,
                                accessProof.newDataHash,
                                accessProof.encryptedPubKey,
                                accessProof.nonce
                            )
                        )
                    ),
                    32
                )
            )
        );

        address accessAssistant = messageHash.recover(accessProof.proof);
        require(accessAssistant != address(0), "Invalid access assistant");
        return accessAssistant;
    }

    function verfifyOwnershipProof(
        OwnershipProof memory ownershipProof
    ) private view returns (bool) {
        if (ownershipProof.oracleType == OracleType.TEE) {
            bytes32 messageHash = keccak256(
                abi.encodePacked(
                    "\x19Ethereum Signed Message:\n66",
                    Strings.toHexString(
                        uint256(
                            keccak256(
                                abi.encodePacked(
                                    ownershipProof.oldDataHash,
                                    ownershipProof.newDataHash,
                                    ownershipProof.sealedKey,
                                    ownershipProof.encryptedPubKey,
                                    ownershipProof.nonce
                                )
                            )
                        ),
                        32
                    )
                )
            );

            return teeOracleVerify(messageHash, ownershipProof.proof);
        }
        // TODO: add ZKP verification
        else {
            return false;
        }
    }

    /// @notice Process a single transfer validity proof
    /// @param proof The proof data
    /// @return output The processed proof data as a struct
    function processTransferProof(
        TransferValidityProof calldata proof
    ) private view returns (TransferValidityProofOutput memory output) {
        // 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);

        bool isOwn = verfifyOwnershipProof(proof.ownershipProof);

        require(isOwn, "Invalid ownership proof");

        return output;
    }

    /// @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
    function verifyTransferValidity(
        TransferValidityProof[] calldata proofs
    )
        public
        virtual
        override
        whenNotPaused
        returns (TransferValidityProofOutput[] memory)
    {
        TransferValidityProofOutput[]
            memory outputs = new TransferValidityProofOutput[](proofs.length);

        for (uint256 i = 0; i < proofs.length; i++) {
            TransferValidityProofOutput memory output = processTransferProof(
                proofs[i]
            );

            outputs[i] = output;

            bytes32 accessProofNonce = hashNonce(output.accessProofNonce);
            _checkAndMarkProof(accessProofNonce);

            bytes32 ownershipProofNonce = hashNonce(output.ownershipProofNonce);
            _checkAndMarkProof(ownershipProofNonce);
        }

        return outputs;
    }

    uint256[50] private __gap;
}

Main NFT

contract AgentNFT is
    AccessControlEnumerableUpgradeable,
    IERC7857,
    IERC7857Metadata
{
    event Updated(
        uint256 indexed _tokenId,
        IntelligentData[] _oldDatas,
        IntelligentData[] _newDatas
    );

    event Minted(
        uint256 indexed _tokenId,
        address indexed _creator,
        address indexed _owner
    );

    struct TokenData {
        address owner;
        address[] authorizedUsers;
        address approvedUser;
        IntelligentData[] iDatas;
    }

    /// @custom:storage-location erc7201:agent.storage.AgentNFT
    struct AgentNFTStorage {
        // Token data
        mapping(uint256 => TokenData) tokens;
        mapping(address owner => mapping(address operator => bool)) operatorApprovals;
        mapping(address user => address accessAssistant) accessAssistants;
        uint256 nextTokenId;
        // Contract metadata
        string name;
        string symbol;
        string storageInfo;
        // Core components
        IERC7857DataVerifier verifier;
    }

    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    string public constant VERSION = "2.0.0";

    // keccak256(abi.encode(uint(keccak256("agent.storage.AgentNFT")) - 1)) & ~bytes32(uint(0xff))
    bytes32 private constant AGENT_NFT_STORAGE_LOCATION =
        0x4aa80aaafbe0e5fe3fe1aa97f3c1f8c65d61f96ef1aab2b448154f4e07594600;

    function _getAgentStorage()
        private
        pure
        returns (AgentNFTStorage storage $)
    {
        assembly {
            $.slot := AGENT_NFT_STORAGE_LOCATION
        }
    }

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(
        string memory name_,
        string memory symbol_,
        string memory storageInfo_,
        address verifierAddr,
        address admin_
    ) public virtual initializer {
        require(verifierAddr != address(0), "Zero address");

        __AccessControlEnumerable_init();

        _grantRole(DEFAULT_ADMIN_ROLE, admin_);
        _grantRole(ADMIN_ROLE, admin_);
        _grantRole(PAUSER_ROLE, admin_);

        AgentNFTStorage storage $ = _getAgentStorage();
        $.name = name_;
        $.symbol = symbol_;
        $.storageInfo = storageInfo_;
        $.verifier = IERC7857DataVerifier(verifierAddr);
    }

    // Basic getters
    function name() public view virtual returns (string memory) {
        return _getAgentStorage().name;
    }

    function symbol() public view virtual returns (string memory) {
        return _getAgentStorage().symbol;
    }

    function verifier() public view virtual returns (IERC7857DataVerifier) {
        return _getAgentStorage().verifier;
    }

    // Admin functions
    function updateVerifier(
        address newVerifier
    ) public virtual onlyRole(ADMIN_ROLE) {
        require(newVerifier != address(0), "Zero address");
        _getAgentStorage().verifier = IERC7857DataVerifier(newVerifier);
    }

    function update(
        uint256 tokenId,
        IntelligentData[] calldata newDatas
    ) public virtual {
        AgentNFTStorage storage $ = _getAgentStorage();
        TokenData storage token = $.tokens[tokenId];
        require(token.owner == msg.sender, "Not owner");
        require(newDatas.length > 0, "Empty data array");

        IntelligentData[] memory oldDatas = new IntelligentData[](
            token.iDatas.length
        );
        for (uint i = 0; i < token.iDatas.length; i++) {
            oldDatas[i] = token.iDatas[i];
        }

        delete token.iDatas;

        for (uint i = 0; i < newDatas.length; i++) {
            token.iDatas.push(newDatas[i]);
        }

        emit Updated(tokenId, oldDatas, newDatas);
    }

    function mint(
        IntelligentData[] calldata iDatas,
        address to
    ) public payable virtual returns (uint256 tokenId) {
        require(to != address(0), "Zero address");
        require(iDatas.length > 0, "Empty data array");

        AgentNFTStorage storage $ = _getAgentStorage();

        tokenId = $.nextTokenId++;
        TokenData storage newToken = $.tokens[tokenId];
        newToken.owner = to;
        newToken.approvedUser = address(0);

        for (uint i = 0; i < iDatas.length; i++) {
            newToken.iDatas.push(iDatas[i]);
        }

        emit Minted(tokenId, msg.sender, to);
    }

    function _proofCheck(
        address from,
        address to,
        uint256 tokenId,
        TransferValidityProof[] calldata proofs
    )
        internal
        returns (bytes[] memory sealedKeys, IntelligentData[] memory newDatas)
    {
        AgentNFTStorage storage $ = _getAgentStorage();
        require(to != address(0), "Zero address");
        require($.tokens[tokenId].owner == from, "Not owner");
        require(proofs.length > 0, "Empty proofs array");

        TransferValidityProofOutput[] memory proofOutput = $
            .verifier
            .verifyTransferValidity(proofs);

        require(
            proofOutput.length == $.tokens[tokenId].iDatas.length,
            "Proof count mismatch"
        );

        sealedKeys = new bytes[](proofOutput.length);
        newDatas = new IntelligentData[](proofOutput.length);

        for (uint i = 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"
            );

            bytes memory wantedKey = proofOutput[i].wantedKey;
            bytes memory encryptedPubKey = proofOutput[i].encryptedPubKey;
            if (wantedKey.length == 0) {
                // if the wanted key is empty, the default wanted receiver is receiver itself
                address defaultWantedReceiver = 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(
        address from,
        address to,
        uint256 tokenId,
        TransferValidityProof[] calldata proofs
    ) internal {
        AgentNFTStorage storage $ = _getAgentStorage();
        (
            bytes[] memory sealedKeys,
            IntelligentData[] memory newDatas
        ) = _proofCheck(from, to, tokenId, proofs);

        TokenData storage token = $.tokens[tokenId];
        token.owner = to;
        token.approvedUser = address(0);

        delete token.iDatas;
        for (uint i = 0; i < newDatas.length; i++) {
            token.iDatas.push(newDatas[i]);
        }

        emit Transferred(tokenId, from, to);
        emit PublishedSealedKey(to, tokenId, sealedKeys);
    }

    function iTransfer(
        address to,
        uint256 tokenId,
        TransferValidityProof[] calldata proofs
    ) public virtual {
        require(_isApprovedOrOwner(msg.sender, tokenId), "Not authorized");
        _transfer(ownerOf(tokenId), to, tokenId, proofs);
    }

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual {
        TokenData storage token = _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);

        emit Transferred(tokenId, from, to);
    }

    function iTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        TransferValidityProof[] calldata proofs
    ) public virtual {
        require(_isApprovedOrOwner(msg.sender, tokenId), "Not authorized");
        _transfer(from, to, tokenId, proofs);
    }

    function _clone(
        address from,
        address to,
        uint256 tokenId,
        TransferValidityProof[] calldata proofs
    ) internal returns (uint256) {
        AgentNFTStorage storage $ = _getAgentStorage();

        (
            bytes[] memory sealedKeys,
            IntelligentData[] memory newDatas
        ) = _proofCheck(from, to, tokenId, proofs);

        uint256 newTokenId = $.nextTokenId++;
        TokenData storage newToken = $.tokens[newTokenId];
        newToken.owner = to;
        newToken.approvedUser = address(0);

        for (uint i = 0; i < newDatas.length; i++) {
            newToken.iDatas.push(newDatas[i]);
        }

        emit Cloned(tokenId, newTokenId, from, to);
        emit PublishedSealedKey(to, newTokenId, sealedKeys);

        return newTokenId;
    }

    function iClone(
        address to,
        uint256 tokenId,
        TransferValidityProof[] calldata proofs
    ) public virtual returns (uint256) {
        require(_isApprovedOrOwner(msg.sender, tokenId), "Not authorized");
        return _clone(ownerOf(tokenId), to, tokenId, proofs);
    }

    function iCloneFrom(
        address from,
        address to,
        uint256 tokenId,
        TransferValidityProof[] calldata proofs
    ) public virtual returns (uint256) {
        require(_isApprovedOrOwner(msg.sender, tokenId), "Not authorized");
        return _clone(from, to, tokenId, proofs);
    }

    function authorizeUsage(uint256 tokenId, address to) public virtual {
        require(to != address(0), "Zero address");
        AgentNFTStorage storage $ = _getAgentStorage();
        require($.tokens[tokenId].owner == msg.sender, "Not owner");

        address[] storage authorizedUsers = $.tokens[tokenId].authorizedUsers;
        for (uint i = 0; i < authorizedUsers.length; i++) {
            require(authorizedUsers[i] != to, "Already authorized");
        }

        authorizedUsers.push(to);
        emit Authorization(msg.sender, to, tokenId);
    }

    function ownerOf(uint256 tokenId) public view virtual returns (address) {
        AgentNFTStorage storage $ = _getAgentStorage();
        address owner = $.tokens[tokenId].owner;
        require(owner != address(0), "Token does not exist");
        return owner;
    }

    function authorizedUsersOf(
        uint256 tokenId
    ) public view virtual returns (address[] memory) {
        AgentNFTStorage storage $ = _getAgentStorage();
        require(_exists(tokenId), "Token does not exist");
        return $.tokens[tokenId].authorizedUsers;
    }

    function storageInfo(
        uint256 tokenId
    ) public view virtual returns (string memory) {
        require(_exists(tokenId), "Token does not exist");
        return _getAgentStorage().storageInfo;
    }

    function _exists(uint256 tokenId) internal view returns (bool) {
        return _getAgentStorage().tokens[tokenId].owner != address(0);
    }

    function intelligentDataOf(
        uint256 tokenId
    ) public view virtual returns (IntelligentData[] memory) {
        AgentNFTStorage storage $ = _getAgentStorage();
        require(_exists(tokenId), "Token does not exist");
        return $.tokens[tokenId].iDatas;
    }

    function approve(address to, uint256 tokenId) public virtual {
        address owner = ownerOf(tokenId);
        require(to != owner, "Approval to current owner");
        require(
            msg.sender == owner || isApprovedForAll(owner, msg.sender),
            "Not authorized"
        );

        _getAgentStorage().tokens[tokenId].approvedUser = to;
        emit Approval(owner, to, tokenId);
    }

    function setApprovalForAll(address operator, bool approved) public virtual {
        require(operator != msg.sender, "Approve to caller");
        _getAgentStorage().operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function getApproved(
        uint256 tokenId
    ) public view virtual returns (address) {
        require(_exists(tokenId), "Token does not exist");
        return _getAgentStorage().tokens[tokenId].approvedUser;
    }

    function isApprovedForAll(
        address owner,
        address operator
    ) public view virtual returns (bool) {
        return _getAgentStorage().operatorApprovals[owner][operator];
    }

    function delegateAccess(address assistant) public virtual {
        require(assistant != address(0), "Zero address");
        _getAgentStorage().accessAssistants[msg.sender] = assistant;
        emit DelegateAccess(msg.sender, assistant);
    }

    function getDelegateAccess(
        address user
    ) public view virtual returns (address) {
        return _getAgentStorage().accessAssistants[user];
    }

    function _isApprovedOrOwner(
        address spender,
        uint256 tokenId
    ) internal view returns (bool) {
        require(_exists(tokenId), "Token does not exist");
        address owner = ownerOf(tokenId);
        return (spender == owner ||
            getApproved(tokenId) == spender ||
            isApprovedForAll(owner, spender));
    }

    function batchAuthorizeUsage(
        uint256 tokenId,
        address[] calldata users
    ) public virtual {
        require(users.length > 0, "Empty users array");
        AgentNFTStorage storage $ = _getAgentStorage();
        require($.tokens[tokenId].owner == msg.sender, "Not owner");

        for (uint i = 0; i < users.length; i++) {
            require(users[i] != address(0), "Zero address in users");
            $.tokens[tokenId].authorizedUsers.push(users[i]);
            emit Authorization(msg.sender, users[i], tokenId);
        }
    }

    function revokeAuthorization(uint256 tokenId, address user) public virtual {
        AgentNFTStorage storage $ = _getAgentStorage();
        require($.tokens[tokenId].owner == msg.sender, "Not owner");
        require(user != address(0), "Zero address");

        address[] storage authorizedUsers = $.tokens[tokenId].authorizedUsers;
        bool found = false;

        for (uint i = 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");
        emit AuthorizationRevoked(msg.sender, user, tokenId);
    }
}

Security Considerations

  1. 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
  2. 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
  3. 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
  4. 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

Copyright and related rights waived via CC0.