- October 16, 2025

OpenZeppelin Security

OpenZeppelin Security
Security Audits
-
Summary
Type: Layer 2 & Rollups
Timeline: September 10, 2025 → September 22, 2025
Languages: SolidityFindings
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 theverifyProof
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 configuredDcapAttestationRouter
to perform the actual verification. - Implements an
onlyAuthorized
modifier, restricting access to theverifyProof
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:
- Parses the quote to determine its version (v3, v4, or v5).
- If
toVerifyMr
is enabled, it parses the TEE type (SGX or TDX) and calls theMeasurementDao
to verify the TEE's measurements (e.g., MRENCLAVE, RTMR, and MRTD) against a trusted whitelist. - If
CacheOption
is enabled, it checks if the quote's attestation key is already initialized in theTEECacheVerifier
. If so, it uses the cache for a faster, on-chain verification. - If the key is not cached, it forwards the proof to the main
dcapAttestation
contract (the external library) for full, comprehensive verification. - 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
, andclearupAllKey
) 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'sissuedAt
andexpiredAt
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 parsesTD15ReportBody
structures and performs the necessary TCB and module checks.V5Structs
: A new contract defining the data structures (e.g.,TD15ReportBody
andECDSAQuoteV5AuthData
) 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 theMeasurementDao
with the correct and secure measurement values (MRENCLAVE
,MRSIGNER
,RTMRs
, andMRTD
s). 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 theMeasurementDao
. 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. TheDcapAttestationRouter
callsverifyAndAttestOnChain
on this external contract without passing anymsg.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) andTEECacheVerifier
contracts automatically authorize their owners in their constructors. Authorizations are subsequently used to restrict access to functions such asverifyProof
andinitializeCache
, which modify the contract's state. Notably, when ownership is transferred viatransferOwnership
, 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 externaldcapAttestation
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.
- configure all critical contract addresses, including the
- Authorized Callers allowlists in
TEEVerifierProxy
,DcapAttestationRouter
, andTEECacheVerifier
are configured by the owner. Corresponding functions gated by theonlyAuthorized
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 callverifyProof
. The authorized callers should at least include theRollup
contract.DcapAttestationRouter
: Authorized callers can callverifyProof
. The authorized callers should at least include theTEEVerifierProxy
contract.TEECacheVerifier
: Authorized callers can callverifyAndAttestOnChain
andinitializeCache
. The authorized callers should at least include theDcapAttestationRouter
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 BytesSet
s. 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
, theverifyProof
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
orTEECacheVerifier
contract, and update the corresponding address inDcapAttestationRouter
. This implies that themr
,rtmr
, andmrtdMap
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;
wherertmr[0][x] == true
indicates thatx
is in the first set ofrtmr
measurements. Note that, in order to make this secure and prevent keys from older versions from being used, an auxiliary variableuint256 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 coordinatecurrentVersionNumber
.
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, andTEECacheVerifier.sol
line 9,console
is imported but not used. - In
MeasurementDao.sol
line 7, theconsole
is commented out instead of being deleted. - In
TEECacheVerifier
, thedisableCallerRestriction
function is commented out but not deleted. - In
TEECacheVerifier
, in thedeleteKey
function, the check on the number of keys is commented out. - In the
verifyAndAttestOnChain
function ofTEECacheVerifier
, 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
andadd_mrtd
accept arbitrary-lengthbytes
instead of enforcing the expected 48‑byte SHA‑384 length.initializeCache
accepts arbitrary-lengthbytes
without enforcing the expected 64-byte ECDSA attestation key length.- None of
add_rtMr
,add_mrtd
,add_mr_enclave
, andinitializeCache
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
:
- The
setAuthorized
function - The
enableCallerRestriction
function - The
disableCallerRestriction
function - The
_setConfig
function - The
enableVerifyMRTD
function - The
disableVerifyMRTD
function
In MeasurementDao.sol
:
- The
add_mr_enclave
function - The
delete_mr_enclave
function - The
clearup_mr_enclave
function - The
add_rtMr
function - The
delete_rtMr
function - The
clearup_rtMr
function - The
add_mrtd
function - The
delete_mrtd
function - The
clearup_mrtd
function
In TEECacheVerifier.sol
:
- The
setAuthorized
function - The
enableCallerRestriction
function - The
initializeCache
function - The
deleteKey
function - The
clearupAllKey
function
In TEEVerifierProxy.sol
:
- The
setAuthorized
function - The
enableCallerRestriction
function - The
disableCallerRestriction
function - The
_setConfig
function
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:
- In
DcapAttestationRouter.sol
, the_authorized
state variable - In
DcapAttestationRouter.sol
, the_isCallerRestricted
state variable - In
TEECacheVerifier.sol
, the_authorized
state variable - In
TEECacheVerifier.sol
, the_isCallerRestricted
state variable - In
TEEVerifierProxy.sol
, the_authorized
state variable - In
TEEVerifierProxy.sol
, the_isCallerRestricted
state variable
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 usingdelegatecall
. 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 theowner
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 Rollup
→ TEEVerifierProxy
→ DcapAttestationRouter
→ (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 aDELEGATECALL
). - 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 theonlyAuthorized
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 fourTEEVerifierProxy
contracts (with two rollups pointing to the same proxy), which point to three differentDcapAttestationRouter
contracts, which point to two differentMeasurementDao
andTEECacheVerifier
contracts each. If, as appears to be the case in a normal transaction flow, only one contract is expected to be able to call theonlyAuthorized
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'sAccessControl
orAccessControlEnumerable
. - In the current design,
TEEVerifierProxy
only contains the address of theDcapAttestationRouter
instance and an allowlist for access control. Its only essential function directly forwards theverifyProof
call toDcapAttestationRouter
. The same result can be achieved by havingRollup
store the address of the correct instance ofDcapAttestationRouter
and callingDcapAttestationRouter
directly. The upgradeability is the same since the address inRollup
can be changed to point to the new router, when a new instance ofDcapAttestationRouter
is deployed. - Finally, forwarding calls via the
CALL
operation is not a standard proxy pattern. Typically, proxies for upgradeability use theDELEGATECALL
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
andTEECacheVerifier
is in all capitals, but inITeeRollupVerifier
, only the first T is capitalized, and inDcapAttestationRouter
, only the first D is capitalized. - The
CacheOption
storage variable inDcapAttestationRouter
is in CapWords style (a.k.a. Pascal case), whereas the other storage variables are in mixedCase (a.k.a. camel case). - The
mr
andrtmr
mappings are named without the "Map" suffix, whereasmrtdMap
is given the "Map" suffix. - The
add_rtMr
,delete_rtMr
,get_rtMr
, andclearup_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
, ormrtdMap
are of the formadd...
, but the function for adding an attestation key to the cache is namedinitializeCache
, and the function for checking the existence of a key in the cache is namedisInitialized
. This is confusing because the nameinitializeCache
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 toaddKey
andcontains
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
, andclearupAllKey
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
andclear_mrtd
). However, the names for the functionsgetAllKey
andclearupAllKey
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 togetCache
andclearCache
, 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.
Ready to secure your code?