Across Counterfactual & Periphery Updates Diff Audit

Source: OpenZeppelin•

Across Counterfactual & Periphery Updates Diff Audit

OpenZeppelin's diff audit of seven Across pull requests: counterfactual delivery, command dispatch, SpokePool periphery and HyperCore helpers. 12 issues, 10 resolved.

FindingsTotal issues: 12 (10 resolved)Critical: 0 (0 resolved) · High: 0 (0 resolved) · Medium: 1 (1 resolved) · Low: 4 (4 resolved)

Notes & Additional Information7 notes raised (5 resolved)

Client Reported Issues0 reported issues (0 resolved)

Scope

OpenZeppelin conducted a diff audit of the across-protocol/contracts-v5 repository covering five pull requests:

  • PR #202, from base snapshot eda88c2 to target snapshot 0416af4
  • PR #203, from base snapshot eda88c2 to target snapshot 939c648
  • PR #207, from base snapshot eda88c2 to target snapshot c48ac33
  • PR #210, from base snapshot e34da79 to target snapshot 68ea4cb
  • PR #212, from base snapshot 72522fa to target snapshot 8151d40

In scope were the following modified or added files:

OpenZeppelin also conducted a diff audit of the across-protocol/contracts repository covering two pull requests:

In scope were the following modified or added files:

Test files were not included in the scope of this review. In some cases, examining them was necessary in order to understand or verify the behavior of the code in scope, and observations arising from that work have been reported where they were considered useful. These should not be read as a comprehensive review of the test suites.

System Overview

This section covers seven pull requests in scope for this engagement, spanning two repositories: five extend a distinct part of the counterfactual delivery vertical or the shared command-dispatch engine it runs on, in across-protocol/contracts-v5, and two extend the SpokePoolPeriphery gasless-deposit surface and the HyperCoreLib bridging library, in across-protocol/contracts.

PR #202 (CCTP fee cap change)

CounterfactualCCTPBridgeExecutor, one of the per-bridge source executors in the counterfactual vertical, replaces a single proportional Circle fast-transfer fee cap with two independently configurable, additive components mirroring Circle's own fee shape: a fixed forwarding-service amount in burn-token units, and a proportional component in basis points of the burned amount. Either component may be left unset by the route; if both are unset, any nonzero fee is rejected on-chain, since a zero fee never reads the cap at all. The two beacon getters resolving these caps, cctpMaxFeeFlatGetter and cctpMaxFeeBpsGetter, are selectors resolved against the per-chain configuration contract at execution time rather than values committed directly in the route, letting the protocol repoint them, or the executor implementation itself, by upgrading the beacon without touching already-committed user roots.

PR #203 (HyperCore withdrawal leaf)

CounterfactualHyperCoreWithdrawer is a route leaf addressing a HyperEVM-specific gap outside the Gateway-routed prefunding flow the rest of the counterfactual system uses. HyperCore, the separate settlement ledger Hyperliquid maintains alongside HyperEVM, tracks its own balance for the same address, and funds that land there are invisible to HyperEVM's ordinary deposit detection. The leaf issues a HyperCore action moving a caller-specified amount of a given token from a caller-specified source account, the proxy's spot balance, its main perpetuals account, or a builder-deployed perpetuals exchange, to that token's HyperCore system address. HyperCore then asynchronously credits the equivalent balance back to the same address on HyperEVM, where the proxy's ordinary route leaves can pick it up as a regular deposit. The leaf performs no Gateway interaction and reads no on-chain balance; it acts entirely through HyperCore's precompiles and its CoreWriter system contract. Authorization is gated to a committed pair of addresses, reusing the same parameter schema as the existing, Gateway-independent withdraw leaf so that one committed pair covers both recovery paths in a proxy's tree. No signature accompanies the call, since the leaf's only possible destination is the calling address's own HyperEVM balance.

PR #207 (single-use counterfactual release and Paxos Transit)

This pull request extends the counterfactual delivery system with a single-use delivery primitive and a source-chain integration for the Paxos Transit bridge that consumes it. Where a destination bridge cannot attach an on-chain message to a transfer, the delivery address itself must encode which order the transfer belongs to; this change derives that address deterministically from the delivery terms, so that funds sent there can only ever be claimed under those terms.

CounterfactualStepRelease is a Merkle leaf executed by delegatecall inside a per-order counterfactual proxy. On invocation it forwards the proxy's entire balance of a committed asset, once that balance clears a committed minimum, to a single committed recipient, and only when called by a single committed releaser. The token, minimum amount, recipient, and releaser are all part of the CREATE2 preimage that determines the proxy's own address, so altering any of those terms yields a different, unfunded address rather than redirecting an existing delivery. CounterfactualReleaseAdapter is the on-chain trigger for this leaf: invoked mid-execution as an adapter call on the destination chain, it rebuilds the expected leaf from the live execution's own step identifier and the delivery terms committed to that execution, deploys the proxy if it does not yet exist, and dispatches the release. It names itself as the leaf's sole authorized releaser, so a release can only be triggered through this specific contract.

CounterfactualPaxosBridgeExecutor is the source-chain counterfactual executor for orders routed through Paxos Transit, an external swap-and-bridge service reached through its TransitStation contract. Because Transit does not carry a destination witness as message data, the executor computes the destination delivery address on-chain before the order is submitted, using the same deterministic derivation the destination side will later use to trigger the release, and requires the externally signed Transit order to name that address as its receiver. Where the destination chain does not share the source chain's address-derivation scheme, for example an EVM source paired with a Tron destination or the reverse, derivation is delegated to one of two new predictor contracts, EvmFactoryProxyPredictor and TronFactoryProxyPredictor, which reproduce the counterpart chain's own factory logic locally. The executor also enforces a minimum net delivered amount, expressed in a canonical 18-decimal denomination shared across chains whose tokens differ in decimals, before the order is submitted.

A relayer submits the source order directly to the protocol's entry point, which invokes the counterfactual executor. The executor verifies an off-chain-signed quote, releases previously deposited funds from the user's counterfactual proxy, and submits the Transit order together with the receiver address it computed. Delivery to that address happens off-chain and asynchronously; once Transit places funds there, a separate, later execution on the destination chain consumes them by triggering the release leaf described above.

PR #210 (Executor packed-amount and TRANSFER change)

Executor is the command dispatcher that carries out a single path's tape after the Gateway routes funding to it. The tape is a byte array of command types paired with a matching array of committed inputs; a handful of utility commands (APPROVE, TRANSFER, WRAP_ETH, UNWRAP_WETH) and the native call value on the CALL family each carry an amount field that is packed rather than a plain integer, so the same field can express a fixed value or a value derived from the Executor's balance at execution time.

The packed field is decoded by a single internal helper shared by every one of those call sites. It reads the low 240 bits as a raw amount and the high 16 bits as a mode selector. A selector of zero yields the raw amount unmodified. A raw amount equal to the all-ones sentinel switches the selector into a basis-points reading, resolving to that fraction of the Executor's live balance of the relevant token. This pull request adds a third mode, selected by a dedicated non-zero selector value distinct from the basis-points range: the raw amount is read as a floor, and the field resolves to the Executor's live balance in excess of that floor, or zero when the floor is not cleared. This lets a path express forwarding everything above a reserve without knowing the exact balance in advance, the same way the basis-points mode already lets a path express a proportional amount without knowing the absolute balance.

This pull request also changes TRANSFER specifically: once the packed field resolves to zero, under any of the three modes, the command now returns without issuing the underlying transfer, whereas it previously still executed a zero-value call or token transfer. APPROVE and the call-value paths are unaffected by this guard and continue to execute unconditionally on their resolved amount.

PR #212 (atomic proxy deployment and root migration)

The counterfactual deposit system lets a user fund a deterministic proxy address before that proxy is ever deployed on-chain, and later release the balance it accumulated through a Merkle-tree-gated dispatcher. Each proxy is a beacon-upgradeable BeaconProxy running CounterfactualDeposit, whose committed activeRoot authorizes a fixed set of route leaves. A leaf is invoked by proving its inclusion in that root and delegatecalling the named implementation contract in the proxy's own storage and address context, so the leaf's code executes with the proxy's balances and identity. On the V5 source-execution path, a relayer calls the Gateway directly with an API-built path naming a specialized, closed source executor; that executor asks the proxy to reconstruct the live execution context and release funds into it through the proxy's dispatcher, which proves a route leaf against the proxy's committed activeRoot before delegatecalling the requested implementation.

This pull request extends that release step, _prefund in CounterfactualSourceExecutorBase, to also handle the proxy's deployment and, optionally, a migration of its activeRoot, atomically with the fund release, in the same transaction. Previously, a proxy that had never been deployed could not be used through this flow at all: no available entry point both deployed the proxy and satisfied the dispatcher's requirement that the caller be the Gateway's live, currently invoked executor. The extension has the executor itself call the deterministic CounterfactualDepositFactory to deploy the proxy when it has no code yet, then call the proxy's dispatcher directly, so the caller the dispatcher observes is always the executor, never an intermediary. A root migration, when requested, is folded into the same call by using the dispatcher's combined entrypoint instead of its plain release entrypoint.

PR #1529 (SpokePoolPeriphery relative timestamps)

SpokePoolPeriphery extends the Across deposit flow with signed, gasless entry points. A depositor authorizes a deposit, or a swap immediately followed by a deposit, off-chain by producing an EIP-712 signature over the deposit parameters together with a separate signed authorization to pull the input token, using ERC-2612 permit, Permit2, or EIP-3009 receiveWithAuthorization. An independent submitter later executes the transaction, redeeming both signatures to pull funds from the signer and forwarding the deposit to the SpokePool.

Because the deposit-data signature can be produced well before the submitter executes it, quoteTimestamp and fillDeadline, the fields that set a deposit's pricing snapshot and its fill window on the SpokePool, can be expressed relative to execution time rather than as fixed absolute values. Any value at or below a one-year threshold is treated as relative and resolved against block.timestamp at the moment the transaction executes: quoteTimestamp is resolved by subtracting the value, so it always names the block in which the deposit lands as the pricing snapshot, and fillDeadline by adding it, so the deposit keeps a full fill window regardless of submission delay. Values above the threshold are forwarded unchanged as absolute timestamps. This resolution is applied once, immediately before the call into the SpokePool, and uniformly across every deposit and swap-and-deposit entry point.

A signed payload that uses relative timestamps no longer expires on its own the way an absolute, already-past fillDeadline previously would, since the resolved values are always recomputed as of whenever the payload is actually submitted. The entry points that pull funds via ERC-2612 permit account for this directly: the deposit-data signature also commits to a deadline, checked against the current time before the deposit proceeds. This is necessary because permit is called inside a try/catch, so a signer's existing token allowance can still fund the pull even after their permit signature itself has expired.

PR #1530 (HyperCore shared primitives)

The HyperCoreLib is the stateless internal library that Hyperliquid-integration contracts link in to move assets and native value between HyperEVM (the EVM-compatible execution layer of Hyperliquid) and HyperCore (Hyperliquid's separate, off-EVM order-book and settlement layer).

This pull request extends HyperCoreLib by adding the values and helpers describing native HYPE and identifying the two supported networks, adding a primitive bridging native HYPE from HyperEVM to the caller's own HyperCore spot balance, and replacing the library's system-address resolver with one that additionally recognizes native HYPE and rejects indices that cannot be bridged. The three existing call sites that resolved a system address are repointed to the replacement, so the new rejection also governs the library's pre-existing ERC-20 bridging and bridge-liquidity paths. These are the primitives that CounterfactualHyperCoreWithdrawer, the withdrawal leaf added by PR #203 above, had defined for itself; consolidating them here lets that leaf drop its local copies, and the change is made in this repository because contracts-v5 consumes the library as a git submodule.

How a token reaches HyperCore is what the new resolver encodes. A regular ERC-20 is sent to a per-token address derived deterministically from the token's HyperCore index, and a plain transfer to that address is what triggers the cross-layer credit. Native HYPE has no linked EVM contract of its own, so it uses a single fixed system address rather than a derived one. In the derived case the library now also checks that the index corresponds to a token actually linked between the two layers before committing to a transfer, since sending funds to an address the destination layer cannot interpret would leave them unrecoverable.

Certain Hyperliquid identifiers differ between HyperEVM mainnet and its testnet, the native HYPE Core index among them, so the helpers added here resolve those values from block.chainid rather than fixing them to a single value.

Security Model and Trust Assumptions

PR #202 (CCTP fee cap change)

  • Backend/quote authority: signs the committed fee-cap and quote data; the executor does not independently check that the fee cap stays consistent with the route's bridge-safety floor, so this is trusted rather than enforced on-chain.
  • Beacon owner: can retarget the fee-cap getters and the resolved executor by upgrading the beacon; must sequence upgrades so no committed-but-unexecuted route is left decoding against an incompatible executor encoding.
  • Not yet deployed to production, so no route is currently exposed to the upgrade-compatibility risk above.

PR #203 (HyperCore withdrawal leaf)

  • Admin/User: either committed address can trigger recovery for any token, amount, or source at any time; can only recover to the proxy's own HyperEVM address, not redirect elsewhere.
  • Recovery correctness depends on HyperCore's own accounting and the token's bridge configuration; the leaf only checks that the index is linked, not that the link is fully backed.
  • Outcome is asynchronous and unverifiable on-chain: a HyperCore-side failure is dropped silently, with no revert or on-chain signal.
  • Only usable on proxies whose active root already includes this leaf; older proxies need an admin-authorized root migration first.

PR #207 (single-use counterfactual release and Paxos Transit)

  • Off-chain route configuration: fixes destination chain IDs, release-adapter/leaf addresses, and address-derivation logic; only partially cross-checked on-chain (local-vs-foreign agreement only, not full identifier consistency).
  • Quote-signing authority: signs each order's economic terms (bridge-safety floor, fee); trusted not to authorize terms outside what the user actually funded.
  • Relayer/submitter: supplies unauthenticated derivation data; can only cause a failed delivery, not redirect funds, since the destination address is fixed by the delivery terms baked into the proxy's own CREATE2 address.
  • Paxos Transit: external bridge service; its own signer is constrained by the executor's independently-derived destination address and minimum-delivery check, so it cannot redirect funds even if compromised.
  • The two chain-specific address predictors hardcode the counterpart chain's factory/beacon/creation-code constants, unverified on-chain; a stale value permanently strands funds at an address the real factory can never deploy to.

PR #210 (Executor packed-amount and TRANSFER change)

  • Path author: fully controls the packed amount's mode and floor via committed tape data; pairing the basis-points sentinel with the new remainder mode silently resolves to zero instead of reverting.
  • Remainder-mode recipient: receives the Executor's entire live balance above the floor, including leftover funds from unrelated prior executions on this shared, chain-wide contract.
  • The new zero-amount guard on TRANSFER is silent (no event), which can break a path that relied on a zero-value transfer itself as a signal.

PR #212 (atomic proxy deployment and root migration)

  • Relayer/submitter: supplies unauthenticated deploy/migration parameters; cannot redirect either, since the target proxy address and the migration target are both validated against independently committed values.
  • Deploying factory: authorized only by the address its deployment lands at, not by identity, so a malicious or broken factory can cause a revert but never a fund misdirection.
  • Root-migration sentinel: submitting the proxy's own current root as a migration target (intending a no-op) instead re-applies it; if stale by execution time, this silently reverts the proxy to an older root.
  • Deployment is permissionless and cross-caller; cross-chain address parity depends on the factory/beacon being deployed identically on every chain, an assumption made outside this code.

PR #1529 (SpokePoolPeriphery relative timestamps)

  • Depositor: trusts the periphery to resolve relative quoteTimestamp/fillDeadline correctly at execution time, and to bound the signed payload's redeemable lifetime for the entry point used.
  • Submitter: any address may execute a signed payload on the depositor's behalf; under relative timestamps, the submitter's chosen execution time determines the resolved LP-fee rate and fill window, within the payload's valid window.
  • ERC-2612 permit entry points enforce an explicit, signature-bound deadline directly. Permit2/EIP-3009 entry points instead rely on those mechanisms' own deadline/validBefore fields, which were sized to bound the token pull, not quote/fill staleness.

PR #1530 (HyperCore shared primitives)

  • HyperCore: reads trust the precompiles to reflect true state; writes trust CoreWriter-queued actions to be applied as specified. Writes are asynchronous, so a balance check can be stale within the same block.
  • Consuming contracts: must supply the correct HyperCore token index and decimal difference; an incorrect index can route funds to an unrelated linked token or to HYPE's fixed system address.
  • Circle's CoreDepositWallet: USDC bridging trusts this hardcoded address to remain Circle's genuine, correctly behaving deployment, with no fallback if that assumption fails.
  • Several precompile/system addresses are hardcoded per-network constants; an unconditional use of a mainnet-only or testnet-only constant is not caught by any on-chain check.

Medium Severity

Beacon Implementation Omits Flat CCTP Fee-Cap Getter

CounterfactualCCTPBridgeExecutor caps the quote-signed maxFeeCctp using two independently configurable beacon getters that mirror Circle's fee shape: a flat component in burn-token units and a proportional component in basis points of the burn. In _maxFeeCctpCap, each getter is resolved independently when its selector is nonzero, bytes4(0) zeroes that component, and when both are unset the flow fails closed with RouteNotConfigured.

The production beacon does not implement the flat getter. CounterfactualChainConfigV5, the immutable getters on CounterfactualBeaconV5, and ICounterfactualBeaconV5 only expose usdcCctpMaxFeeBps; no flat-fee field or getter (for example usdcCctpMaxFeeFlat) exists anywhere in the deployed configuration. BeaconLib.resolveUint reverts RouteNotConfigured whenever the staticcall to a beacon selector fails, which is the case for any selector the beacon does not implement. As a result, a route that commits a nonzero cctpMaxFeeFlatGetter, as the design and the backend integration documentation describe, reverts before the CCTP burn whenever maxFeeCctp is positive. Only bps-only routes function against the production beacon today. The flat-fee-cap path is exercised in the test suite solely through MockCounterfactualBeacon, which does implement the flat getter, so the gap does not surface against the production beacon in testing.

Consider adding an independently configurable usdcCctpMaxFeeFlat field to CounterfactualChainConfigV5, together with the corresponding immutable and getter on CounterfactualBeaconV5 and its declaration on ICounterfactualBeaconV5, mirroring the existing usdcCctpMaxFeeBps wiring.

Update: Resolved in pull request #229. The team stated:

“Fixed in the PR below”

The flat getter now exists on the production beacon in the same shape as the bps getter, so the staticcall that previously reverted resolves and flat-capped CCTP routes work against the real beacon.

Low Severity

Unsafe ABI Encoding

It is not an uncommon practice to use abi.encodeWithSignature or abi.encodeWithSelector to generate calldata for a low-level call. However, the first option is not typo-safe and the second option is not type-safe. The result is that both of these methods are error-prone and should be considered unsafe.

The use of abi.encodeWithSelector within ForeignProxyLib.sol is unsafe.

Consider replacing all occurrences of unsafe ABI encodings with abi.encodeCall, which checks whether the supplied values match the types expected by the called function and also avoids errors caused by typos.

Update: Resolved in pull request #222. The team stated:

“Fixed in the PR below”

encodecall is now used.

Hardcoded USDC CoreDepositWallet Address Breaks Bridging On HyperEVM Testnet

Consider resolving USDC_CORE_DEPOSIT_WALLET_ADDRESS through a chain-specific helper, mirroring the existing hypeCoreIndex pattern, returning 0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24 on HYPEREVM_CHAIN_ID, 0x0B80659a4076E9E93C7DbE0f10675A16a3e5C206 on HYPEREVM_TESTNET_CHAIN_ID, and reverting on any other chain.

Update: Resolved in pull request #1539. The team stated:

“Fixed in the PR below”

The helper follows the existing hypeCoreIndex pattern and uses exactly the testnet address, so USDC bridging works on both networks.

Native HYPE Bridge Helper Silently Loses Unrepresentable Dust

Update: Resolved in pull request #1540. The team stated:

“Fixed in the PR below”

The native path now matches the ERC-20 path by using maximumEVMSendAmountToAmounts.

Bridge Safety Check Can Revert Instead Of Returning False, Disabling HyperEVM Fallback On Fills

“Fixed in the PR below”

Unused Error

  • High
  • Low

What this article says