Summary

Type: DeFi
Timeline: August 6, 2025 → August 11, 2025
Languages: Solidity

Findings
Total issues: 24 (20 resolved, 1 partially resolved)
Critical: 0 (0 resolved) · High: 0 (0 resolved) · Medium: 2 (2 resolved) · Low: 6 (5 resolved, 1 partially resolved)

Notes & Additional Information
16 notes raised (13 resolved)

Scope

OpenZeppelin audited the flashbots/flashtestations repository in two phases. Phase 1 was the main audit which targeted commit 9ce1371 and Phase 2 consisted of some gas optimization additions and was reviewed at commit 0140167 of PR #24. In addition to the smart contracts, the audit also covered the associated deployment and interaction scripts.

In scope of both the phases were the following files:

 ├── src
│   ├── BlockBuilderPolicy.sol
│   ├── FlashtestationRegistry.sol
│   ├── utils
│   │   └── QuoteParser.sol
│   └── interfaces
│       ├── IAttestation.sol
│       └── IFlashtestationRegistry.sol
└── script
    ├── BlockBuilderPolicy.s.sol
    ├── FlashtestationRegistry.s.sol
    └── Interactions.s.sol

System Overview

Flashtestations is a transparent, on-chain protocol for Trusted Execution Environment (TEE) verification and Intel DCAP attestation. It enables any TDX (Intel Trust Domain Extensions) device to verifiably prove its outputs on-chain. The primary use case is to demonstrate that blocks on the Unichain network have been constructed using fair and transparent ordering rules.

By integrating verifiable TEE attestations into block production, the protocol enhances transparency, enforces priority ordering, mitigates Maximal Extractable Value (MEV), and enables features such as revert protection. These improvements contribute to Unichain’s ongoing efforts to build a faster, fairer, and more decentralized blockchain infrastructure.

Core Components

The system comprises two primary smart contracts:

  • FlashtestationRegistry: Manages TEE identities and configuration data through Automata’s Intel DCAP attestation. It allows anyone to register or invalidate TEE services and to query their attestation status. This contract is upgradeable and designed to be largely permissionless, except for the upgrade process itself.

  • BlockBuilderPolicy: Defines and manages sets of workload IDs that determine valid configurations for specific remote block-building use cases. A workload ID uniquely identifies a TEE workload, which represents a specific version of an application’s code, derived from the TEE's measurement registers. These registers capture cryptographic hashes of the code, data, and configuration loaded into the TEE, ensuring that the workload is reproducible from source. The policy is upgradeable and tightly governed, requiring permissions to modify workloads. Proof verification is available to any user, but trust assumptions are made on blockContentHash due to EVM limitations (no retrospection).

Security Model and Trust Assumptions

The following trust assumptions and security risks were identified during the audit:

FlashtestationRegistry

  • This contract is upgradeable and largely permissionless. Any participant can register TEE services, invalidate existing attestations, or query the status of TEEs.
  • The only permissioned action is contract upgrading, which results in a central point of control.
  • The security of the system relies heavily on the correctness of the attestation contract (maintained by Automata Network), particularly in its ability to accurately verify raw quotes.
  • There is implicit trust in external actors to proactively call the invalidateAttestation function when an attestation becomes invalid. While the function is publicly accessible, failure to act promptly can result in incorrect TEE states being stored and trusted within the registry.

BlockBuilderPolicy

  • This contract is upgradeable and mostly permissioned. Only governance can modify workload IDs or upgrade the contract.
  • The only permissionless function is the verification of block builder proofs.
  • A critical trust assumption is placed on the user-provided inputs, particularly version and blockContentHash, which are not validated on-chain due to EVM limitations.
  • The Flashtestations protocol specifications are secure.
 

Medium Severity

Fee Incompatibility With Attestation Contract

Found in Phase 1

When the FlashtestationRegistry contract needs to verify a quote against the attestation contract, it does so without taking into account any fees. This is not fully compatible with the actual attestation contract implementation because there is a fee involved with the verification. If a fee percentage is set, then any call to verifyAndAttestOnChain will fail, and the protocol will neither be able to register TEE services nor invalidate attestations.

Consider making the verifyAndAttestOnChain function from the IAttestation interface payable and handling the fee payment accordingly when the fee percentage is set.

Update: Resolved in pull request #25 at commit 5b0370d.

Version Check Can Be Arbitrarily Bypassed When Verifying Block Builder Proofs

Found in Phase 1

In order to verify a block builder proof, the version of the flashtestation protocol used to generate the block builder proof and the hash of the block content must be provided. The only constraint for the version is that it needs to be in the SUPPORTED_VERSIONS array. However, there are no checks to ensure that the version provided is indeed the version used for generating the blockContentHash. That allows any user to arbitrarily provide a version that is supported in order to bypass the supported version check.

Consider adding measures to ensure that the version provided by the user is the one that is used for generating the blockContentHash.

Update: Resolved in pull request #26 at commit 38e19c2.

Low Severity

Missing Zero-Address Checks

Found in Phase 1

When operations with address parameters are performed, it is crucial to ensure that the address is not set to zero. Setting an address to zero is problematic because it has special burn/renounce semantics. This action should be handled by a separate function to prevent accidental loss of access during value or ownership transfers.

Throughout the codebase, multiple instances of missing zero-address checks were identified:

Consider always performing a zero-address check before assigning a state variable.

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

Missing Deadline Protection for EIP-712 Signatures

Found in Phase 1

When constraining permit signatures, it is highly recommended to have a deadline protection in case a signature is not executed within a specific time window set by the signer.

Throughout the codebase, multiple instances of missing deadline protection for signatures were identified:

Consider checking that the provided deadline is not expired and is part of the EIP-712 digest hash.

Update: Partially resolved in pull request #30 at commit 5dd4305. The team stated:

We do not add a deadline for BlockBuilderPolicy's permitVerifyBlockBuilderProof, because the high frequency with which we'll be broadcasting new calls to permitVerifyBlockBuilderProof which invalidate the old calls makes a deadline not very useful for it. We intend to broadcast 1 such call every 200ms, for each flashblock. In fact, we can't even correctly implement a deadline for permitVerifyBlockBuilderProof, because the smallest granularity we could use for the deadline is 1 second, and that is not enough time to specify multiple flashblocks (which we'll need to do).

Missing Nonce Signature Invalidation

Found in Phase 1

When a user signs a permit digest with a specific nonce but later wants to invalidate previous signatures, the most used implementations are either a function to increment the nonce or executing a no-op operation. In the case of FlashtestationRegistry, neither of these two options is available. No-op permit executions are reverted in order to avoid gas costs. Thus, the only way to increment the nonce is by executing a valid signature with a different valid rawQuote value.

Consider adding a function to increment the nonce of the caller in order to invalidate previous signatures not yet executed.

Update: Resolved in pull request #31 at commit 6b7e14f and optimized in pull request #42 at commit 18fc01d.

Missing Reentrancy Guard in invalidateAttestation

Found in Phase 1

Functions that register new TEE services have a reentrancy guard modifier because they do not follow the CEI pattern and rely on the external call verifyAndAttestOnChain from the attestation contract. While this call can be trusted because the attestation contract should not be malicious and is not upgradeable, there are situations where the execution is forwarded to the tx.origin.

Hence, with the recent introduction of EIP-7702, it is possible for a malicious user to reenter the FlashtestationRegistry with an outdated state. For this reason, the reentrancy guard is completely necessary. However, while the invalidateAttestation function relies on this external call, it does not have the reentrancy guard protection. Hence, it is vulnerable to being reentered.

Consider adding the nonReentrant modifier to the invalidateAttestation function.

Update: Resolved in pull request #28 at commit c719a4e.

No Checks on sourceLocators Array When Adding Workloads

Found in Phase 1

When a new workload is added to a policy, the governance has to provide the commitHash and sourceLocators to save the metadata for the workload. The commitHash is the commit hash of the git repository whose source code is used to build the TEE image identified by the workloadId, and the sourceLocators is an array of URIs pointing to that source code. The commitHash cannot be empty because the length must be greater than 0, but sourceLocators has no checks. This array should have at least one element to fetch the source code.

Consider ensuring that the sourceLocators array has at least one element.

Update: Resolved in pull request #29 at commit f95ce2c.

Domain Separator Not Public

Found in Phase 1

The domain separator, as defined in EIP-712, is used for obtaining the final message hash signed by the user. The BlockBuilderPolicy and FlashtestationRegistry contracts use EIP-712 typed data but do not expose their domain separator via a public view function. As such, off-chain signers and integrators must reconstruct the domain, which can be error-prone.

Consider exposing the domain separator via a public function to allow for the easy fetching of the domain separator.

Update: Resolved in pull request #32 at commit c78ac25.

Notes & Additional Information

Incomplete Docstring

Found in Phase 1

In the initialize function of the FlashtestationRegistry contract, the owner parameter is not documented.

Consider thoroughly documenting all functions/events (and their parameters or return values) that are part of a contract's public API. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).

Update: Resolved in pull request #33 at commit fdfef22.

Missing Docstrings

Found in Phase 1

Throughout the codebase, multiple instances of missing docstrings (or comments not classified as docstrings) were identified:

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 #34 at commit 210a861.

Custom Errors in require Statements

Found in Phase 1

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 0.8.27 extended support for this feature to the legacy pipeline as well.

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

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

Update: Resolved in pull request #40 at commit 2e33699.

_disableInitializers() Not Being Called From Initializable Contract Constructors

Found in Phase 1

In a proxy pattern, an implementation contract allows anyone to call its initialize function. While not a direct security concern, preventing the implementation contract from being initialized by an unintended party is important, as this could allow an attacker to take over the contract.

Throughout the codebase, multiple instances of initializable contracts where _disableInitializers() is not called in the constructor were identified:

Consider calling _disableInitializers() in initializable contract constructors to prevent malicious actors from front-running initialization.

Update: Acknowledged, not resolved. The team stated:

We don't see a possible way this would impact the implementation contracts, and it adds more complexity to the contract interface than we think is worth it.

Lack of Indexed Event Parameters

Found in Phase 1

Throughout the codebase, several events do not have indexed parameters:

To improve the ability of off-chain services to search and filter for specific events, consider indexing event parameters.

Update: Resolved. Resolved in pull request #41 at commit b921c24. The team stated:

We didn't get to this one

Missing Named Parameters in Mappings

Found in both phases. First 4 instances in Phase 1 and last one in Phase 2.

Since Solidity 0.8.18, mappings can include named parameters to provide more clarity about their purpose. Named parameters allow mappings to be declared in the form mapping(KeyType KeyName? => ValueType ValueName?). This feature enhances code readability and maintainability.

Throughout the codebase, multiple instances of mappings without named parameters were identified:

Consider adding named parameters to mappings in order to improve the readability and maintainability of the codebase.

Update: Resolved in pull request #35 at commit 4cd4603.

Redundant Getter Function

Found in Phase 1

When state variables use public visibility in a contract, a getter method for the variable is automatically generated.

Within the BlockBuilderPolicy contract in BlockBuilderPolicy.sol, the getWorkloadMetadata function is redundant because the approvedWorkloads state variable, being public, already has a getter.

To improve the overall clarity, intent, and readability of the codebase, consider removing the redundant getter functions.

Update: Resolved in pull request #34 at commit 210a861.

State Variable Visibility Not Explicitly Declared

Found in Phase 1

Within BlockBuilderPolicy.sol, 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: Acknowledged, not resolved. The team stated:

We didn't get to this one

Unnecessary Data Field in Event Emission

Found in Phase 1

In BlockBuilderPolicy.sol, the emit BlockBuilderProofVerified(teeAddress, workloadId, block.number, version, blockContentHash, commitHash); event emission includes unnecessary data fields such as block.number.

To improve the efficiency of the contract, consider removing unnecessary data fields from the event emission such as block.number or block.timestamp since they are already included in the block information.

Update: Resolved in pull request #38 at commit ad43c90.

Unnecessary Gaps

Found in Phase 1

Gaps are necessary storage areas reserved for upgradeable contracts with inherited modules that do not implement namespaced storage layouts. However, both FlashtestationRegistry and BlockBuilderPolicy import OpenZeppelin upgradeable contracts with a version that already supports this feature. Thus, there is no need to add gap arrays at the end of the implementation storage as long as the order of variables remains the same with future upgrades.

Consider removing the gaps and using namespaced storage for the contracts.

Update: Acknowledged, not resolved. The team stated:

We're not confident at changing to use EIP-7201 at this point.

Misleading Comment

Found in Phase 1

The docstring in line 141 of the BlockBuilderPolicy contract points to an old version of the documentation and can, therefore, be misleading to the readers.

Consider updating the documentation link to the latest version.

Update: Resolved in pull request #39 at commit 8277c53.

Use ReentrancyGuardTransientUpgradeable Module

Found in Phase 1

The ReentrancyGuardTransientUpgradeable module should be used instead of ReentrancyGuardTransient, as it ensures that all modules remain upgradeable. Although both versions function identically, transitioning to the upgradeable version aligns with best practices for upgradeable contract architectures.

Consider using the ReentrancyGuardTransientUpgradeable module instead of the ReentrancyGuardTransient module and ensuring that all inherited upgradeable module initializers are called within the initialize function, such as __UUPSUpgradeable_init() for UUPS support and __ReentrancyGuardTransient_init() for the reentrancy guard. Doing so will help improve the maintainability and upgradeability of the contract.

Update: Resolved in pull request #36 at commit 6dace7a and pull request #43 at commit a2371ba.

Naming Suggestion

Found in Phase 1

In the doRegister function of the FlashtestationRegistry contract, the first parameter is named caller. This is misleading as the registering TEE is not always the address that sends the transaction. A third party can relay a signed message via permitRegisterTEEService. As such, msg.sender is not always the TEE being registered.

Consider changing the name of the argument to something more unambiguous like teeAddress.

Update: Resolved in pull request #37 at commit 3ad9547.

Use Stored quoteHash Instead of Rehashing in checkPreviousRegistration

Found in Phase 2

When registering a new TEE service in FlashtestationRegistry, the implementation checks whether rawQuote has already been registered for the same TEE address and reverts if necessary. To do this, it hashes the new quote and compares it against the hash of the stored rawQuote. However, since the hash of the quote is already stored in the registry, it allows the new quote’s hash to be compared directly against the stored hash, saving gas by requiring only a single SLOAD instead of up to 20 * 1024 / 32 = 640 SLOAD operations, and avoids recomputing the hash.

Consider comparing the hash of the new quote with the quoteHash stored in registeredTEEs to optimize gas usage.

Update: Resolved in pull request #24 at commit 5a7fb4a.

Avoid Variable Shadowing

Found in Phase 2

In the _cachedIsAllowedPolicy function, the returned named variable allowed is shadowed. While this doesn't cause any functional issues, we recommend removing the named return variable to improve code clarity.

Consider removing the named return for the boolean allowed in order to prevent variable shadowing.

Update: Resolved in pull request #24 at commit b389e66.

Redundant Unwrapping of workloadId Increases Gas Usage

Found in Phase 2

When processing the happy path in _cachedIsAllowedPolicy function, the implementation repeatedly unwraps workloadId in multiple places (1, 2), rather than caching the unwrapped value. This results in redundant operations and unnecessary gas usage. Based on gas reports, caching workloadId leads to a median gas reduction of 3 units.

Consider unwrapping workloadId once and reusing the cached value to optimize for gas efficiency.

Update: Resolved in pull request #24 at commit 1e25376.

Conclusion

Flashtestations is an on-chain protocol for verifiable TEE attestations (Intel TDX/DCAP), enabling TDX devices to prove outputs and making Unichain’s block construction transparent. The codebase was found to be well-written and secure. The audit identified minor issues and provided recommendations to enhance code clarity and maintainability. The Uniswap Labs team is appreciated for being highly cooperative and providing clear explanations throughout the audit.