Summary

Type: Layer 2 & Rollups
Timeline: September 10, 2025 → September 22, 2025
Languages: Solidity

Findings
Total issues: 22 (22 resolved)
Critical: 0 (0 resolved) · High: 0 (0 resolved) · Medium: 2 (2 resolved) · Low: 5 (5 resolved)

Notes & Additional Information
15 notes raised (15 resolved)

Scope

OpenZeppelin audited the jovaynetwork/jovay-contracts repository at commit 24f525f.

Following the conclusion of the fix review, the final post-audit commit is bdaf093.

In scope were the following files. The root of the tree is located at jovay-contracts/rollup_contracts/contracts/L1/tee_verifier/src:

 src
├── interfaces
│   └── ITeeRollupVerifier.sol
├── DcapAttestationRouter.sol
├── MeasurementDao.sol
├── TEECacheVerifier.sol
└── TEEVerifierProxy.sol

In addition, two patches were introduced: one adding verification for version 5 quotes (0001-support-v5-quote.patch) and another containing a crucial one-line fix for the PCCSRouter contract (0001-bugfix-collaterals-expiration-check.patch). A diff audit of V5QuoteVerifier.sol against V4QuoteVerifier.sol located in the same directory was performed, along with a diff audit of PCCSRouter.sol against the pre-patch version.

System Overview

The system under review provides an on-chain verification mechanism for TEE (Trusted Execution Environment) attestations, designed to integrate with a rollup architecture. Its primary function is to validate that a given data commitment was generated within a genuine and up-to-date Intel SGX or TDX enclave. This is achieved by processing attestation quotes through a series of smart contracts that check the quote's cryptographic validity and cross-reference its measurements against a trusted registry. The system is modular, separating the user-facing entry point, internal routing logic, gas-saving cache, measurement data, and the external verification library into distinct, interconnected components.

TEEVerifierProxy

The TEEVerifierProxy contract serves as the primary entry point for the verification system. It abstracts the underlying complexity of the attestation process from the end-user, which is typically another smart contract (in this system, the Rollup contract). Its state is limited to configuration and access control and it does not store any data related to the proofs themselves. Its main role is to forward the verification requests to the appropriate logic contract.

Core Functionality:

  • Receives an aggregated proof (aggrProof) for verification via the verifyProof function. Here, "proof" may mean a proof in the traditional sense, in the form of a non-interactive zero-knowledge (ZK) proof, or a TEE proof, also known as a quote. In later versions of this system, ZK proofs will be supported. Currently, however, only TEE proofs are supported. A TEE proof establishes a commitment to public data, whose computation is attested to by a P-256 ECDSA signature generated using the private key of a trusted execution environment with verified measurements.
  • Forwards the verifyProof call to the configured DcapAttestationRouter to perform the actual verification.
  • Implements an onlyAuthorized modifier, restricting access to the verifyProof function to whitelisted callers managed by the contract owner.

DcapAttestationRouter

This contract is the central hub of the verification logic. It orchestrates the attestation process by routing the proof to the appropriate components based on its configuration and the properties of the proof itself. It determines whether to perform a full verification via the external automata-dcap-attestation library or to use the internal TEECacheVerifier for an expedited check.

Procedural Flow:

  1. Parses the quote to determine its version (v3, v4, or v5).
  2. If toVerifyMr is enabled, it parses the TEE type (SGX or TDX) and calls the MeasurementDao to verify the TEE's measurements (e.g., MRENCLAVE, RTMR, and MRTD) against a trusted whitelist.
  3. If CacheOption is enabled, it checks if the quote's attestation key is already initialized in the TEECacheVerifier. If so, it uses the cache for a faster, on-chain verification.
  4. If the key is not cached, it forwards the proof to the main dcapAttestation contract (the external library) for full, comprehensive verification.
  5. Upon successful verification, it extracts the 32-byte data commitment from the quote's user data field at a version-specific offset.

TEECacheVerifier

The TEECacheVerifier contract provides an on-chain mechanism for verifying quotes, designed to optimize gas costs and reduce reliance on the more complex external dcapAttestation contract. It works by caching previously verified attestation keys. If a quote is signed by a key that has already been fully validated and cached, this contract can verify it on-chain by checking the ECDSA signature against the quote's header and body.

Key Features:

  • Maintains a cache of trusted ECDSA attestation keys in a mapping and an array
  • Provides verifyAndAttestOnChain to perform on-chain ECDSA signature verification for quotes signed by a cached key, supporting v3, v4, and v5 quote formats
  • Includes owner-restricted administrative functions to manage the cache (initializeCache, deleteKey, and clearupAllKey) and authorized callers

MeasurementDao

The MeasurementDao is a stateful contract that acts as an allowlist registry for trusted TEE measurements. It stores and manages cryptographic identifiers corresponding to approved enclave software. The DcapAttestationRouter contract queries this contract to ensure that a quote originates from an authorized and secure application.

Managed Measurements:

  • MRENCLAVE and MRSIGNER: For SGX enclaves, it stores pairs of enclave and signer measurements.
  • RTMR: For TDX enclaves, it stores a list of trusted runtime measurements.
  • MRTD: For TDX enclaves, it stores a list of trusted metadata measurements.

Automata DCAP Attestation Dependency

This external set of contracts, included as a dependency, performs the heavy lifting of full attestation verification. It is responsible for parsing the complex structures of different quote versions, verifying cryptographic signatures, and validating the entire certificate chain for the attestation collaterals against data stored in a separate on-chain PCCS system. The scope includes a patch that adds support for Quote v5, and another patch that fixes a critical bug in collateral validation.

Key Components:

  • PCCSRouter: Manages and verifies the chain of trust for Platform Certificate and Key Services (PCKS) collaterals by fetching them from various Data Access Object contracts. A critical bug-fix patch was applied to this contract to ensure it correctly checks that the verification timestamp is within the collateral's issuedAt and expiredAt dates.
  • V5QuoteVerifier: A new contract introduced via a patch to handle the specific structure and verification logic for the Intel TDX Quote Version 5 format. It parses TD15ReportBody structures and performs the necessary TCB and module checks.
  • V5Structs: A new contract defining the data structures (e.g., TD15ReportBody and ECDSAQuoteV5AuthData) necessary to parse and handle the version 5 quote format on-chain.

Security Model and Trust Assumptions

The system's security is founded on the cryptographic guarantees of Intel's DCAP attestation protocol, the integrity of the bundled Automata Network automata-dcap-attestation contracts, and the proper administrative management of the on-chain components. It assumes that a secure and trusted off-chain environment generates the proofs, and the on-chain system's role is solely to verify the integrity of those proofs. The security model does not extend to the validity of the data within the commitment itself, only to the authenticity of its origin.

  • External Dependency Integrity: The system's core verification logic depends entirely on the correctness and security of the bundled automata-dcap-attestation library. Any vulnerability in this dependency will compromise the entire verification process.
  • Correct Measurement Configuration: The security of the system is critically dependent on the owner populating the MeasurementDao with the correct and secure measurement values (MRENCLAVE, MRSIGNER, RTMRs, and MRTDs). Failure to do so could lead to the system trusting proofs from insecure or malicious enclaves.
  • Open-Source and Verifiable Enclave Software: The system assumes that the source code for the application running inside the TEE enclave is public and open-source. This allows any external observer to independently compile the code, reproduce the enclave measurements (e.g., MRENCLAVE, MRSIGNER), and verify that they match the trusted values registered on-chain in the MeasurementDao. Without this transparency, users must blindly trust that the whitelisted measurements correspond to secure and non-malicious application logic.
  • Intel DCAP Security: The entire model is built upon the assumption that Intel's DCAP protocol and its underlying cryptographic primitives are secure and have not been compromised.
  • External Dependency Integrity: TDX servers are maintained by the cloud service provider, which is assumed not to behave maliciously (e.g., by shutting down all the verifiers).
  • Key Revocation by Owner: It is assumed that when a TCB is revoked due to security concerns, the owner will promptly remove the related cached keys in TEECacheVerifier.
  • Attestation Fee Configuration: A critical operational assumption is that the external dcapAttestation contract is configured with its fee (_feeBP) set to zero. The DcapAttestationRouter calls verifyAndAttestOnChain on this external contract without passing any msg.value, meaning the call will fail if the external contract expects a fee to be paid.
  • Secure Runtime and Key Management: It is assumed that the owner key is stored securely and that all administrative actions are performed in a secure environment, free from malware or phishing threats that could lead to key compromise.
  • Automatic Owner Authorization: The TEEVerifierProxy (removed) and TEECacheVerifier contracts automatically authorize their owners in their constructors. Authorizations are subsequently used to restrict access to functions such as verifyProof and initializeCache, which modify the contract's state. Notably, when ownership is transferred via transferOwnership, the previous owner’s authorization remains. This is the intended design and the previous owner is assumed to not act maliciously.
  • Attestation Quote Version: In production, only version 5 quotes will be used for attestation verification. The handling logic for version 3 and 4 quotes is now redundant and will be removed from the codebase in the future.

Privileged Roles

The following privileged roles were identified in the system under review:

  • Owner: A single owner role holds significant and centralized power across the entire system. This owner is fully trusted to:
    • configure all critical contract addresses, including the DcapAttestationRouter, MeasurementDao, TEECacheVerifier, and the external dcapAttestation contract.
    • manage authorized callers that are permitted to submit proofs for verification.
    • add, remove, and clear all trusted TEE measurements in the MeasurementDao contract. An attacker who compromises the owner key can whitelist malicious TEE software, thereby breaking the entire system's trust model.
    • manage the attestation key cache in TEECacheVerifier, including the ability to add, delete, or clear keys.
  • Authorized Callers allowlists in TEEVerifierProxy, DcapAttestationRouter, and TEECacheVerifier are configured by the owner. Corresponding functions gated by the onlyAuthorized modifier are only allowed to be called by authorized callers. The owner also has the ability to enable or disable caller restriction: disabling the caller restriction allows these functions to be called publicly. The list of authorized callers, and what their authorization allows them to do, differs in each contract.
    • TEEVerifierProxy: Authorized callers can call verifyProof. The authorized callers should at least include the Rollup contract.
    • DcapAttestationRouter: Authorized callers can call verifyProof. The authorized callers should at least include the TEEVerifierProxy contract.
    • TEECacheVerifier: Authorized callers can call verifyAndAttestOnChain and initializeCache. The authorized callers should at least include the DcapAttestationRouter contract.
 

Medium Severity

Incorrect Commitment Extraction for SGX V4 Quotes

The DcapAttestationRouter contract is designed to verify attestation proofs from different Trusted Execution Environments (TEEs) and quote versions. The verification logic relies on parsing a serialized data structure returned by an attestation service to extract a security commitment, which corresponds to the reportData field in the original TEE report. The contract uses a set of predefined constants (USER_DATA_V3_OFFSET, USER_DATA_V4_OFFSET, and USER_DATA_V5_OFFSET) to locate this commitment based on the quote version.

However, the implementation does not correctly handle all valid permutations of version 4 quotes. While version 4 quotes can be generated for both SGX and TDX enclaves, the underlying report structure and the offset of the reportData field differ between the two. The _verifyProof function checks the quote version but fails to differentiate by TEE type. For any v4 quote, it unconditionally uses the USER_DATA_V4_OFFSET value, which is calculated for a TDX report. When the contract processes a v4 quote from an SGX enclave, it applies this incorrect offset, causing it to read from the wrong location in the serialized data and failing to extract the valid commitment.

Consider modifying the _verifyProof function to ensure that it correctly distinguishes between TEE types for version 4 quotes. The logic should be updated to read the TEE type from the quote header and use a conditional check. When the quoteVersion is 4, this check should apply the USER_DATA_V4_OFFSET for a TDX quote but apply the correct, distinct offset required for an SGX quote.

Update: Resolved in pull request #9 at commit 3fac035. The team states that only v5 quotes will be used, and the code for handling v3/v4 quotes is kept now for compatibility but will be removed later.

Potential DoS from O(n) Deletion Functions

The owner-only deletion functions delete_mr_enclave, delete_rtMr, anddelete_mrtd, and the authorized-only deleteKey function linearly scan their respective arrays to locate items before removal, resulting in O(n) complexity. With large lists, these operations risk exceeding block gas limits, leading to out-of-gas failures and potential DoS of administrative maintenance. OpenZeppelin's EnumerableMap and EnumerableSet structs perform deletions, as well as additions and existence checking, in O(1) time. In addition, they encompass both the mapping and accompanying list in one object, which would simplify the code.

Consider importing EnumerableMap and EnumerableSet, implementing mr as a Bytes32ToBytes32Map, and implementing rtmr, mrtdMap, and _verificationCache as BytesSets. Alternatively, consider recording item index in mappings at insertion time to enable O(1) deletions, or enforcing an upper bound on list sizes to keep worst-case gas within safe limits.

Update: Resolved in pull request #11 at commit eddd42d. The Jovay team added four index mappings (mrEnclaveIndex, rtmrIndex, mrtdIndex, _keyIndex) to store the list positions of added items.

Low Severity

Missing Validation for TDX Type in _verifyMeasurement

The verifyProof function is designed to support both SGX and TDX types. During the verification, the _verifyMeasurement function reads a 4-byte teeType from the quote header offset 4 and routes to SGX or TDX measurement verification accordingly. Especially, when the teeType is SGX_TEE, verifyMeasurementSGX will be called. Otherwise, verifyMeasurementTDX will be called. However, even if teeType is not TDX_TEE (e.g., a corrupted quote), verifyMeasurementTDX will still be called, which may lead to unexpected behavior.

Consider adding a check to ensure that teeType is either SGX_TEE or TDX_TEE. Alternatively, consider returning false or reverting the transaction.

Update: Resolved in pull request #25 at commit 558d082, and pull request #50 at commit 52d1cf2.

Missing or Incomplete Docstrings

Throughout the codebase, multiple instances of missing docstrings were identified:

  • In DcapAttestationRouter.sol, none of the constants, state variables, and functions have docstrings.
  • In MeasurementDao.sol, none of the constants and state variables as well as most of the functions do not have docstrings.
  • In TEECacheVerifier.sol, none of the constants, state variables, and functions have docstrings.
  • In TEEVerifierProxy.sol, none of the state variables and functions have docstrings.
  • In ITeeRollupVerifier.sol, the verifyProof function has an incomplete docstring. The return values are not documented.

Where there is documentation, it is often sparse and does not provide enough information for the reader to understand the code. For example, the docstrings for the add_rtMr and delete_rtMr functions mention the parameter rtmr3 but do not explain its meaning.

Consider thoroughly documenting all functions (and their parameters) that are part of any contract's public API. Functions implementing sensitive functionality, even if not public, should be clearly documented as well. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).

Update: Resolved in pull request #27 at commits 55210fc and 973ac1c.

Outdated Solidity Versions and Floating Pragma

Using an outdated version of Solidity can lead to vulnerabilities and unexpected behavior in contracts. It is important to keep the Solidity compiler version up to date. In addition, floating pragma directives were observed throughout the codebase. Pragma directives should be fixed to clearly identify the Solidity version with which the contracts will be compiled. However, throughout the codebase, the files in scope are using an outdated Solidity version with a floating pragma (^0.8.0).

Consider taking advantage of the latest Solidity version to improve the overall readability and security of the codebase. Regardless of which version of Solidity is used, consider pinning the version consistently throughout the codebase using a fixed pragma directive in order to prevent bugs due to incompatible future releases.

Update: Resolved in pull request #26 at commit 9c22490.

Possible Out-of-Gas Error from O(n) Clearing of Measurements and Cache

The functions for clearing mappings, clearup_mr_enclave, clearup_rtMr, clearup_mrtd, and clearupAllKey iterate through each element of the corresponding list, running in O(n) time. If the lists grow too large, this operation can exceed the block gas limit and revert. This can prevent the measurement or cache clearing, blocking a desired administrative function. In clearupAllKey, the fact that the _initializedKeys list contains at most 10,000 keys is meant to mitigate this issue. However, at 2,900 gas per deletion, clearing 10,000 keys would cost 29 million gas, which exceeds the block gas limit of Ethereum. The measurement lists have no length restrictions.

Erasing the stored memory of each entry of a list will necessarily take O(n) time: OpenZeppelin's EnumerableMap and EnumerableSet, for example, have O(n) clear functions built in. If clearing the entire list at once is never needed, or there is reason to believe that the lists will never grow too large, this may be acceptable: it is recommended to document the rationale in this case. However, if this functionality is necessary and the lists may grow large in practice, there are alternative methods that sidestep the entire issue by not actually clearing the memory, some examples of which are provided below. Note that these are given in terms of mappings and lists, but each of these options can be implemented mutatis mutandis using EnumerableMap and EnumerableSet structs:

  • Enforce an upper bound on the size of each of the measurement lists, and a smaller upper bound on the size of the _initializedKeys list (e.g., 1000 items).
  • Implement functionality for clearing entries of the mappings and their corresponding lists in batches, of a size small enough that an out-of-gas error will not happen.
  • To clear the measurement lists or cache, re-deploy a new instance of the MeasurementDao or TEECacheVerifier contract, and update the corresponding address in DcapAttestationRouter. This implies that the mr, rtmr, and mrtdMap mappings and their corresponding lists would all be cleared at the same time: to clear only one of them, entries of the other two would have to be manually added to the new contract.
  • Store measurements or keys as a mapping of mappings and lists of lists, the first coordinate of which is the "version number" of the mapping or list. When you want to clear the previous mapping-list pair, increment the "version number" to get a new, empty mapping, then disable getting measurements or keys from mappings with a lower version number. For example, mapping(uint256 version => mapping(bytes measurement => bool isApproved)) rtmr; where rtmr[0][x] == true indicates that x is in the first set of rtmr measurements. Note that, in order to make this secure and prevent keys from older versions from being used, an auxiliary variable uint256 currentVersionNumber would have to be created, and measurements or keys would need to be added, deleted, or checked for existence only from mappings or lists with first coordinate currentVersionNumber.

Consider implementing one of the above-listed options to guarantee the owner's ability to clear each list.

Update: Resolved in pull request #27 at commits eac3d4d and 95d61f8.

Incorrect Empty Check in _loadDataIfNotExpired

The collateral validity gate in PCCSRouter._loadDataIfNotExpired relies on the pcsDaoAddr’s getCollateralValidity function to return validity expiration bounds. It only guards on non-empty return data (ret.length > 0) and upon the successful decoding of two variables (uint64 issuedAt and uint64 expiredAt). However, the getCollateralValidity function will always return two named uint64 values that default to zero when the validity slot is missing (see getCollateralValidity and _loadPcsValidity), which causes the ret.length > 0 check to always succeed. The current check can still treat missing collaterals as valid when callers supply a zero timestamp to getCertHashWithTimestamp or getCrlHashWithTimestamp, leading to unexpected results.

Consider aligning with the upstream implementation by requiring both decoded values to be non‑zero values (issuedAt != 0 && expiredAt != 0) and then applying the range check.

Update: Resolved in pull request #27 at commit 6b1a05a.

Notes & Additional Information

Missing SPDX License Identifiers

Both DcapAttestationRouter.sol and TEEVerifierProxy.sol lack an SPDX License Identifier, whereas other contracts in the repository include it. This omission introduces ambiguity about the intended license, and can create compliance and distribution risks for downstream users and integrations.

Consider adding // SPDX-License-Identifier: MIT as the first line in both files to align with the repository standard and eliminate ambiguity.

Update: Resolved in pull request #28 at commit b707359 and pull request #55 at commit a6c6386.

Unused Imports and Code

Throughout the codebase, multiple instances of unused imports and code were identified:

  • In DcapAttestationRouter.sol line 4, TEEVerifierProxy.sol line 4, and TEECacheVerifier.sol line 9, console is imported but not used.
  • In MeasurementDao.sol line 7, the console is commented out instead of being deleted.
  • In TEECacheVerifier, the disableCallerRestriction function is commented out but not deleted.
  • In TEECacheVerifier, in the deleteKey function, the check on the number of keys is commented out.
  • In the verifyAndAttestOnChain function of TEECacheVerifier, the version is passed in as an argument, but there is commented-out code that extracts the version from the raw quote.
  • In V5QuoteVerifier, the _verifySGXQuote function is never used.

To improve code quality, readability, and maintainability, consider removing all unused imports and code.

Update: Resolved in pull request #29.

Missing Length and Non‑Zero Input Validation in MeasurementDAO and TEECacheVerifier

The owner-only registration functions add_mr_enclave, add_rtMr, add_mrtd, and the authorized-only registration function initializeCache, do not validate parameters completely:

  • add_rtMr and add_mrtd accept arbitrary-length bytes instead of enforcing the expected 48‑byte SHA‑384 length.
  • initializeCache accepts arbitrary-length bytes without enforcing the expected 64-byte ECDSA attestation key length.
  • None of add_rtMr, add_mrtd, add_mr_enclave, and initializeCache reject zero values.

When the owner or an authorized user accidentally registers incorrect-length or zero values, subsequent verification fails, and unnecessary gas costs are incurred.

Consider enforcing strict parameter validation in the aforementioned functions by checking input length and value.

Update: Resolved in pull request #31 at commit 81278fa.

Unnecessary External Call When Cache Is Disabled

The _verifyProof function of the DcapAttestationRouter contract calls CacheAttestation.parseAttestationKey before checking whether caching is enabled. If CacheOption == false, then this call is unnecessary and wastes gas.

Consider fetching CacheOption into a local variable and checking it before calling CacheAttestation.parseAttestationKey.

Update: Resolved in pull request #49 at commits 2bde615 and 679b35d.

Functions Updating State Without Event Emissions

Throughout the codebase, multiple instances of functions updating the state without an event emission were identified:

In DcapAttestationRouter.sol:

In MeasurementDao.sol:

In TEECacheVerifier.sol:

In TEEVerifierProxy.sol:

Consider emitting events whenever there are state changes to improve the clarity of the codebase and make it less error-prone.

Update: Resolved in pull request #27 at commit d6b881d.

State Variable Visibility Not Explicitly Declared

Throughout the codebase, multiple instances of state variables lacking an explicitly declared visibility were identified:

For improved code clarity, consider always explicitly declaring the visibility of state variables, even when the default visibility matches the intended visibility.

Update: Resolved in pull request #33 at commit 7fee799.

Missing Security Contact

Providing a specific security contact (such as an email address or ENS name) within a smart contract significantly simplifies the process for individuals to communicate if they identify a vulnerability in the code. This practice is quite beneficial as it permits the code owners to dictate the communication channel for vulnerability disclosure, eliminating the risk of miscommunication or failure to report due to a lack of knowledge on how to do so. In addition, if the contract incorporates third-party libraries and a bug surfaces in those, it becomes easier for their maintainers to contact the appropriate person about the problem and provide mitigation instructions.

None of the contracts in scope have a security contact.

Consider adding a NatSpec comment containing a security contact above each contract definition. Using the @custom:security-contact convention is recommended as it has been adopted by the OpenZeppelin Wizard and the ethereum-lists.

Update: Resolved in pull request #27 at commit 8111e7c.

Custom Errors in require Statements

Since Solidity version 0.8.26, custom error support has been added to require statements. Initially, this feature was only available through the IR pipeline. However, Solidity version 0.8.27 extended support for this feature to the legacy pipeline as well.

Throughout the codebase, multiple instances of if-revert statements were identified that could be replaced with require statements:

In DcapAttestationRouter.sol:

In MeasurementDao.sol:

In TEECacheVerifier.sol:

In TEEVerifierProxy.sol:

For conciseness, consider replacing if-revert statements with require statements.

Update: Resolved in pull request #27 at commit 973ac1c7.

NatSpec Title Mismatch

In ITeeRollupVerifier.sol, in the NatSpec for the interface, the title is declared as IRollupVerifier, whereas the interface itself is ITeeRollupVerifier.

For improved consistency, consider changing the title to match the name of the contract.

Update: Resolved in pull request #27 at commit b2e9947.

Access-Control Logic Manually Duplicated Across Contracts

The TEEVerifierProxy, DcapAttestationRouter, and TEECacheVerifier contracts all have identical storage variables (_authorized and _isCallerRestricted) and identical implementations of the onlyAuthorized modifier (functions setAuthorized, enableCallerRestriction, and disableCallerRestriction). Instead of manually duplicating the code for access-control logic, the code could be simplified using inheritance.

Consider importing and inheriting OpenZeppelin's AccessControl or AccessControlEnumerable to securely implement role-based authorization and simplify the codebase. Alternatively, consider creating a separate contract implementing the desired authorization functionality and have all three contracts inherit that.

Update: Resolved in pull request #52 at commits 6883ee3 and 92b5fa6.

Contract Naming Suggestions

In smart contract development, contract names should clearly and accurately reflect their functionality and governance structure. Using established terms for patterns like proxies or DAOs helps align developer and auditor expectations with the contract's actual behavior. Misleading names can cause fundamental misunderstandings about a system's architecture and trust assumptions.

Two contracts in the system have names that are inconsistent with their underlying implementation, creating a potential for confusion:

  • The TEEVerifierProxy contract is named as a "Proxy," a term that strongly implies an upgradeability pattern using delegatecall. However, the contract only forwards calls via a standard external call and does not provide any upgradeability mechanism.
  • The MeasurementDao contract uses the "DAO" suffix. This is misleading as "DAO" in the blockchain space refers to a Decentralized Autonomous Organization, which implies community governance. While the name may be intended to signify a "Data Access Object," this is not a standard convention in Solidity and creates ambiguity. Furthermore, although the owner could be a DAO's governance contract, this is not apparent from the code itself, making the name unclear regardless of the intended meaning.

To ensure that the names accurately represent the contracts' roles and avoid confusion, consider renaming them. For example, TEEVerifierProxy could be renamed to TEEVerifierForwarder to better describe its forwarding pattern, and MeasurementDao could be renamed to MeasurementRegistry to clarify that it functions as a centrally-controlled list of approved values.

Update: Resolved in pull request #52 at commit 049401e.

Non-Standard Proxy Architecture

The flow of information through the smart contracts in the current architecture, beginning with a call to Rollup.verifyBatch, is RollupTEEVerifierProxyDcapAttestationRouter → (MeasurementDao, TEECacheVerifier, or AutomataDcapAttestationFee). In this flow:

  • Each contract stores the address of the next.
  • Each call in the chain is a CALL operation (as opposed to a DELEGATECALL).
  • Each contract after Rollup has an _authorized mapping. This is an allowlist that determines which contracts may call its essential functions, each of which is given the onlyAuthorized modifier.

While this architecture does not raise any particular security concerns, it is non-standard and possibly overly complex. Depending on the design goals of the system, it may be considerably simplified:

  • At each step, there is only one address for the next contract in the chain, whereas the whitelist indicating possible callers from the previous contract in the chain may contain multiple addresses. Therefore, this design allows for a many-to-one mapping of caller contracts to called contracts at each step. For example, in theory, five different Rollup contracts may send calls to four TEEVerifierProxy contracts (with two rollups pointing to the same proxy), which point to three different DcapAttestationRouter contracts, which point to two different MeasurementDao and TEECacheVerifier contracts each. If, as appears to be the case in a normal transaction flow, only one contract is expected to be able to call the onlyAuthorized functions, the _authorized whitelist could be replaced with a single address. This would yield a one-to-one mapping of callers to callees, making the chain of calls easier to determine. Note that, if desired, either design may be implemented in conjunction with a role-based access control system such as OpenZeppelin's AccessControl or AccessControlEnumerable.
  • In the current design, TEEVerifierProxy only contains the address of the DcapAttestationRouter instance and an allowlist for access control. Its only essential function directly forwards the verifyProof call to DcapAttestationRouter. The same result can be achieved by having Rollup store the address of the correct instance of DcapAttestationRouter and calling DcapAttestationRouter directly. The upgradeability is the same since the address in Rollup can be changed to point to the new router, when a new instance of DcapAttestationRouter is deployed.
  • Finally, forwarding calls via the CALL operation is not a standard proxy pattern. Typically, proxies for upgradeability use the DELEGATECALL operation to maintain their own storage while operating functions of the callee. Currently, the recommended proxy pattern in most cases is the Universal Upgradeable Proxy Standard (UUPS) pattern.

Consider simplifying the architecture design and/or implementing DcapAttestationRouter as a UUPSUpgradeable contract, while the TEEVerifierProxy can be removed and replaced with a standard ERC1967Proxy. The implementation of upgradeability requires careful review with respect to initialization and storage layout.

Update: Resolved in pull request #61 at commit fb0cb8f. The Jovay team has disregarded the proxy design and removed the TEEVerifierPorxy contract. The authorization allow list mechanism has been kept, and the logic has been moved to AccessControl.sol in pull request #52 at commit 6883ee3.

Inconsistent Formatting and Naming

Throughout the codebase, multiple instances of inconsistent formatting and naming were identified:

  • The "TEE" in TEEVerifierProxy and TEECacheVerifier is in all capitals, but in ITeeRollupVerifier, only the first T is capitalized, and in DcapAttestationRouter, only the first D is capitalized.
  • The CacheOption storage variable in DcapAttestationRouter is in CapWords style (a.k.a. Pascal case), whereas the other storage variables are in mixedCase (a.k.a. camel case).
  • The mr and rtmr mappings are named without the "Map" suffix, whereas mrtdMap is given the "Map" suffix.
  • The add_rtMr, delete_rtMr, get_rtMr, and clearup_rtMr functions only capitalize the letter "M".
  • Most functions in the MeasurementDao contract are written in snake case, whereas functions in Solidity are written in mixedCase.
  • The functions for adding measurements to mr, rtmr, or mrtdMap are of the form add..., but the function for adding an attestation key to the cache is named initializeCache, and the function for checking the existence of a key in the cache is named isInitialized. This is confusing because the name initializeCache indicates that the cache itself is being initialized, ideally a one-time operation: correspondingly, isInitialized implies that the function is checking to see if the cache itself has been initialized. For improved clarity, consider renaming these functions to addKey and contains or something similar, respectively.
  • The notice in ITeeRollupVerifier states "Verify zk proof", whereas the TEE verifier actually only verifies TEE proofs, and ZK proofs are currently unsupported.
  • In the clearup_mr_enclave, clearup_rtMr, clearup_mrtd, and clearupAllKey functions, "up" should be removed from the name: "clear up" means to clarify or explain, while "clear" means to empty.
  • In MeasurementsDao, the functions for getting and clearing mappings and lists correctly refer to the entire list as the object being retrieved or cleared (for example, get_mrtd and clear_mrtd). However, the names for the functions getAllKey and clearupAllKey refer to the type of item in the list (key), rather than the list itself. Since the cache itself is being retrieved or cleared, consider renaming these functions to getCache and clearCache, respectively.

Consider adhering to the Solidity style guide for formatting and establishing a consistent naming convention for functions and variables throughout the codebase.

Update: Resolved in pull request #51 and pull request #52.

Missing Existence Check in initializeCache

In the initializeCache function, there is no validation to confirm that the input key is not already in the _verificationCache mapping before it is added to the mapping and the _initializedKeys list. Currently, the _verifyProof function in the DcapAttestationRouter contract ensures that the key is not already added before invoking the initializeCache function. However, if other authorized users can directly call the initializeCache function, this could lead to unintended duplicate keys in the _initializedKeys list. This does not constitute a security vulnerability, as the deleteKey function removes all duplicate entries, ensuring that deleted keys do not persist. Nevertheless, this oversight temporarily permits duplicate entries and leads to unnecessary gas consumption.

Consider implementing a check to verify whether the key already exists in the _verificationCache before adding it to the _verificationCache mapping and the _initializedKeys list.

Update: Resolved in pull request #51 at commit f7b259b.

Missing Quote Length Check in Measurement Verification

In the verifyMeasurementSGX, verifyMeasurementTDX, and verifyMRTD functions of MeasurementDao, and in the _verifyMeasurement function of DcapAttestationRouter, the substring method is called on the input quote to obtain mrEnclave, mrSigner, rtmr3, mrtd, and teeType. If the quote is not long enough when substring is called, the call will revert without error message. This may cause unexpected behavior, since in each of these functions the caller expects a boolean value. To adhere to the verification function's contract, if the quote isn't long enough, it should just return false.

Consider checking that the quote is long enough before calling substring to avoid unexpected reversions.

Update: Resolved in pull request #51 at commit 92cc621.

Conclusion

The audited scope covered a system for on-chain verification of TEE quotes. In this system, TEE verification is applied to the verification of state transitions in an L2 rollup and leverages the Automata DCAP Attestation library. The architecture is modular, with a clear separation of responsibilities across routing, proof verification, measurement whitelisting, and attestation key caching. Recent patches also introduced support for version 5 quote verification, as well as an important fix to the PCCS router.

Overall, the system correctly parses and verifies most quotes. During the review, it was identified that V4 SGX quotes were not being processed as intended. Some inefficiencies were also noted that, in certain edge cases, could create risks of accidental denial-of-service in administrative functions. In addition, the system’s security model rests on strong trust assumptions about the owner and authorized callers in each contract, underscoring the importance of carefully managing and safeguarding these roles. To further enhance the system, recommendations aimed at improving efficiency, clarity, and code standardization were also provided.

Overall, the codebase was found to be well-structured and thoughtfully designed. The Jovay team is appreciated for their responsiveness and in-depth technical engagement throughout the audit.