Summary

Type: DeFi
Timeline: From 2025-08-11 → To 2025-09-05
Languages: Solidity

Findings
Total issues: 23 (20 resolved)
Critical: 0 (0 resolved) · High: 2 (2 resolved) · Medium: 2 (2 resolved) · Low: 7 (6 resolved)

Notes & Additional Information
12 notes raised (10 resolved)

Scope

OpenZeppelin audited the Uniswap/Tribunal repository at commit 675141f.

In scope were the following files:

 src
├── BlockNumberish.sol
├── ERC7683Tribunal.sol
├── Tribunal.sol
├── interfaces
│   ├── IArbSys.sol
│   ├── IDestinationSettler.sol
│   ├── IRecipientCallback.sol
│   ├── ITribunal.sol
│   └── ITribunalCallback.sol
├── lib
│   ├── DomainLib.sol
│   └── PriceCurveLib.sol
└── types
    ├── TribunalStructs.sol
    └── TribunalTypeHashes.sol

System Overview

Tribunal is a framework for settling cross-chain swaps on PGA (priority gas auction) chains. It ensures that funds move strictly according to a sponsor's mandate and that, in the event of a contention, a single party can finalize the settlement. Tribunal integrates with The Compact protocol for deposits and claims, and provides two integration hooks, _processDirective and _quoteDirective, enabling bridges or messaging layers to implement delivery and pricing on the destination chain.

Within The Compact protocol, Tribunal serves as the arbiter, interacting directly with both the protocol itself and with allocators. It is designed as a highly customizable framework. As part of this scope, the ERC7683Tribunal contract was also included, ensuring compatibility with the ERC7683Allocator contract for implementing the ERC-7683 standard.

High-Level Lifecycle

  1. Preparation: The sponsor deposits and registers a Compact on the source chain assigning an adjuster, a cross-chain fill, and a source-chain fallback action that will trigger a deposit and registration of a target-chain compact.
  2. Activating a Stage: The adjuster co-signs an Adjustment to reveal and activate one Fill (usually cross-chain first. A source-chain fallback can be activated later).
  3. Finalization: A filler executes the live Fill, or the sponsor cancels. Bridged funds, if any, are routed to filler/sponsor or registered on the target chain per the recorded disposition.

Execution Paths

  • Same-Chain: Tribunal claims via The Compact and pays the recipient.
  • Cross-Chain: Tribunal records a disposition and invokes _processDirective for the integrator to deliver the message/value. settleOrRegister standardizes how bridged funds land and resolves races (early cancel vs prior fill).
  • Callbacks (optional): Recipient callbacks can chain actions (e.g., auto-register on target).

Security Model and Trust Assumptions

During the audit, the following trust assumptions were made:

  • Bridge/Message Layer Trust: Cross-chain safety and liveness depend on the inheriting implementation of _processDirective and the underlying transport. Incorrect implementations, delayed delivery, or fork/reorg-handling errors can cause stuck funds or delays.
  • Adjuster Correctness: The adjuster selects the active fill via fillIndex, sets the target block and any supplemental price curve, and can optionally restrict the filler and block window. A malicious or erroneous adjuster can deny service or skew pricing within configured bounds.
  • Trusted Callbacks Recipient and sender callbacks execute external code. While the callbacks are guarded by reentrancy protection and return-value checks (for recipient callbacks), they can still revert and block fills. As such, they are assumed to be trusted.
  • Token Behaviors. Fee-on-transfer tokens are explicitly supported but yield less than requested to recipients. Integrations and UIs must mention this fact clearly.
 

High Severity

DoS in settleOrRegister With Non-Native Token batchDeposit*

During batch deposits, TheCompact enforces specific requirements on the provided deposit structure. For example, if the first token in the deposit is non-native, the associated native amount must be zero.

In settleOrRegister, the contract's balance is forwarded to the batchDeposit* functions without verifying whether the first token is non-native. This allows a DoS scenario for all calls involving a non-native token if an attacker sends even 1 wei of native tokens to the Tribunal contract. Since Tribunal has an empty receive function, there are no restrictions preventing an attacker from increasing the contract balance and exploiting this condition.

Consider only forwarding the requested amount to the Compact's batchDeposit* functions and entirely omitting native tokens when the deposit involves a non-native token.

Update: Resolved in commits 719ac2a and 1667290.

Incorrect Logic in applySupplementalPriceCurve

The applySupplementalPriceCurve function uses errorBuffer to aggregate errors from sharesScalingDirection and revert in the end if errorBuffer is not equal to 0. The issue is that sharesScalingDirection returns true in case the scaling direction is shared and false if it is not. That means that the logic aggregates correct executions and counts them as errors within the errorBuffer.

Consider checking if sharesScalingDirection returns false before adding its result to errorBuffer.

Update: Resolved in commit 36c31ca.

Medium Severity

Validity Window Check Inverted in _fill Causing Fills Inside the Window to Revert

In the protocol, an adjuster can optionally constrain who may execute a fill and for how long after a designated targetBlock. These constraints are encoded in Adjustment.validityConditions: the lower 160 bits hold an optional exclusive filler address (zero means no restriction), and the upper bits hold an optional block window length. When a non-zero window is set, the fill should be permitted from targetBlock up to and including targetBlock + validBlockWindow. Separately, lower-bound enforcement (fillBlock >= targetBlock) is already handled in deriveAmounts via an InvalidTargetBlock check.

The current implementation in the Tribunal contract inverts the window check, reverting when the fill occurs inside the validity window and only allowing it after the window has passed. Given the intended semantics, the comparison should enforce an upper bound (i.e, reverting only when the fill occurs after targetBlock + validBlockWindow).

Consider flipping the comparison to enforce the upper bound and keep the filler restriction unchanged.

Update: Resolved in commits ccc9cb6 and 1667290.

Reentrancy Through the Recipient Callback

After processing the fill, the fill function executes a callback to the recipient via performRecipientCallback. Then, any remaining ETH is returned to the filler. This creates a reentrancy risk, as the recipient could potentially exploit the callback to steal funds held in the contract by executing the settleOrRegister function.

Consider adding the nonReentrant modifier to the settleOrRegister function.

Update: Resolved in commit 6ede7aa.

Low Severity

Potential Panic in _deriveMandateHash

The _deriveMandateHash function validates the fillIndex argument to prevent accessing elements outside the fillHashes array. Accessing an index beyond the valid range causes a runtime panic. The current validation allows fillIndex to equal the array length, which is not a valid index and may result in a panic when accessed.

Consider adjusting the validation to ensure that the index is strictly less than the array length, preventing any out-of-bounds access.

Update: Resolved in commit 49516d9.

_quote Ignores Supplemental Price Curve

The _fill function applies adjustment.supplementalPriceCurve, while the _quote function does not. This results in different effective price curves being used depending on whether the action is a quote or a fill.

Consider aligning the behavior of _quote with _fill so that both consistently apply adjustment.supplementalPriceCurve.

Update: Resolved in commit 91336cf.

Invalid Argument Order in InvalidTargetBlock Error

In the deriveAmounts function, there is a check ensuring that targetBlock is not in the future. If this condition fails, the function reverts with the InvalidTargetBlock error. This error is defined to take arguments in the order (blockNumber, targetBlockNumber), but the implementation currently provides them in reverse.

Consider correcting the argument order in the InvalidTargetBlock error to match its defined signature.

Update: Resolved in commit f73c604.

Upper Bits of adjuster Not Masked When Decoding from Calldata

In the _parseCalldata function, the adjuster is loaded with LibBytes.loadCalldata and then assigned in assembly without masking the upper 12 bytes. Although properly encoded calldata will have zeroed upper bits, malformed inputs could lead to unexpected behavior if other assembly code later relies on strict 160-bit representations, for example in equality checks, hashing, or storage packing.

Consider explicitly masking the value to 160 bits during decoding.

Update: Resolved in commit d5e148f.

Floating Pragma

Pragma directives should be fixed to clearly identify the Solidity version with which the contracts will be compiled.

Throughout the codebase, multiple instances of floating pragma directives were identified:

Consider using fixed pragma directives.

Update: Acknowledged, not resolved. The team stated:

Since Tribunal is actually a framework that is meant to be inherited by other contracts, we think a floating pragma is actually appropriate!

Incomplete Docstrings

In IRecipientCallback.sol, the tribunalCallback function has incomplete documentation. Specifically, the chainId parameter is not described, and not all return values are 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 commit 5d24d78.

Missing Docstrings

Throughout the codebase, multiple instances of missing 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 commit 61f0ef0.

Notes & Additional Information

Expensive Function Dispatch in BlockNumberish

In the BlockNumberish contract, the _getBlockNumberish function is dynamically assigned to one of two implementations depending on the chain ID. When compiled with the via-IR pipeline, this setup results in function dispatch through a switch statement. As a result, every call to _getBlockNumberish incurs additional overhead compared to a direct conditional check. This dispatch mechanism increases gas costs unnecessarily, since each call must first resolve the function pointer and then execute the selected implementation, instead of performing a single branch and return.

Consider consolidating the logic into a single _getBlockNumberish function that evaluates the chain ID at runtime and directly returns the appropriate block number, eliminating the need for function pointer dispatch.

Update: Resolved in commit 471a538.

Unreachable Code in settleOrRegister

In the settleOrRegister function, when mandateHash is zero, the batchDeposit function is executed and the function returns immediately in line 224. Later in the function, there is another code block that performs the same operation when mandateHash is empty. This second block is never executed because the earlier return statement prevents it from being reached.

Consider removing the unreachable code block or restructuring the logic so that the second block can be executed if intended.

Update: Resolved in commit 542858c.

Transient Storage Support Is Not Verified

In the Compact, a dedicated Tstorish contract ensures compatibility by falling back to regular storage writes when transient storage is not supported. However, in Tribunal, no equivalent fallback mechanism exists. Instead, transient storage is directly used in the nonReentrant modifier without checking whether the feature is supported.

Consider implementing a fallback mechanism similar to the Compact's Tstorish contract.

Update: Acknowledged, not resolved. The team stated:

An unnecessary complication as Tribunal does not have the same "needs to be supported at the same deployment address on every conceivable EVM chain" dependency as The Compact.

Redundant Index Guard in PriceCurveLib.getCalculatedValues Condition

The getCalculatedValues function in PriceCurveLib iterates over price-curve segments and uses the hasPassedZeroDuration flag to track when a zero-duration element has been encountered. When the next non-zero segment is reached, the code conditionally interpolates from the previous (zero-duration) scaling factor using a compound condition: hasPassedZeroDuration && i > 0 && getBlockDuration(PriceCurveElement.wrap(parameters[i - 1])) == 0.

The i > 0 check is redundant. If hasPassedZeroDuration is true, it means that a zero-duration element was already processed in a prior iteration and the loop continue was executed, so the earliest subsequent non-zero iteration must have i >= 1. Therefore, the parameters[i - 1] access is already safe under the existing control flow, and the additional i > 0 guard does not add safety.

Consider removing i > 0 from the condition and relying on hasPassedZeroDuration to imply that a previous element exists.

Update: Resolved in commit 950d014.

Custom Errors in require Statements

Since Solidity version 0.8.26, custom errors can be used directly within require statements. This feature, initially limited to the IR pipeline, became available in the legacy pipeline starting with version 0.8.27.

The codebase currently uses multiple if checks followed by revert statements with custom errors. While functionally correct, this approach is more verbose than necessary and may increase gas costs compared to using require with custom errors.

Consider replacing if-revert patterns with require statements that directly include custom errors. This will simplify the code and may lead to reduced gas usage while preserving the intended error handling.

Update: Acknowledged, not resolved. The team stated:

We believe that if statements are usually more explicit.

Magic Numbers

Throughout the codebase, multiple instances of literal values with unexplained meanings were identified:

  • The 4 literal number in Tribunal.sol
  • The 5 literal number in Tribunal.sol
  • The 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 literal number in Tribunal.sol
  • The 1e18 literal number in Tribunal.sol
  • The 1e18 literal number in Tribunal.sol
  • The 1e18 literal number in Tribunal.sol
  • The 1e18 literal number in Tribunal.sol
  • The 1e18 literal number in Tribunal.sol
  • The 1e18 literal number in Tribunal.sol
  • The 160 literal number in Tribunal.sol
  • The 1000 literal number in Tribunal.sol

Consider defining and using constant variables instead of using literals to improve the clarity and maintainability of the codebase.

Update: Resolved in commits 4cac16b and dd9b7f4.

Inconsistent Order Within Contracts

The Tribunal contract deviates from the Solidity Style Guide due to having inconsistent ordering of functions.

To improve the project's overall legibility, consider standardizing ordering throughout the contract as recommended by the Solidity Style Guide (Order of Functions).

Update: Resolved in commit 1222e1b.

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.

Throughout the codebase, multiple instances of contracts not having a security contact were identified:

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 commit 49851cf.

Unsafe Casts

In the applySupplementalPriceCurve function of the PriceCurveLib library, the combinedScalingFactor is unsafely cast to a uint240 value.

To prevent unexpected behavior, consider ensuring that the value of combinedScalingFactor fits within the bounds of the uint240 type before casting.

Update: Resolved in commit a64d286.

Unused Function With internal Visibility

In ERC7683Tribunal.sol, the getFillerData function is unused.

Consider either making the getFillerData function external or public, as it provides logic for encoding the filler data. Alternatively, if the function is not intended to be used externally, consider removing it altogether. Doing so will help improve the overall clarity and maintainability of the codebase.

Update: Resolved in commit bcdead3.

Unused Imports

Within ERC7683Tribunal.sol, multiple instances of unused imports were identified:

Consider removing unused imports to improve the overall clarity and readability of the codebase.

Update: Resolved in commit 42e78fb.

Incorrect Documentation

The NatSpec comment for maximumClaimAmounts states, "The minimum claim amounts for each commitment", which directly conflicts with the variable name that clearly conveys a maximum bound.

To improve the clarity and maintainability of the codebase, consider updating the documentation.

Update: Resolved in commit 20fb2a7.

 
 

Conclusion

Tribunal is a settlement framework designed for cross-chain swaps on PGA (Priority Gas Auction) chains. It forces all transfers to comply with the sponsor's mandate and integrates with The Compact to manage deposits and claims. The framework also provides integration points that allow external bridges or messaging layers to handle delivery and pricing on the destination chain.

During the audit, several high-, medium-, low-, and note-level issues were identified. All high- and medium-severity issues, as well as the majority of low- and note-level findings, were resolved prior to deployment. The current test coverage is limited for a production-grade protocol and would benefit significantly from comprehensive unit, integration, and end-to-end testing. In addition, incorporating fuzz testing could enhance the maturity of the codebase.

The Uniswap Labs team is appreciated for their support throughout the review process and for promptly responding to all questions posed by the audit team.