Table of Contents

Summary

Type: DeFi
Timeline: 2026-06-01 → 2026-06-12
Languages: Solidity

Findings
Total issues: 13 (12 resolved)
Critical: 0 (0 resolved) · High: 0 (0 resolved) · Medium: 0 (0 resolved) · Low: 5 (4 resolved)

Notes & Additional Information
8 notes raised (8 resolved)

Client Reported Issues
0 reported issues (0 resolved)

Scope

OpenZeppelin performed an audit of the arcus-xyz/rootchain-contracts repository at the e950dbc commit.

In scope were the following files:

 src
├── BridgeVault.sol
├── CheckpointManager.sol
├── EmergencySettlement.sol
├── interfaces
│   ├── IBridgeVault.sol
│   ├── ICheckpointManager.sol
│   ├── IEmergencySettlement.sol
│   ├── IValidatorBeacon.sol
│   └── IValidatorConsensus.sol
├── lib
│   ├── BalanceAssertedTransfer.sol
│   └── InboxWriter.sol
├── ValidatorBeacon.sol
└── ValidatorConsensus.sol

The off-chain validator, the sidechain, and the off-chain proof generator are outside the scope of this review and are part of the trust assumptions described below. Deployment and post-deployment role configuration are likewise out of scope; role holders are stated as documented, not as verified on-chain.

System Overview

The system under review is the rootchain (EVM-side) half of a bridge connecting the rootchain to a sidechain that runs a hybrid perpetuals exchange. The exchange functionality executes fully off-chain, on the sidechain. The rootchain contracts exist to custody the bridged ERC-20 funds, carry messages in both directions between the two chains, gate every sidechain state commitment behind an on-chain validator vote, and give users a self-service escape hatch that stays available if the validators stop finalizing.

By deliberate design, these contracts enforce only transport-level invariants (message ordering, hash-binding of committed data, and per-token custody solvency) and delegate all semantic validity of the exchange state to the validators. They are built to be correct given that the validator set behaves honestly. They do not attempt to detect dishonest validator quorums, and the escape hatch defends against validator absence (a liveness failure), not validator dishonesty.

Users deposit permitted ERC-20 tokens through the BridgeVault contract, the sole custodian, which escrows the funds and enqueues opaque requests in the CheckpointManager inbox. The sidechain processes that request, and once the validator set reaches quorum in ValidatorConsensus, an executor finalizes a batch in CheckpointManager, committing a new state root and dispatching the batch side effects (such as crediting the deposit or authorizing a withdrawal) back through BridgeVault. If the validator stops consuming the inbox for long enough, anyone may activate emergency settlement, a one-way freeze of the last committed state root that also halts further finalization and inbox writes, after which users prove and reclaim their balances directly through EmergencySettlement and BridgeVault. ValidatorBeacon stands apart from this flow as a signaling-only registry and custodies nothing.

BridgeVault (BV)

The sole custodian of bridged ERC-20 funds on the rootchain and the sole writer into the CheckpointManager inbox. It tracks each token across three disjoint balance domains, pending-deposit, on-exchange, and pending-withdrawal, under an intended custody invariant that the real token balance covers their sum, and treats every fund movement as a transition between them. Deposits follow an initiate, confirm, and cancel lifecycle, while withdrawals are a two-step flow in which CheckpointManager authorizes the payout and a user-facing call executes the transfer.

CheckpointManager (CM)

The core of the bridge and the orchestrator of the escape hatch. It holds no funds and defers semantic validity to the validator, surfaced on-chain through a finalizer role. It maintains the inbox, an append-only FIFO of opaque request blobs consumed in strict order by finalized batches, and dispatches the side effects. The CheckpointManager is the sole authorizer of every BridgeVault payout, and it owns the emergency-settlement freeze, driving the settlement haircut from a validator-committed summary of pooled margin, total positive margin, and total bad debt.

ValidatorConsensus (VC)

The on-chain voting gate in front of CheckpointManager, converting validator votes into authorized finalizations and governance actions. For block finalization, validators vote on a compact block hash and, once a block reaches the threshold, an executor supplies the full header whose derived state root and commitments are forwarded to CheckpointManager. Consensus binds the block hash, not the logic that produced it: logicVersion and dataAvailability are informational and not in the quorum key. Quorum on identical hashes is thus agreement on the resulting state regardless of each validator's logic version — divergent logic yields a different hash and no quorum. For governance, validators vote on sequential, parent-chained action batches that execute atomically once quorum is reached. Votes are EIP-712 meta-transactions (including EIP-1271 contract signatures) with monotonic keyed nonces, and quorum is treated as an absolute validator count rather than a fraction.

ValidatorBeacon (VB)

A signaling-only, append-only registry of validator logic versions that holds no funds, makes no external calls, and enforces nothing on-chain. It records proposed versions and their metadata, supports permanent one-way cancellation, and accepts permissionless, unweighted opposition signals as a hint, leaving the choice of which version to run entirely to each off-chain validator.

EmergencySettlement (ES)

A stateless, pure sparse-Merkle inclusion-proof verifier holding no funds or storage, invoked by CheckpointManager during the escape hatch. Against a caller-supplied state root it proves an account's net margin and per-token collateral balances (and the pooled settlement summary), returning the proven figure to the caller.

Security Model and Trust Assumptions

The security of the system rests on a clear division of responsibility: the rootchain contracts guarantee correct transport, accounting, and custody solvency, while the correctness of exchange state is delegated to a fully trusted validator set. The escape hatch exists to bound the consequences of a validator liveness failure, not to remove trust in the validators' committed data.

The practical center of that trust is the validator quorum, which is the system's effective superadmin. Through ValidatorConsensus a quorum reaches both finalized blocks and governance actions, and because ValidatorConsensus, CheckpointManager, and the TimelockController each hold DEFAULT_ADMIN_ROLE on every governed contract, a quorum can exercise that admin authority directly through a governance action or through the side effects of a finalized block. Unlike the timelock path, which is delayed, publicly observable, and vetoable by the validator set, the validator path can immediately upgrade any contract, reconfigure custody, move funds, and even rewrite the rules of the quorum itself, lowering the threshold to as little as one or rotating the validator set in one atomic batch. Because the three superadmins are co-equal and mutually revocable, a quorum can also revoke the slower timelock admin and collapse governance to the instant path. A single malicious quorum event is therefore irreversible and total, so the operative assumption is honesty on every action, permanently.

The escape hatch guarantees that the exchange cannot lock user funds simply by halting: if the validator set stops consuming the inbox, the head entry ages past the configured delay and anyone can freeze the system and exit against the last finalized root. It does not, however, resist censorship by a validator set that stays live, because on-chain, consuming a request is decoupled from honoring it: advancing inboxHead requires only that a finalized batch's committed processedRequests match the inbox FIFO prefix, while the sideEffects are committed independently and nothing binds a consumed request to a resulting action. The permissionless raw-request path (the forced-withdrawal mechanism) therefore guarantees censorship-resistant entry but not execution: a live but dishonest quorum can keep consuming a user's request, leaving the staleness trigger disarmed, while never producing the payout. The hatch protects against validator absence, not a live validator that consumes without acting and user funds' recovery rests on validator accuracy of committed data.

  • External data integrity. Finalized batch contents (state roots, processed-request commitments, side effects) are assumed to be valid sidechain state transitions; the contracts cannot re-derive them. EmergencySettlement proves only that a leaf was included under a committed root, not that the committed value is economically correct. The same trust extends to the settlement haircut: its aggregates (pooled margin, total positive margin, total bad debt) are proven once from the frozen root for inclusion only, so their economic correctness and availability is assumed. Because that root is the last finalized one, it may lag live sidechain activity at the moment finalization stops, so emergency exits settle against the checkpoint, not necessarily each account's most recent positions in the side-chain.
  • Secure runtime and configuration. Emergency-settlement payouts still depend on administrative liveness and honesty: the absolute withdrawal pause blocks user exits even during a settlement, and because admin actions are not gated on the freeze, the admin can, mid-settlement, repoint the components the hatch relies on (the EmergencySettlement verifier and the BridgeVault). The hatch is thus trust-minimized against validator absence but not against malicious administration. The state tree is also assumed to use at most sixteen domains, a bound the hardcoded SETTLEMENT_SUMMARY_DOMAIN constant must respect.
  • Supported tokens. Only admin-allowlisted, well-behaved ERC-20 tokens are supported, and the unsupported failure modes differ: a token that under-delivers at deposit (fee-on-transfer or share-based) fails closed and cannot be deposited; a token that rebases down after deposit silently drops custody below the tracked balances and risks a run on the bank, so it must never be allowlisted; and a dormant-fee token can under-deliver to recipients on withdrawal. Suitability is a continuous assumption: an allowlisted token that later begins to pause, blacklist, or revert can block every flow for that token, including refunds, since the balance-delta wrappers sit on both legs with no bypass, and recovery could then need an administrative upgrade.
  • Collateral netting. On-chain code does not enforce the economic correctness of validator-committed values. In particular, non-margin collateral is paid out 1:1 regardless of the account's netMargin sign and is never netted against it on-chain, so guaranteeing that any collateral backing a deficit has already been absorbed into a negative netMargin is the sidechain's responsibility; if the validator commits a leaf where it has not, an underwater account can claim collateral it economically should not.
  • Deployment and environment. Several configuration properties (inter-contract wiring, per-token deposit settings, the raw-request spam parameters) are not enforced on-chain, so the trusted admin must set them correctly. The reentrancy guards use transient storage (EIP-1153), so deployment assumes Cancun-era EVM semantics.
  • Data availability. The hatch's soundness is unconditional on-chain: a prover can only submit leaves that reconstruct the frozen root, so figures cannot be faked. Its liveness, though, depends on off-chain data, since building a proof needs the true leaves and sibling hashes, which the contracts never publish (only the root is on-chain). The hatch therefore protects against a validator that stops, but not against one that stops and withholds the state: the root freezes, yet without the leaves no proof can be built and the funds cannot be exited.
  • Side-effect inclusion and atomicity. Because on-chain consuming a request is decoupled from honoring it, the system relies on the validator logic to pair every consumed request with its required side effects.

Governance

Administrative authority over the rootchain contracts is a single, fully trusted superadmin power (DEFAULT_ADMIN_ROLE) reachable through three paths, each held by a different authority and differing only in latency and coordination; all three rest on the one assumption of an honest validator set. This three-path model is the intended configuration described in docs/ACCESS_CONTROL.md, not a structure verified against a live deployment, which is out of scope. The TimelockController (TC) is a standard OpenZeppelin deployment included here to describe the model; its code and configuration were not part of the audited scope.

  • Timelock path (TC). The normal public route, where all non-emergency administrative actions are expected to flow. The Upgrade Proposer (UP, a multisig) schedules operations that execute only after minDelay, giving the public time to observe a pending change. UP is the most weakly trusted actor, since it can only propose: ValidatorConsensus holds the canceller role on TC and can veto anything UP schedules within the minDelay window, effective only if that window is long enough for a quorum to form. The same split also makes TC the recovery route for evicting a validator under a unanimity threshold (threshold equal to the validator count): both validator-driven paths then need the target's own vote to reach quorum, so only UP can schedule the removal.
  • Validator path (VC). ValidatorConsensus is itself a superadmin, letting a validator quorum act without the timelock delay, for emergencies such as vetoing a malicious timelock operation, shipping a zero-day upgrade, or an urgent reconfiguration.
  • Side-effect path (CM). CheckpointManager is a superadmin so that a validator-finalized batch can carry administrative actions as side effects; the documented rationale is coordinated upgrades spanning validator logic and the contracts. Its routine bridge calls (confirmDeposit, initiateWithdrawal) run on an onlyCheckpointManager address check and need no role; the DEFAULT_ADMIN_ROLE grant is load-bearing only for the governance side effects detailed below. Because those side effects are unrestricted low-level calls, finalization is an untimelocked admin channel over every governed contract, but one gated by the sideEffectsHash the validators voted on — so it adds no trust beyond what that quorum already holds through the validator-consensus path.

Privileged Roles

Role membership is set by deployment and post-deployment configuration, which is outside this review's scope, so every holder below is the one documented in docs/ACCESS_CONTROL.md, not a fact verified against a live deployment. As documented, the three superadmins (TC, VC, CM) hold DEFAULT_ADMIN_ROLE on every governed contract, so the per-contract entries below note only what that role controls there, plus the contract-specific roles.

CheckpointManager (CM)

  • DEFAULT_ADMIN_ROLE: manages roles, the emergency-settlement delay (which could be zero), the BridgeVault and EmergencySettlement addresses, and UUPS upgrades.
  • FINALIZER_ROLE (VC): finalizes batches and dispatches their side effects; the sole on-chain source of committed state roots.
  • INBOX_WRITER_ROLE (BV): writes requests into the FIFO inbox.

BridgeVault (BV)

  • DEFAULT_ADMIN_ROLE: sets per-token deposit configuration, the deposit expiry period, the raw-request spam parameters (gas burn and maximum payload size), both pause flags, the CheckpointManager address, and authorizes UUPS upgrades. The withdrawal pause is absolute over user exits, blocking executeWithdrawal and cancelDeposit, but not the CheckpointManager-driven initiateWithdrawal — so payouts can still be queued into the pending-withdrawal domain during a pause, with no rollback until withdrawals resume.

ValidatorConsensus (VC)

  • DEFAULT_ADMIN_ROLE: manages roles and configuration (the threshold and the CheckpointManager address) and authorizes UUPS upgrades; it can reshape the validator set and lower the threshold. The overridden revokeRole and renounceRole re-validate the threshold on validator removal, so under unanimity (threshold equal to the validator count) a removal or renouncement reverts until the threshold is lowered first.
  • VALIDATOR_ROLE (the validator signers): the voting set for both block finalization and governance actions. Quorum is an absolute count, so adding validators dilutes it until the threshold is raised. A vote is counted at execution time against the validator set and threshold then in force, not snapshotted when it was cast. Thus, lowering the threshold can immediately make another, already-stored action executable. Also, a validator's stored vote stops counting once it leaves the set and counts again if it rejoins. Both effects are confined to the next item in sequence, since finalization remains strictly in order.
  • EXECUTOR_ROLE (a designated executor): submits the quorum-consuming transaction and selects which quorum-reaching payload to finalize, but is trusted for liveness only and can finalize nothing the validators have not voted for. Finalization is all-or-nothing with no per-call gas limit, so the executor must fund the whole batch; recovering one that cannot finalize falls to the validators re-voting a corrected batch, and because finalization is strictly in order, a persistent failure stalls all later batches and can ultimately result in the emergency-settlement freeze.

ValidatorBeacon (VB)

  • DEFAULT_ADMIN_ROLE: manages the proposer and canceller roles and authorizes UUPS upgrades.
  • PROPOSER_ROLE (UP): registers new validator-logic versions.
  • CANCELLER_ROLE (UP): marks versions cancelled. The initializer additionally grants CANCELLER_ROLE to the proposer (deliberate per the in-code comment, but undocumented) — a no-op while the proposer is UP, but if the two are distinct addresses the proposer silently gains cancel authority too.

TimelockController (TC)

  • DEFAULT_ADMIN_ROLE (TC itself): administers the timelock roles through delayed operations.
  • PROPOSER_ROLE (UP): schedules delayed governance operations.
  • CANCELLER_ROLE (UP, VC): vetoes scheduled operations before execution; the VC grant is what gives the validator set override authority over anything UP proposes.
  • EXECUTOR_ROLE (UP): executes ready operations after the delay.

EmergencySettlement (ES)

  • Holds no roles. It is a stateless, pure proof verifier with no administrative surface, and is "upgraded" by deploying a fresh instance and repointing it through CheckpointManager, which is itself an administrative action on CheckpointManager.

Low Severity

Vote Retraction Does Not Invalidate Outstanding Meta-Transaction Vote Signatures

A validator may cast a vote directly or by issuing an EIP-712 signature that a relayer submits through voteBlockBySig or voteGovernanceActionBySig, where replay is prevented by a keyed nonce. The direct retraction functions unvoteBlock and unvoteGovernanceAction only clear the on-chain vote boolean; they do not consume the keyed nonce.

As a result, a validator that has handed a signature to a relayer and then retracts the vote has not invalidated that signature. If the signature has not yet been relayed, it can still be submitted afterward and will re-cast the vote. The only effective revocation is cancelBlockVoteSignatures or cancelGovernanceVoteSignatures, which consumes the nonce. The need to call both a retraction and a cancellation function is easy to overlook.

Consider having unvoteBlock and unvoteGovernanceAction also consume the keyed nonce for the relevant block or action number, so that a single retraction invalidates any outstanding signature. Alternatively, consider documenting that retracting a vote while a signature may be outstanding requires both the retraction call and the corresponding signature-cancellation call.

Update: Resolved in pull request #9 at commit 928c3f4. The Arcus team stated:

Added documentation for existing behavior.

  • 928c3f4 Documents that direct vote retraction does not invalidate unrelayed signatures and points validators to the matching nonce-cancellation functions.

Missing Zero-Value Parameter Validation

Several initializers and setters assign core parameters without checking them against zero or a lower bound, and most are set once or expensive to correct afterward. The unchecked address parameters are:

  • The initialize function of the CheckpointManager: admin (a zero admin leaves role administration unrecoverable), finalizer, inbox writer, and margin asset (a zero margin asset breaks settlement).
  • The initialize functions of the ValidatorConsensus and the BridgeVault: admin and the CheckpointManager address.
  • The validator set, where a seated address(0) can never sign and so dilutes the live set against the absolute quorum threshold.

Two numeric values relied on for liveness or spam resistance also lack a floor: the deposit expiry period and the raw-request gas burn behind the permissionless enqueueRawRequest. The emergencySettlementDelay is unbounded too, but documented to be an admin trust assumption.

Consider rejecting address(0) for the address parameters and validators in all three initializers, and enforcing a non-zero minimum for the deposit expiry period and raw-request gas burn in the initializer and their setters.

Update: Acknowledged, not resolved. The Arcus team stated:

Acknowledged. These contracts are deployed and initialized once by the root administrator, and correctness depends on auditing the complete deployment configuration rather than on isolated zero-value guards. A non-zero but incorrect address or parameter would be equally invalid, so zero is not treated as a special security boundary for initializer configuration.

The same principle applies for post-initialization configuration. Administrative setters are only callable by fully trusted governance, and each governance action must be reviewed as a complete configuration change. We therefore rely on deployment and governance review processes to validate all addresses, roles, thresholds, token configuration, and timing parameters before execution.

Permissionless Raw Inbox Requests Can Impersonate Typed Deposit Requests

BridgeVault writes two different kinds of entries into the CheckpointManager inbox through the same enqueueRequest entrypoint: typed deposits produced by InboxWriter, and arbitrary opaque blobs submitted through enqueueRawRequest. CheckpointManager stores only the payload hash and timestamp and emits the payload for data availability; it performs no decoding, and the RequestEnqueued event carries no information about which path produced the entry.

Because enqueueRawRequest forwards the caller-supplied bytes unwrapped, any address can enqueue a payload that is byte-for-byte identical to the typed deposit encoding produced by writeDeposit, namely abi.encode(TYPE_DEPOSIT, depositor, owner, accountIndex, token, amount, expiresAt), for an arbitrary owner and amount, without transferring any funds. A validator that ingests deposits by decoding inbox payloads alone cannot distinguish such a forged entry from a genuine deposit that BridgeVault actually recorded, and would credit the sidechain account with funds that were never custodied.

On-chain, the forged entry has no corresponding PendingDeposit, so confirmDeposit reverts with PendingDepositNotFound and a batch that attempts to confirm it cannot finalize; a forged payload therefore cannot move funds on-chain. The atomicity rule that a sidechain credit must be applied only when the corresponding on-chain confirmDeposit finalizes is a pre-existing design requirement, not a consequence of this issue. The concern here is narrower: because the contracts enforce no on-chain separation between the two channels, correctness rests on validators honoring that rule and never crediting deposits from inbox bytes alone. That reliance is a matter of validator-discipline and liveness consideration rather than an enforced on-chain guarantee.

Consider domain-separating the two channels so that a raw request can never be decoded as a typed deposit, for example by wrapping raw payloads under a reserved type tag that cannot collide with TYPE_DEPOSIT, or by routing raw requests through a distinct inbox namespace. Consider additionally specifying that a deposit credit must be gated on the on-chain DepositInitiated event or a confirmDeposit round-trip, and never on the raw inbox payload alone.

Update: Resolved in pull request #10 at commit 96a45fe. The Arcus team stated:

We resolved this by adding NatSpec to BridgeVault, IBridgeVault, and InboxWriter explicitly stating that raw request payloads are untrusted bytes, may decode like typed deposits, and must not be used by validators to credit deposits from inbox bytes alone. A deposit credit is real only when tied to BridgeVault's on-chain deposit lifecycle and successful confirmDeposit finalization.

Proven Funds May Strand Since Emergency-Settlement Payouts Are Recipient-Only

BridgeVault settles every payout in two steps. CheckpointManager first authorizes a payout through initiateWithdrawal, which moves the amount from the onExchange domain into pendingWithdrawals and records a PendingWithdrawal. The funds leave the vault only when executeWithdrawal is later called, which is the sole function that clears the record and performs the ERC-20 transfer. When a record is created with anyoneCanExecute set to false, executeWithdrawal restricts execution to the recorded recipient and reverts with NotRecipient for any other caller.

Both emergency-settlement payouts are created with this flag set to false. proveSettlementMargin and proveSettlementCollateral each call initiateWithdrawal with anyoneCanExecute set to false and the recipient set to the proven account owner. The escape hatch exists to return user funds when the validator set stops finalizing, yet a recipient-only payout can be executed only by the owner address itself. If that owner cannot originate the call, for example a contract without a suitable entrypoint, an address that no party controls, or the zero address, the proven funds remain locked in the pendingWithdrawals domain with no recovery path, even though the inclusion proof succeeded. This makes the liveness of the escape hatch depend on the recipient's ability to transact, a dependency the trust model does not otherwise place on emergency exits.

Consider creating the emergency-settlement payouts with anyoneCanExecute set to true. Because executeWithdrawal always transfers to the recorded recipient and never to the caller, permissionless execution still delivers the funds to the proven owner and cannot redirect them, which removes the liveness dependency at no additional risk.

Update: Resolved in pull request #12 at commit 1b2b443 and at commit 0581a08. The Arcus team stated:

Made the code update to make withdrawal triggering permissionless. The original, permissioned design had been intentional, and matches e.g. StarkEx, but on further reflection and reviewing patterns on other bridge systems, have decided permissionless is preferred here.

Commits:

  • 1b2b443 Makes emergency-settlement margin and collateral payouts permissionless to execute while preserving owner-bound recipients and adds regression coverage for both paths.

  • 0581a08 Documents that settlement payouts are permissionless for exit liveness while BridgeVault still transfers only to the proven owner.

Deposits To A Zero-Address Owner Can Strand Funds After Confirmation

Each deposit records an owner identifying the sidechain account to credit, separate from the funder and the cancel recipient. The shared _initiateDeposit helper rejects a zero cancel recipient but not a zero owner, which is reachable through initiateDepositReturnToSender, while the other two entrypoints already block it. The helper escrows the funds and writes the unvalidated owner into the CheckpointManager inbox payload.

While an unconfirmed deposit can be reclaimed through cancelDeposit, a confirmed one credits the zero-address account on the sidechain, and every outcome past that point rests on off-chain validator behavior that is out of scope. In normal operation the withdrawal recipient is a free parameter on initiateWithdrawal and is not bound on-chain to the owner, so the contracts permit an exit to a usable address. However, a validator that ties the recipient to the account owner would pay only the zero address. In emergency settlement the payout recipient equals the owner proven in the committed leaf, so the funds are stranded only if the frozen root actually contains a zero-owner leaf with a claimable balance, since CheckpointManager then sets the recipient to that owner with anyoneCanExecute set to false, which can never execute. In each path the contracts faithfully propagate a zero owner they never validated, so the resulting loss depends on the validator implementation rather than on the contracts alone.

Consider rejecting a zero owner in _initiateDeposit, mirroring the existing cancel-recipient check, so the contracts never forward a zero account into the inbox or pin an unexecutable emergency-settlement payout, regardless of how the off-chain validator handles it.

Update: Resolved in pull request #11 at commit b5ba714.

Notes & Additional Information

NatSpec References Documentation Files That Do Not Exist

The EmergencySettlement interface directs readers to docs/EMERGENCY_SETTLEMENT.md for the full specification, but that file is not present in the repository. The same dangling reference appears in ICheckpointManager (docs/CHECKPOINT_MANAGER.md) and InboxWriter (docs/BRIDGE_VAULT.md).

Consider adding the referenced documents before release, or removing the pointers until they exist, so the NatSpec does not direct readers to missing files.

Update: Resolved in pull request #1 at commit a3ce92d and at commit 66c59c3. The Arcus team stated:

We chose to remove outdated references to external docs as we already have clear and thorough documentation inline within each implementation contract.

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.

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 #6 at commit 10eeb40.

External/Public Interfaces Are Unevenly Documented

NatSpec coverage is uneven: the security-critical surfaces are documented, but several external and public functions carry no NatSpec, @param/@return tags are essentially unused, and no contract uses @inheritdoc, so interface and implementation docs are maintained separately and can drift.

Consider completing NatSpec coverage on the remaining external/public functions and linking implementations via @inheritdoc to keep the two in sync.

Update: Resolved in pull request #2 at commit 83a5773, 8341258 and at commit 3f187f6. The Arcus team stated:

We standardized the NatSpec documentation model across the public contract surface. Interfaces now serve as the canonical ABI documentation layer, with external/public functions documented using @notice, @param, and @return where applicable. Implementations now link back to the interface docs using @inheritdoc, while retaining implementation-specific @dev notes for protocol invariants, trust assumptions, and behavior that is useful when reading the contract source directly.

Missing Initialization Call for Inherited Nonces Module

The initialize of the ValidatorConsensus contract chains the initializers of its inherited bases but omits __NoncesKeyed_init, despite inheriting from NoncesKeyedUpgradeable. There is no functional impact, since the function is empty in OpenZeppelin Contracts Upgradeable 5.2.0, but the initialization chain should remain complete for consistency and to stay resilient to future dependency versions.

Consider adding a call to __NoncesKeyed_init in the initialize function.

Update: Resolved in pull request #5 at commit 7436c09.

Initializers Do Not Emit Configuration Events

Each upgradeable contract sets meaningful configuration in its initialize function but emits no event, even though the runtime setters for the same parameters do. BridgeVault sets the CheckpointManager address and the deposit-expiry and raw-request parameters; CheckpointManager sets the emergency-settlement delay; ValidatorConsensus sets the CheckpointManager address and threshold; and ValidatorBeacon writes the genesis validator-logic version. Each of these has a corresponding update event on its setter, so an off-chain consumer that reconstructs configuration from events observes every later change but never the initial baseline.

Consider emitting from each initializer the same events the corresponding setters emit on update, so the initial configuration is observable through the same event stream as subsequent changes.

Update: Resolved in pull request #3 at commit 792c411.

Latest Version Getter Can Return a Cancelled Version

The latestVersion getter returns the highest proposed version, but cancelVersion only sets a flag and without shifting numbering, so cancelling the highest version leaves latestVersion pointing at a cancelled entry. A consumer that reads latestVersion directly, without walking down to the greatest non-cancelled version, could select cancelled logic.

Consider documenting on the latestVersion getter that the returned version may be cancelled and is not necessarily active.

Update: Resolved in pull request #4 at commit 1d5a506.

Margin Pool Surplus Above Net Equity Has No Settlement Claim Path

During emergency settlement, positive-margin payouts are computed by _haircutPayout as positiveMargin * min(M, P - N) / P, where M is the committed margin pool, P is the total positive margin, and N is the total bad debt. When the pool is over-collateralized, meaning M > P - N, the distributable amount is capped at P - N and the surplus M - (P - N) is never paid out. Because the margin pool is a custody share distinct from the 1:1 collateral leaves, this surplus has no settlement claim path and remains locked in BridgeVault custody after every margin holder has exited. This matches the documented design and affects neither solvency nor the correctness of any payout, but the residual is unrecoverable through the settlement flow, unless the contract is upgraded.

Consider documenting that any margin pool balance exceeding net equity at settlement is not claimable through the haircut distribution, and, if recovery of such a residual is desired, providing a governed path to reclaim it.

Update: Resolved in pull request #7 at commit a221c44. The Arcus team stated:

We have documented the behavior: the expected settlement summary has M == P - N and that any committed surplus has no emergency-settlement claim path.

Unattributed Token Custody Cannot Be Recovered From BridgeVault

BridgeVault maintains the custody invariant tokenBalance(token) >= A + B + C across its pending-deposit, on-exchange, and pending-withdrawal domains. Two paths let the actual balance exceed the tracked sum: transferIn credits only the requested amount and tolerates a larger realized transfer, leaving the excess unattributed, and a direct ERC-20 transfer to the vault credits no domain at all. The contract exposes no sweep or recovery function, so any over-received or donated balance is permanently locked, unless BridgeVault is upgraded. This only strengthens the solvency invariant and cannot affect user payouts, so there is no impact beyond the stranded tokens themselves.

Consider documenting that any token balance in excess of A + B + C is unattributed and not reclaimable through the normal deposit and withdrawal flows, and, if recovery of such a residual is desired, providing a governed path that transfers out the excess without weakening the solvency invariant.

Update: Resolved in pull request #8 at commit 7c8f238. The Arcus team stated:

We have added documentation: BridgeVault token balance above tracked custody domains is unattributed and not recoverable through normal protocol flows.

Conclusion

The engagement covered the rootchain contracts comprising the bridge and checkpoint system: CheckpointManager, BridgeVault, ValidatorConsensus, ValidatorBeacon, and EmergencySettlement, together with their supporting interfaces and libraries.

No significant issues were identified during the review. The codebase is well structured, with clear separation between the message-transport layer, the custody and accounting layer, the validator consensus gate, and the stateless settlement-proof verifier. Trust boundaries are made explicit, privileged roles are scoped deliberately, and the accepted consequences of the validator and administrator trust assumptions are documented directly in the source. Only minor, non-security observations were noted, none of which affect the correctness, safety, or custody guarantees of the system.

By design, the contracts enforce only transport-level correctness and delegate all validity of the exchange state to the validator set. A validator quorum is the system's effective superadmin: it can move funds, reconfigure custody, and upgrade any contract in a single action, with no delay and no veto. The security of user funds therefore rests on the validator set behaving honestly. The emergency-settlement escape hatch narrows this trust but does not remove it. It protects against validators going absent, a liveness failure, by letting users exit against the last finalized root. It does not protect against a validator set that stays live and acts dishonestly, such as by censoring requests or committing incorrect state.

The development team is encouraged to maintain the current standard of documentation and testing as the protocol evolves, and to re-audit any future changes to the validator trust model, the side-effect dispatch path, or the emergency-settlement accounting, as these areas concentrate the most critical assumptions of the system.

Appendix

Issue Classification

OpenZeppelin classifies smart contract vulnerabilities on a 5-level scale:

  • Critical
  • High
  • Medium
  • Low
  • Note/Information

Critical Severity

This classification is applied when the issue’s impact is catastrophic, threatening extensive damage to the client's reputation and/or causing severe financial loss to the client or users. The likelihood of exploitation can be high, warranting a swift response. Critical issues typically involve significant risks such as the permanent loss or locking of a large volume of users' sensitive assets or the failure of core system functionalities without viable mitigations. These issues demand immediate attention due to their potential to compromise system integrity or user trust significantly.

High Severity

These issues are characterized by the potential to substantially impact the client’s reputation and/or result in considerable financial losses. The likelihood of exploitation is significant, warranting a swift response. Such issues might include temporary loss or locking of a significant number of users' sensitive assets or disruptions to critical system functionalities, albeit with potential, yet limited, mitigations available. The emphasis is on the significant but not always catastrophic effects on system operation or asset security, necessitating prompt and effective remediation.

Medium Severity

Issues classified as being of medium severity can lead to a noticeable negative impact on the client's reputation and/or moderate financial losses. Such issues, if left unattended, have a moderate likelihood of being exploited or may cause unwanted side effects in the system. These issues are typically confined to a smaller subset of users' sensitive assets or might involve deviations from the specified system design that, while not directly financial in nature, compromise system integrity or user experience. The focus here is on issues that pose a real but contained risk, warranting timely attention to prevent escalation.

Low Severity

Low-severity issues are those that have a low impact on the client's operations and/or reputation. These issues may represent minor risks or inefficiencies to the client's specific business model. They are identified as areas for improvement that, while not urgent, could enhance the security and quality of the codebase if addressed.

Notes & Additional Information Severity

This category is reserved for issues that, despite having a minimal impact, are still important to resolve. Addressing these issues contributes to the overall security posture and code quality improvement but does not require immediate action. It reflects a commitment to maintaining high standards and continuous improvement, even in areas that do not pose immediate risks.