Guardian's review of LayerZero Integration for Olympus, published June 2026. The report records 20 findings across 2 review rounds, including 1 medium and 3 low.
- Published
- Review window
- April 16 to June 2, 2026
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Ethereum, Arbitrum, Optimism, Base, Berachain
- Sector
- Tokens
- 0 Critical
- 0 High
- 1 Medium
- 3 Low
- 16 Informational
Scope
5 files in scope · 997 nSLOC
| File | nSLOC | Lines |
|---|---|---|
src/periphery/bridge/LZCrossChainBridge.sol | 60 | 106 |
src/policies/bridge/LZBridgeGateway.sol | 297 | 568 |
src/libraries/LZConfigLib.sol | 161 | 280 |
src/proposals/LZBridgeSecurityUpgradeProposal.sol | 329 | 448 |
src/proposals/LZBridgeActivator.sol | 150 | 235 |
Findings 20
Main Review
16 findings · April 16 to 21, 2026-
M-01 Medium Optional DVNs inherit LayerZero defaults Logical Error Resolved
Description
The bridge intends to pin LayerZero verification to exactly two required DVNs per route, but LZConfigLib.encodeUlnConfig() leaves optional DVNs in LayerZero’s default-inherited state.
The helper encodes optionalDVNCount = 0 with an empty optionalDVNs array. In LayerZero V2 OApp ULN config, 0 does not mean “zero optional DVNs”; it means “use the default config.” To explicitly configure no optional DVNs, the value must be type(uint8).max.
This conflicts with the proposal’s stated goal of eliminating LayerZero default drag-along and using dual-DVN verification: LayerZero Labs + Google Cloud for non-Berachain routes, and LayerZero Labs + Nethermind for Berachain routes. If current defaults have no optional DVNs, the resolved config may look correct today, but the raw app config still inherits defaults.
If LayerZero later adds default optional DVNs or changes the optional DVN threshold for a route, Olympus can inherit that change without governance action. Messages may then require an extra default DVN, increasing fees or blocking delivery if that DVN is unavailable or unsupported. In the bridge flow, users can burn OHM on the source chain while the destination mint remains stuck until the inherited requirement is satisfied or admins recover the message.
Recommendation
Use LayerZero’s NIL sentinel for no optional DVNs: set optionalDVNCount = type(uint8).max, optionalDVNThreshold = 0, and keep optionalDVNs empty.
-
L-01 Low Peer Update Strands Verified Messages Compatibility Resolved
Description
Calling
setPeercan update or clear the peer for an EID. If messages from that peer are already verified on the LZ endpoint but not yet executed, they become permanently undeliverable, andlzReceivereverts on the peer check. Admin must proceed to manually recover each stranded message (i.e., skip, clear).Recommendation
Document this behavior. Consider a two-step peer removal (disable receives → wait for in-flight messages to settle → remove peer) or check for pending messages before clearing.
-
L-02 Low Bridge fee event can overstate paid fee Events Resolved
Description
LZCrossChainBridge.sendOhm() emits msg.value as the fees field in Bridged, but msg.value is the native value supplied upfront, not necessarily the actual LayerZero fee paid.
LayerZero V2 refunds excess native value to the configured refund address. In this bridge flow, the gateway passes the user as the refund address, so any overpayment is returned, but the event still records the full original msg.value.
The actual fee charged by LayerZero is returned in MessagingReceipt.fee, which the gateway receives from EndpointV2.send(). However, that value is not surfaced in the Bridged event.
This could also lead to misaccounting in off-chain accounting systems that rely on the emitted fees value.
Recommendation
Emit MessagingReceipt.fee.nativeFee as the paid fee, or rename/document the field as native value supplied.
-
L-03 Low L2 Post-Batch Validation Omits Endpoint Checks Validation Resolved
Description
LZBridgeGatewayL2Batch._validateConfigureAndEnableverifies the gateway is enabled, peers are set, and enforced options exist. However, unlike the Ethereum proposal, it does not verify that the LZ libraries are pinned (SendUln302/ReceiveUln302), ULN config (DVN addresses, confirmations), or executor config, The Ethereum proposal's_validatereads these values and endpoint state directly. A misconfigured L2 endpoint setup would therefore not be caught by the L2 post-batch validation.Recommendation
Extend
_validateConfigureAndEnableto read endpoint configuration via the gateway'sILZEndpointV2Adminview functions and verify libraries, DVNs, confirmations, and executor match the expected values. Mirror the checks inLZBridgeSecurityUpgradeProposal._validateLZConfig. -
I-01 Informational No Receive-Side Validation Of Recipient Validation Resolved
Description
_receiveBridgeOhmdecodes(address to, uint256 amount)from the cross-chain message but does not validateto. The send side checksto != address(0), but the receive side trusts the payload entirely. If a situation occurs whereto = address(0)is sent to the receiver, the OHM mint reverts, stranding the message. Ifto = address(gateway), OHM is minted to the gateway and locked with no recovery path.Recommendation
Add
_requireNonzeroAddress(to, "to")in_receiveBridgeOhm, consider also rejectingto == address(this). -
I-02 Informational Skip/Burn Bypass bridgedSupply Accounting Trust Assumptions Resolved
Description
The LZ V2 message recovery functions
skip,burn, andclearare executed directly to the endpoint without adjustingbridgedSupplyor mint approval on the canonical chain. Whenbridge_adminuses skip or burn to permanently discard an inbound message, the OHM burned on the source chain is never minted on the destination, butbridgedSupplyremains inflated. Admin must manually calldecreaseBridgedSupplyto enforce thebridgedSupplyand mint approval are up-to-date, while nothing in the code enforces or signals this.Recommendation
Document that
skipandburnof messages require a correspondingdecreaseBridgedSupplycall on the canonical chain. -
I-03 Informational No Token Rescue On Gateway Best Practices Resolved
Description
Neither
LZBridgeGatewaynorLZCrossChainBridgehas a mechanism to recover tokens accidentally sent to them. ERC20 and native token is permanently locked, and since the gateway's lzReceive is payable, this can increase the chance user stuck funds.Recommendation
Consider adding an admin-gated rescue function for stuck tokens.
-
I-04 Informational Dual-Bridge Window During Migration Trust Assumptions Acknowledged
Description
The OCG proposal enables the new
LZBridgeGatewaybut does not deactivate the oldCrossChainBridge. Deactivation is deferred to a separate DAO multisig batch after proposal execution. During this window both bridges are active. The old bridge lacksbridgedSupplytracking, DVN pinning, and receive-side disable checks. Transfers through the old bridge during this window bypass bridgedSupply tracking, explicit DVN pinning, and receive-side disable controls.Recommendation
Minimize the window between proposal execution and old bridge deactivation. Document the maximum acceptable delay and ensure monitoring is in place to alert if the old bridge is used after the new one is live.
-
I-05 Informational sendOhm Does Not Validate Recipient Address Gas Optimization Resolved
Description
LZCrossChainBridge.sendOhmvalidatesamount_ != 0but does not checkto_ != address(0). The gateway'sburnAndSenddoes validate this, but the user has already paid gas for the OHM transfer before hitting that revert.Recommendation
Add
_requireNonzeroAddress(to_, "to")insendOhmto fail fast. -
I-06 Informational Consider Additional DVN For Added Resilience Suggestion Resolved
Description
The bridge uses 2-of-2 required DVNs per route, which meets LayerZero's multi-DVN recommendation and would have added an extra layer of protection against the recent KelpDAO exploit (April 2026) which relied on a 1-of-1 configuration. However, a 2-of-2 setup means compromising both DVNs still enables message forgery. A 3-of-3 configuration would provide additional resilience against attackers targeting DVN infrastructure, as demonstrated in the KelpDAO incident.
Recommendation
Evaluate whether adding a third required DVN is operationally feasible to add an extra layer of security.
-
I-07 Informational Unused V1 Chain ID Constants In LZConfigLib Superfluous Code Acknowledged
Description
LZConfigLibdefines V1 LayerZero chain ID constants (ETH_CHAIN_ID = 101,ARB_CHAIN_ID = 110, etc.) that are not referenced anywhere in the codebase. All active configuration uses the V2 endpoint IDs (ETH_EID = 30101,ARB_EID = 30110, etc.).Recommendation
Remove the unused V1 chain ID constants to reduce confusion between V1 and V2 identifiers.
-
I-08 Informational Old Bridge Deactivated Without Disabled Check Trust Assumptions Resolved
Description
LZCrossChainBridgeBatch.setupdeactivates the oldCrossChainBridgein the Kernel without first verifying thatbridgeActive == false. A separatedisableOldBridgeentry point exists and is expected to run beforehand, butsetupdoes not enforce this ordering. If setup is ran beforedisableOldBridge, user transactions submitted in the intervening window could still interact with the old bridge.Recommendation
Add a pre-condition check in setup that verifies the old bridge's
bridgeActivestate is already false, or include thedisablecall in the same batch to guarantee ordering. -
I-09 Informational Unresolved TODO Comments Best Practices Acknowledged
Description
Several TODO comments remain in the proposal, deployment scripts, and batch scripts that should be resolved before deployment:
LZBridgeSecurityUpgradeProposal.sol: proposal ID is hardcoded to 15 with a TODO. The proposal description contains TODO placeholders for the audit report link, PR link, and RFC/OIP reference.LZBridgeGatewayBatch.sol: initBridgedSupply reads from an args file with a TODO noting the value must be set before execution.DeployV3.s.sol: TODOs flag refactoring and error handling improvements.Recommendation
Resolve all TODO comments before deployment, followed by removing the comments to avoid confusion.
-
I-10 Informational Enabled/Disable Events Omit Data Parameters Events Acknowledged
Description
PolicyEnabler.enableandPolicyEnabler.disableeach accept a bytes calldata data parameter (enableData_ and disableData_) but do not include these in the Enabled and Disabled events. For policies that use this data for configuration at enable/disable time, the values are not captured in on-chain logs.Recommendation
Consider emitting
enableData_anddisableData_as part of the Enabled and Disabled events. -
I-11 Informational Non-Canonical Chains Have No Minting Cap Trust Assumptions Resolved
Description
In non-canonical chains,
_receiveBridgeOhmuses JIT self-approval with no upper bound:MINTR.increaseMintApproval(address(this), amount); MINTR.mintOhm(to, amount);If for example LayerZero verification infrastructure for a specific route is compromised (e.g. DVN compromised as demonstrated by the KelpDAO incident in April 2026), unlimited
OHMcan be minted on the non-canonical chain. The canonical chain'sbridgedSupplycap does not protect non-canonical chains.The
RateLimiterinherited by the gateway caps outbound transfers only._inflowreduces consumed outbound capacity, freeing it for further sends, rather than capping inbound volume. Rate limits therefore do not provide an effective cap on inbound mints if LZ verification is compromised.Recommendation
Consider whether a
maximum-mint capshould be introduced at the gateway level on non-canonical chains as an additional defense layer. -
I-12 Informational Periphery Can Use Stale Gateway Informational Resolved
Description
The saved LZ bridge deployment sequences deploy LZCrossChainBridge before LZBridgeGateway.
DeployV3.deploy() executes the sequence in order and stores each deployed address in the in-memory deployedTo map only after that deployment completes. DeployV3._getAddressNotZero() first checks this in-memory map, then falls back to env.json.
However, deployLZCrossChainBridge() reads olympus.policies.LZBridgeGateway during construction. Since LZBridgeGateway appears later in both saved LZ deployment sequences, the newly deployed gateway is not available in deployedTo when the periphery is constructed.
As a result, the periphery either reverts if env.json has no gateway address, or it is deployed with whatever gateway address was already saved in env.json. If env.json contains an older gateway address, the new periphery is wired to that stale gateway instead of the gateway deployed later in the same sequence.
Recommendation
Deploy LZBridgeGateway before LZCrossChainBridge, or explicitly require that env.json already contains the intended gateway address before deploying the periphery.
Remediation Review
4 findings · May 28 to June 2, 2026-
I-13 Informational Stale
_subActionTargetKindStorage Best Practices ResolvedDescription
When a queued action is executed or cancelled,
_queuedActions[actionId].actionsis deleted, but the per queued sub-action_subActionTargetKind[actionId][index]mapping entries are not. These entries remain in storage permanently after the action completes, even though they are never read again.Recommendation
Clear
_subActionTargetKind[actionId][index]entries in the execution and cancellation paths. -
I-14 Informational Queued Actions Remain After Policy Disable Documentation Resolved
Description
When
LZBridgeAndDelegateConfigis disabled for security purposes,_validateExecutionblocks execution of queued actions via_requireEnabled(), but the queued actions remain in storage.If the policy is disabled and later re-enabled, any previously-queued actions (whose execution windows have not expired yet) become executable again. An operator who re-enables the policy without first cancelling all potentially dangerous queued actions could allow a malicious action to complete.
Recommendation
Document the operational procedure that all malicious queued actions must be cancelled by the emergency role before the policy is re-enabled.
-
I-15 Informational Cross-Batch Execution Order Is Not Enforced Documentation Resolved
Description
Within a queued batch, sub-actions execute atomically in array order. Across separate queued actions, there is no enforced ordering, and execution is permissionless, so two independently-queued batches can be executed in any order once their timelocks elapse. Operators who queue dependent actions in separate batches cannot rely on them executing in queue order.
Recommendation
Document that actions with execution-order dependencies must be queued within a single batch, and that independent batches should not be assumed to execute in queue order.
-
I-16 Informational Delegate Clear Docs Mismatch Documentation Resolved
Description
The ILZBridgeGateway.setDelegate() interface documents that passing address(0) clears the LayerZero endpoint delegate.
However, LZBridgeGateway.setDelegate() calls validateSetDelegate(delegate_), and validateSetDelegate() rejects the zero address. The timelocked LZBridgeAndDelegateConfig path also calls the same validation helper before queueing setDelegate, so zero-address delegate clearing is not available through either direct or timelocked configuration.
As a result, operators or scripts following the interface documentation may attempt to clear the delegate with address(0) and see the action revert at validation or execution time. The implementation still supports replacing the delegate with another nonzero address, so this is a documentation/API consistency issue rather than a loss of delegate control.
Recommendation
Update the interface documentation to state that delegate_ must be nonzero.
No findings match.
More from Olympus
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
