Orderly engaged Guardian to review the security of their Orderly Solana Vault Review. From the 10th of October to the 22nd of October, a team of 2 auditors reviewed the source code in scope.
- Published
- Review window
- October 10 to 22, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Solana
- Sector
- Perpetuals
- 0 Critical
- 1 High
- 3 Medium
- 18 Low
- 19 Informational
Scope
-
gitlab.com/orderlynetwork/orderly-v2
bf11f8eae6883f5d103fff0681adfad993eec7ad636b0b733ca3ac7aa5b7
Overview
Orderly engaged Guardian to review the security of their Orderly Solana Vault Review. From the 10th of October to the 22nd of October, a team of 2 auditors reviewed the source code in scope.
Findings 41
Main Review
26 findings-
H-01 High Broken Multi-Vault Support Due Logical Error Resolved
Description
Proof of concept: PoC
The
ProtocolVaultLedgernow supports multiple vaults with the latest updates. Each new vault has its own namespaced storage slots, while theprotocolVaultis accessed through the regular storage. These vaults also have their own brokers, which are stored in thevaultBrokermapping.Previously, during the
distributeAssetsflow from the ledger to the vault chains, theStrategyVaultCCMessagedid not include the vault address. As a result, thedepositToStrategyfunction on the receiving vault chain was always being called on theprotocolVault.After the multi-vault updates, the
StrategyVaultCCMessagenow includes the [vault address]((a Guardian proof of concept 2315c687ec2bde6e39692c691f129/contracts/Ledger/LedgerCoreImpl.sol#L368)), and that specific address is invoked during_lzReceive:ttps://github.com/GuardianOrg/strategy-vault-team1-1760042507908/blob/bf11f8eae622315c687e c2bde6e39692c691f129/contracts/VaultCrossChainManager.sol#L145
However, the
depositToStrategyfunction in theProtocolVaultcontract still uses the hardcodedORDERLY_BROKERwhen retrieving thevaultIdand preparing theVaultDepositFEpayload. Since these other vaults will not always haveORDERLY_BROKERas their broker, messages sent from the ledger will be misinterpreted on the vault chains.The same issue is present in the
updateUnClaimedanddepositFromStrategyfunctions as well. However, the impact of this issue is limited, as thevaultIdis only used for event emission.Recommendation
To support multiple strategy vaults with different brokers, these broker hashes should also be included in the cross-chain message, and the
ProtocolVaultshould fetch vaultIds using the vault-specific brokers rather than hardcodedORDERLY_BROKER.Resolution
Orderly Team: The issue was resolved in commit 1aa2a4b.
-
M-01 Medium CCTPv2 Rebalance Credits Ignore Bridge Fees Logical Error Resolved
Description
The
CCTPv2migration assumes that the bridge mints exactly the quantity that was burned, but Circle’s v2 flow now deducts a fee before minting on the destination chain.During the burn path the vault approves the full
data.amountand whendepositForBurnsucceeds, forwards that same value back to the ledgerCCTPv2 documentation clarifies that only
amount - feeis minted on the destination chain, yet once the mint completes the vault still reports the originaldata.amountThe
Ledgerbooks this figure by callingfinishMintToken, which blindly incrementstokenBalanceOnchainwith the reported amountAs a result, every rebalance inflates the on-chain balance tracker even though the vault’s actual holdings lag by the CCTP fee. Over time this mismatch causes withdrawals to revert (vault balance lower than expected) or leaves liabilities unbacked.
The
LayerZeromessage emitted byburnFinishis still necessary: CCTP itself only moves USDC, while theLayerZerochannel carries the authoritative status update back to the ledger so it can mark the burn as complete, update accounting and coordinate the matching mint.Because the payload currently carries the unadjusted burn amount, the ledger adopts the wrong balance.
Recommendation
Capture the net quantity that actually arrives on the destination chain—either by measuring the vault’s token balance before and after
receiveMessage, or by decoding theCCTPv2payload to read the minted amount—then pass that net value (and optionally the fee for auditability) intomintFinishandfinishMintTokeninstead of the originaldata.amount.Resolution
Orderly Team: The issue was resolved in commit 0290854.
-
M-02 Medium Escrow Bypass In Solana V2 Withdrawal Validation Resolved
Description
The new Solana withdrawal pipeline lives in
LedgerImplD._executeWithdraw2SOL. Inside the state block that decides whether a withdrawal can proceed we now only guard on the raw ledger balance.Compare that to the legacy paths:
LedgerImplA.executeWithdrawAction(the main EVM route) andLedgerImplC.executeWithdrawSolAction(the Solana V1 bridge) both subtract the escrow balance before allowing a payout
That subtraction is critical because internal transfers first credit a user by bumping both
account.balancesand the escrow bucket:Only when the matching debit arrives (or the transfer is otherwise finalized) do we release the escrowed units.
Because
_executeWithdraw2SOLnever subtractsescrowBalances, the Solana V2 route treats those in‑flight credits as immediately spendable. A malicious account can:- Arrange for an internal transfer (or any engine action that posts a credit) so that
account.balances[tokenHash]increases while
escrowBalances[accountId][tokenHash]carries the same amount. 2. Call Solana V2 withdrawal for that amount. The existing guardaccount.balances[tokenHash] < withdrawV2.tokenAmountpasses, the withdrawal is frozen/finished, andILedgerCrossChainManagerV2.withdraw2ContractV2is invoked to deliver real USDC to Solana. 3. Later, when the original transfer fails or is rolled back,_finalizeTransfersimply clears the escrow entry; there is no clawback because the withdrawal already drained the funds. This way, the attacker double-spent the escrowed amount.Since the Solana V2 driver also calls
finishFrozenBalanceimmediately (no async result), there is no later chance to detect the deficit.Similarly, the
_executeWithdraw2EVMflow does not check escrow balances, allowing them to be withdrawn.Recommendation
Keep both the
_executeWithdraw2EVMand_executeWithdraw2SOLpaths consistent with the other withdrawal implementations by deducting escrow before approving the withdrawal.Run this check in the same state block that handles nonce/fee validation, reuse the existing error code (state = 9) or call the dedicated revert helper and maintain feature parity across all withdrawal flows. That way, users cannot cash out credits that are still unresolved elsewhere in the engine.
Resolution
Orderly Team: The issue was resolved in commit f493ebf.
-
L-01 Low Native Deposit Overpay Burns Funds Validation Resolved
Description
The native deposit path trusts whatever
msg.valuethe caller forwards and only checks that the amount covers the requested deposit. Inside_ethDepositthe code derives acrossChainFeeasmsg.value -data.tokenAmount.When
depositFeeEnabledis false the branch simply callsIVaultCrossChainManager(crossChainManagerAddress).deposit(depositData)without forwarding the fee or returning it:uint128 nativeDepositAmount = msg.value.toUint128(); if (nativeDepositAmount < data.tokenAmount) revert NativeTokenDepositAmountMismatch(); uint256 crossChainFee = nativeDepositAmount - data.tokenAmount; if (depositFeeEnabled) { if (crossChainFee == 0) revert ZeroDepositFee(); IVaultCrossChainManager(crossChainManagerAddress).depositWithFeeRefund{value: crossChainFee}( msg.sender, depositData ); } else { IVaultCrossChainManager(crossChainManagerAddress).deposit(depositData); }Because the else branch neither refunds
crossChainFeenor forwards it, any overpayment remains trapped on the vault contract. Integrators who include a buffer to coverLayerZerocosts, mis-estimate the fee, or intentionally round up the value will lose the difference with no indication.This can lead to user fund loss and undermines the expectation that excess native value is either used for fees or returned.
Recommendation
Type-check native deposits so that excess funds do not accumulate on the contract. Either reject any
msg.valueabovedata.tokenAmountwhendepositFeeEnabledisfalse, or proactively send the surplus back to the sender after_ethDepositcompletes.Resolution
Orderly Team: The issue was resolved in commit b320fde.
-
L-02 Low Initialize Cannot Be Called Unexpected Behavior Resolved
Description
The
initializefunction in theOmnichainLedgerV2contract performsdelegatecallsto multiple implementation addresses. However, these implementation addresses are only set duringinitializeV2.Since
initializeV2is restricted by theDEFAULT_ADMIN_ROLE, which is granted only duringinitialize, a fresh deployment cannot set the implementation addresses before initialize runs.While the admin is already set in the previous version and
initializeV2can be called during an upgrade, initialize cannot be invoked as part of the upgrade process, rendering that function redundant in this contract.Recommendation
Consider removing the initialize function from
OmnichainLedgerV2if onlyinitializeV2is intended to be called as part of the upgrade process.If this contract is intended for use in a fresh deployment setup, ensure that both initialize and
initializeV2can be called independently and do not rely on values set by the other.Resolution
Orderly Team: The issue was resolved in commit 16e0881.
-
L-03 Low Incorrect Timestamp Update Unexpected Behavior Resolved
Description
The
updateMarketUploadfunction in theMarketManagercontract can be used to update mark and index prices or the sum of unitary funding.When it is called with
UploadSumUnitaryFundingsto update the unitary funding value, the function incorrectly updates thesetLastMarkPriceUpdatedvalue instead ofsetLastFundingUpdated, thereby updatinglastMarkPriceUpdatedincorrectly.cfg.setSumUnitaryFundings(sumUnitaryFunding.sumUnitaryFunding); cfg.setLastMarkPriceUpdated(sumUnitaryFunding.timestamp); //@audit-issue should be setLastFundingUpdatedRecommendation
Use the
setLastFundingUpdatedfunction in the case of a funding update.Resolution
Orderly Team: The issue was resolved in commit 5fcc28b.
-
L-04 Low CCTP Finality Threshold Misconfiguration Configuration Resolved
Description
Circle’s CCTP V2 technical guide (section “Defined Finality Thresholds” - https://developers.circle.com/cctp/technical-guide#defined-finality-thresholds) states that only two values are honored on-chain:
1000 for confirmed/fast transfers and 2000 for finalized/standard transfers.
Any value below 1000 is coerced upward to 1000 and any value above 1000 is coerced to 2000. In our deployment the owner-facing configurator does not enforce this contract;
setCCTPConfigaccepts an arbitraryuint32and persists it directly:function setCCTPConfig(uint256 _maxFee, uint32 _finalityThreshold) public onlyOwner { cctpMaxFee = _maxFee; cctpFinalityThreshold = _finalityThreshold; }In production an operator could select 2, 500 or 1234 thinking they were tightening or relaxing finality, but Circle’s contract silently maps everything below 1000 to 1000 and everything else to 2000.
Operators therefore believe they have requested “500 confirmations” while the bridge actually runs at the fast tier, exposing rebalances to reorg risk; alternatively, they might expect an intermediate value yet the bridge enforces the slower finalized tier and higher fee.
Recommendation
Guard the setter so only the two valid thresholds are accepted. Expose them as named constants (for example
FINALITY_CONFIRMED = 1000andFINALITY_FINALIZED = 2000) and revert on any other input, or offer an enum that maps to those constants.Populate sensible defaults per chain and surface the choice explicitly in operator tooling, keeping runtime configuration aligned with the actual CCTP behavior.
Resolution
Orderly Team: The issue was resolved in commit 423b704.
-
L-05 Low Hardcoded VaultType In Operation Data Events Acknowledged
Description
The
vaultTypeis hardcoded toPROTOCOLin the_getOperationDatafunction, even though the codebase now supports multiple vault types after recent updates.This operation data is used during vault deposits, withdrawals, and the cross-chain messages associated with these actions.
Although
vaultTypeis not used for validation on the ledger chain, it results in misleading event emissions for off-chain listeners or engines.Recommendation
Update the contract to support multiple vault types, or consider using separate contracts for different types.
Resolution
Orderly Team: Acknowledged.
-
L-06 Low CancelAllVestingRequests Messages Are Allowed Configuration Resolved
Description
CancelAllVestingRequestsmessage types are still supported on the vault side in theProxyLedger.buildEvmVaultMessagefunction.These messages will successfully be sent from the vault side, but they can no longer be processed on the ledger side because the
ledgerRecvFromVaultfunction does not support them anymore. As a result, users will pay LZ fees for unexecutable actions.Recommendation
Check the
payloadTypein thebuildEvmVaultMessagefunction and disallow type 7.Resolution
Orderly Team: The issue was resolved in commit 9ec0bbc.
-
L-07 Low UintValue Signatures Lack A Domain Separator Best Practices Acknowledged
Description
Signature.verifyUintValueSignaturesigns only the tuple (value, timestamp) before applying the Ethereum personal-message prefix, so the message that is actually authorized off-chain is.No deployment-specific data: no
chainId, noaddress(this), no contract-specific salt... appears in the digest.ValorImpl._dailyUsdcNetFeeRevenueand_esOrderRevenueUpdateconsume these signatures to authorize revenue uploads.That signature therefore grants CeFi the ability to mint protocol revenue: every verified message increases
totalUsdcInTreasure/totalEsOrderInTreasure, refreshes conversion rates and directly shifts redemption accounting.The deployment config shows the same ledger stack running on many networks.
config/ledger.jsonenumerates contract/proxy addresses for environments like dev, qa, staging and play across Arbitrum Sepolia, Optimism Sepolia, Polygon Amoy, Base Sepolia, Avalanche Fuji, Ethereum Sepolia, and the Orderly L2.config/oft.jsonlists the corresponding OFT endpoints.Because the signer key (
usdcUpdaterAddress) is shared across those deployments, any authentically signed revenue update observed on one chain can be replayed on every other chain, minting treasury balances and distorting rates without the signer’s consent.Recommendation
Domain-separate the signed payload so signatures become deployment-specific. Either adopt a full
EIP‑712domain separator or augment the current hash.Including
chainIdandaddress(this)(and ideally a type hash or domain salt) prevents the same signature from minting funds on multiple chains.Resolution
Orderly Team: Acknowledged.
-
L-08 Low Immutable Withdrawal Address Once Set Unexpected Behavior Resolved
Description
LedgerOCCManager.setUser2WithdrawAddrlets each user set a custom withdrawal target but forbids any later change:function setUser2WithdrawAddr(address _withdrawAddr) external { if (user2WithdrawAddr[msg.sender] = address(0)) { user2WithdrawAddr[msg.sender] = _withdrawAddr; emit SetUser2WithdrawAddr(msg.sender, _withdrawAddr); } else { revert("LedgerOCCManager: withdraw address already set"); } }Subsequent cross-chain payouts (
WithdrawOrderBackward,ClaimUsdcRevenueBackward, etc.) automatically forward tokens touser2WithdrawAddr[user]when it is non-zero.If the user loses access to that address or it becomes compromised, every future withdrawal is irreversibly redirected there and the contract offers no mechanism, neither for the user nor an administrator, to reset or rotate the mapping. Funds can therefore be permanently lost.
Recommendation
Permit updates to the mapping. Options include allowing the user to supply a signed message authorizing a new address, introducing a timelocked rotation, or enabling an authorized operator to reset entries.
The design must provide a safe path to recover from a compromised or outdated withdrawal address.
Resolution
Orderly Team: The issue was resolved in commit 5eaff7f.
-
L-09 Low ClaimEsOrderRevenue Should Check ValorSwitched Unexpected Behavior Resolved
Description
The
claimEsOrderRevenuefunction collects revenue fromValor2earnings and stakes it. It uses the internal function_collectUserRevenueForClaimableBatch2to collect the revenue and utilizesuserRevenue2.The
userRevenue2can only increase when a user redeemsValor2, and there is no way to earn\$esORDERrevenue before the protocol transitions toValor2. However, theclaimEsOrderRevenuefunction does not verify whether this transition has occurred.While the function ultimately reverts later in the transaction flow, it is recommended to check the transition status and revert early for better efficiency and clarity.
Recommendation
Check whether the protocol has switched to
Valor2, and allow theclaimEsOrderRevenuefunction to execute only after the switch.Resolution
Orderly Team: The issue was resolved in commit b49fe08.
-
L-10 Low Backward Message Payloads Are Not Empty Unexpected Behavior Resolved
Description
When composing
EvmLedgerMessagefor backward settlement, payload is set to the string literal "0x0". The payload field is bytes, so this produces a non-empty bytes value containing the ASCII characters of0x0, not a zero-length payload.EvmLedgerMessage memory message = EvmLedgerMessage({ dstChainId: _chainId, token: LedgerToken.ORDER, tokenAmount: orderAmountForWithdraw, receiver: _user, payloadType: uint8(PayloadDataType.WithdrawOrderBackward), payload: "0x0" // @audit string literal, not empty bytes });This payload is not used on the vault side and can be empty bytes.
Recommendation
Consider using empty bytes
(bytes("")or newbytes(0))as in LedgerOCCManager:L198Resolution
Orderly Team: The issue was resolved in commit 540b07c.
-
L-11 Low Missing Validation For Proxy Ledger Destination Validation Resolved
Description
When the ledger sends tokens back to an EVM vault,
buildOCCLedgerMsgroutes the OFT transfer tochainId2ProxyLedgerAddr[message.dstChainId]:sendParam = SendParam({ dstEid: dstEid, to: bytes32(uint256(uint160(chainId2ProxyLedgerAddr[message.dstChainId]))), amountLD: amount, ... });If that mapping entry is unset,
chainId2ProxyLedgerAddr[dst]returnsaddress(0), so the OFT send call delivers the ORDER tokens to the zero address and the compose message also targets zero.There is no fallback or refund; every withdrawal routed through that path is irrecoverably lost or stuck until the proxy address is configured.
The same risk applies for USDC revenue messages that use the same mapping. Any misconfiguration or forgetting to populate the mapping bricks user withdrawals.
Recommendation
Before building the
SendParam, require that the destination proxy is known, for example:address proxy = chainId2ProxyLedgerAddr[message.dstChainId]; require(proxy = address(0), "LedgerOCCManager: proxy not set");Only proceed with the cross-chain send once the mapping entry is present; otherwise revert to prevent funds from being sent to zero.
Resolution
Orderly Team: The issue was resolved in commit 47af923.
-
L-12 Low Vesting Request Lookup Enables User Self-DoS DoS Acknowledged
Description
Every vesting operation relies on
_findVestingRequestto locate the user’s request by ID:function _findVestingRequest(address _user, uint256 _requestId) internal view returns (VestingRequest storage) { for (uint256 i = 0; i < userVestingInfos[_user].requests.length; i++) { if (userVestingInfos[_user].requests[i].requestId = _requestId) { return userVestingInfos[_user].requests[i]; } } revert UserDontHaveVestingRequest(_user, _requestId); }There is no limit on how many entries a user can accumulate,
createVestingRequestjust pushes a new struct ontouserVestingInfos[_user].requests. A malicious or inattentive account can create thousands of tiny vesting requests.Later, when they attempt to call
claimVestingRequestorcancelVestingRequest,_findVestingRequestmust iterate over the entire array. Once the array grows large enough, that linear scan exceeds the block gas limit and the function reverts.The user can no longer manage their vesting requests and all pending rewards become inaccessible. This is a self-inflicted DoS stemming from the unbounded linear search.
Recommendation
Avoid O(n) lookups. Track request positions with an additional mapping, for example mapping each (
user,requestId) to its index in the array, or switch to a mapping-based storage structure that gives O(1) access.Alternatively, enforce a sensible upper bound on the number of concurrent vesting requests per user so the loop cannot grow beyond a safe size.
Resolution
Orderly Team: Acknowledged.
-
L-13 Low Ledger Compose Bypasses Pause Control Unexpected Behavior Resolved
Description
LedgerOCCManagerinheritsPausableUpgradeablethroughLedgerAccessControl, so protocol operators can pause the system. However, theLayerZeroinbound handler remains active while paused:function lzCompose( address _from, bytes32 /*_guid*/, bytes calldata _message, address /*executor*/, bytes calldata /*_extraData*/ ) external payable { ... ILedgerReceiver(ledgerAddr).ledgerRecvFromVault(evmVaultMessage); }The function is missing
whenNotPaused, so even in a paused state it accepts cross-chain messages, decodes them and forwards them toledgerRecvFromVault.This defeats the pause mechanism: an operator cannot truly halt operations during an incident because remote messages continue to be processed. Any safety assumptions about pausing the ledger are therefore invalid.
Recommendation
Guard
lzComposewith a pause check, e.g. add thewhenNotPausedmodifier or call_requireNotPaused()at the start of the function. That way, oncepause()is invoked, all inboundLayerZeromessages are rejected until the system is explicitly unpaused.Resolution
Orderly Team: The issue was resolved in commit 23687d4.
-
L-14 Low Vesting Calculation Truncates Twice Rounding Resolved
Description
_calculateVestingOrderAmountdetermines how much$ORDERa user can claim after the lock period. The current implementation breaks the expression into two integer divisions:return _vestingRequest.esOrderAmount / 2 + (_vestingRequest.esOrderAmount * vestedTime) / vestingLinearPeriod / 2;This corresponds to
floor(es/2) + floor((es * vestedTime / vestingLinearPeriod) / 2). Because both terms are truncated separately, users lose up to 0.5 token (in accordance with the fixed-point scale) compared to the idealfloor(es/2 + es * vestedTime / (2 * vestingLinearPeriod)).The bias is small but systematic: every request with non-zero fractional parts suffers an extra rounding loss during the linear vesting phase, so users consistently receive slightly less than intended.
Recommendation
Compute the amount with a single division to minimize truncation:
return (_vestingRequest.esOrderAmount * (vestingLinearPeriod + vestedTime)) / (2 * vestingLinearPeriod);Resolution
Orderly Team: The issue was resolved in commit a66632a.
-
I-01 Informational Misleading Comment Informational Acknowledged
Description
The comment above the
feeRateOfFundmapping reads, “fee rate of each strategy fund by vault ID.” However, this rate is actually stored by strategy provider ID, not vault ID.Recommendation
Update the comment.
Resolution
Orderly Team: The issue was resolved in commit 1e47f60.
-
I-02 Informational Rebalance Slot Reuse Locks Funds Unexpected Behavior Acknowledged
Description
The
VaultManagercontract keeps only 100rebalanceslots viarebalanceStatus[rebalanceId %MAX_REBALACE_SLOT]. WhenexecuteRebalanceBurnrecords a new rebalance, it overwrites the slot unconditionally:rebalanceStatus[data.rebalanceId % MAX_REBALACE_SLOT] = RebalanceTypes.RebalanceStatus({ rebalanceId: data.rebalanceId, burnStatus: Pending, mintStatus: None });The following check only fires when the same
rebalanceIdis seen again:RebalanceTypes.RebalanceStatus storage status = rebalanceStatus[data.rebalanceId % MAX_REBALACE_SLOT]; if (status.rebalanceId = data.rebalanceId) { if (status.burnStatus = Pending) revert RebalanceStillPending(); else if (status.burnStatus = Succ) revert RebalanceAlreadySucc(); }it does nothing when a different ID collides modulo 100. As soon as 100 rebalances are outstanding, the 101st request overwrites the slot for the oldest in-flight transfer even if the burn or mint hasn’t completed.
Later, when the remote chain sends the acknowledgement for that old transfer,
rebalanceBurnFinish/rebalanceMintFinishlook up the slot, see a differentrebalanceId, and revert withRebalanceIdNotMatch.The pending entry can never be cleared, so
tokenBurnFrozenBalanceOnchainortokenBalanceOnchainremains locked forever and the rebalance is effectively bricked.Recommendation
Track outstanding operations with unique identifiers rather than a fixed-size modulo array. This guarantees that pending rebalances cannot be displaced before their callbacks arrive.
Resolution
Orderly Team: Acknowledged.
-
I-03 Informational Delegate Swap Signature Replayable Signatures Acknowledged
Description
DelegateSwapSignature.validateDelegateSwapSignaturehashes onlyabi.encode(data.tradeId,block.chainid, data.inTokenHash, data.inTokenAmount, data.to, data.value, data.swapCalldata) and runs it throughECDSA.toEthSignedMessageHash.The contract address (or any vault-unique data) is missing, and the caller-supplied
data.chainIdis ignored, so the signed digest is identical on every vault that shares the sameswapSigner.Each vault tracks its own
_submittedSwapSet; when vault A executes a swap, it recordstradeIdso the request cannot be replayed on that contract. But vault B’s_submittedSwapSetis empty.If the swap operator submits the exact same (
tradeId, payload, signature) to vault B,_verifySwapSignaturerecomputes the same hash, recovers the same signer and accepts the swap becausetradeIdhas not yet been seen on B.The swap executes again, draining vault B’s tokens even though the signer only meant to approve it for vault A.
Recommendation
Bind the signature to the specific vault instance—e.g., include
address(this)in the abi.encode payload, or move to anEIP-712domain that embeds the verifying contract address—so a signature approved for one vault cannot be replayed on another.Resolution
Orderly Team: Acknowledged.
-
I-04 Informational Implementation Setters Don't Validate Code Best Practices Resolved
Description
The
implMerkleDistributor,implVesting, andimplStakingValorRevenueaddresses can be set to EOAs or non-contract addresses, as the setter functions do not verify that the provided addresses contain code.Similarly,
occAdaptercan also be set to an EOA. Although these are admin-only functions, it is recommended to include code size checks as a best practice.Recommendation
Check that the provided addresses have a non-zero code size in these functions.
Resolution
-
I-05 Informational Missing Events For Timeline Changes Events Resolved
Description
Changing
valorEmissionStartTimestampandvalorSwitchTimestampmaterially affects emission and redemption logic, yet no events are emitted.This impairs on-chain observability and can cause off-chain indexers or UIs to miss these state changes.
Recommendation
Emit events on changes and include old and new values.
Resolution
Orderly Team: The issue was resolved in commit bedcb99.
-
I-06 Informational Unused Role In Ledger Superfluous Code Resolved
Description
The
SYMBOL_MANAGER_ROLEandBROKER_MANAGER_ROLEare introduced in the Ledger contract.While the
BROKER_MANAGER_ROLEis utilized in thesetBrokerFromLedgerfunction, theSYMBOL_MANAGER_ROLEis not used anywhere in the contract and can be removed.Recommendation
Remove the unused role.
Resolution
Orderly Team: The issue was resolved in commit 3bc472c.
-
I-07 Informational HWM Decreases When Minting Discounted Shares Unexpected Behavior Acknowledged
Description
ProtocolVaultLedger._calculateHWMderives the next period’s high-water mark by averaging the existing HWM with the current share price of newly issued shares.The numerator is
totalShares * Math.max(hwm, sharePriceAfterFee) + newIssueShares *sharePriceAfterFeeand the denominator sums the two share counts.Whenever the newly issued shares are priced below the previous HWM, that weighted average falls strictly below the old HWM.
For example, 100 legacy shares at HWM = 1 combined with 50 new shares at 0.8 yields a blended HWM of 140/150 = 0.93.
Because
settleMainAndStrategyFundsassigns this value back tostrategyFundToken.hwm, the protocol can charge performance fees even though existing LPs have not recovered their losses.This violates the monotonic “high-water” guarantee and lets the strategy accrue fees prematurely.
Recommendation
After computing the blended value, clamp it so that the returned HWM is never lower than the previous one: e.g.,
return Math.max(hwm, blendedHwm);.Alternatively, maintain separate baselines for new capital instead of blending discounted shares into the global high-water mark.
Resolution
Orderly Team: Acknowledged.
-
I-08 Informational VaultType Mismatch Between Repos Unexpected Behavior Acknowledged
Description
The
strategy-vaultrepository now supportsCOMMUNITYvault types. However, in thecontract-evmrepository, only thePROTOCOLandCEFFUtypes are supported.All new
COMMUNITYvaults are treated asPROTOCOLtype from thecontract-evmperspective.Recommendation
Update the
contract-evmrepository to also support theCOMMUNITYtype. If this behavior is expected and allCOMMUNITYvaults are indeedPROTOCOLvaults, ensure this is clearly documented.Resolution
Orderly Team: Acknowledged.
-
I-09 Informational Redundant setAllowedStrategyProvider Function Superfluous Code Acknowledged
Description
The
isAllowedStrategyProvidermapping has been removed, but thesetAllowedStrategyProviderfunction remains in theProtocolVaultLedgercontract.This function accepts a
bool knobparameter but does nothing other than emitting an event, which could be misleading.Recommendation
Remove the redundant function.
Resolution
Orderly Team: Acknowledged.
Remediation Review
15 findings-
M-01 Medium halfUp16_8_i256 Function Misrounding Rounding Resolved
Description
Proof of concept: PoC
AccountTypePositionHelper.halfUp16_8_i256usesif (quotient > 0)when deciding whether to increment its integer division result.With a negative dividend whose magnitude is at least half the divisor, Solidity division produces
quotient = 0andremainder < 0; the> 0guard increments that zero to +1. The adjacentint128implementation correctly checksquotient > 0, then handles the zero case by inspecting the dividend sign.A direct example is
halfUp16_8_i256(-60, 100), which returns +1 instead of the required -1 even though the docstring promises half-up rounding (-1.6 -> -2 in the comment above). This helper feedshalfUp24_8_i256(-qty * price * currentHolding, qty)when a trader closes almost their entire position.The incorrect +1 injects a small positive term into
openingCost, flipping what should remain a negative long-cost into a positive value. The next line negates that number and passes it intohalfDown16_8(...).toUint128(), so the cast reverts withSafeCastUnderflow.Every trade, ADL transfer and liquidation calls
calAverageEntryPricefirst, therefore any trader can lock their own account (and any forced unwinds against it) by closing down to a 1e8 remainder where|openingCostOld * qty|crosses the half-divisor threshold.Because of this affected accounts can no longer trade or be liquidated, leaving the protocol with unserviceable bad debt and service downtime for that user.
Recommendation
Match the
int128logic: change the first branch inhalfUp16_8_i256toif (quotient > 0)and keep the explicitelse if (quotient < 0)plus the zero-quotient dividend-sign case so negative dividends round toResolution
Orderly Team: The issue was resolved in commit afda2fb.
-
-
L-01 Low Redundant Check In RedeemValor2 Flow Superfluous Code Resolved
Description
The valor switch status is checked in the
OmnichainLedgerV2contract when the payload isRedeemValor2.It is then checked again inside the
_redeemValor2internal function during thedelegatecallto the implementation, which is redundant.Recommendation
Remove one of the checks.
Resolution
Orderly Team: The issue was resolved in commit 03d50f5.
-
L-02 Low Raw Approve Breaks Non-Standard Tokens Best Practices Pending
Description
OCCManager.vaultSendToLedgerincreases the OFT allowance viaIERC20(erc20TokenAddr).approve(address(orderTokenOft), message.tokenAmount);even though the file already importsSafeERC20.The
approvalfunction of some stablecoins like USDT do not return a boolean reverting whenever used with theIERC20interface as theIERC20interface always expects a boolean as a return.When
approvalRequired()is true the bridge can therefore get stuck: the allowance never updates, transfers revert on non-standard tokens and the function provides no revert reason because the return value is unchecked.This makes vault -> ledger bridging brittle and incompatible with some specific tokens.
Recommendation
Replace the bare
approvewith SafeERC20 helpers that handle zero-first semantics and return-value checking, e.g.ERC20(token).safeApprove(orderTokenOft, 0);ERC20(token).safeApprove(orderTokenOft, message.tokenAmount);orsafeIncreaseAllowancewhen appropriate.This keeps allowances consistent for non-standard
ERC20s and surfaces failures throughSafeERC20’s unified revert path.Resolution
Orderly Team: Pending.
-
L-03 Low getData Blocks Final Dictionary Slot Unexpected Behavior Pending
Description
DecompressorExtensionappliesvalidDictAccess(end)to the exclusive upper bound ofgetData. That modifier requiresend < MAX_DICT_LEN, yet the loop reads[begin, end):for (uint256 i = begin; i < end; i++) { res[i - begin] = _dict[i]; }To fetch slot
MAX_DICT_LEN - 1, callers must passend = MAX_DICT_LEN, but the modifier reverts before the loop runs.The last dictionary entry is therefore unreachable via this accessor, shrinking usable storage and breaking decompressions that rely on the final slot.
OperatorManagerZipinherits this contract but never callsgetData, so the impact is limited to external dictionary readers rather than the existing operator flows.Recommendation
Validate
endlocally withrequire(end < MAX_DICT_LEN end > begin)(or early-return whenbegin =end) instead of reusingvalidDictAccessso the exclusive bound may equalMAX_DICT_LENwithout opening the reserved slots.Resolution
Orderly Team: Pending.
-
L-04 Low Missing onlyOperator In UpdateUnclaimed Access Control Pending
Description
The
onlyOperatormodifier in theupdateUnclaimedfunction of theProtocolVaultLedgercontract was removed after recent fixes.However, this function should only be called by the operator, along with other functions, in a specific order.
Although it still requires the engine signature, retaining this modifier is important to prevent unintended or unauthorized use.
Recommendation
Add onlyOperator modifier.
Resolution
Orderly Team: Pending.
-
I-01 Informational Users May Pay More ccFee Logical Error Pending
Description
The
ccFeecan now be provided during theupdateUnclaimedflow, and this providedccFeeis the per user amount that will be paid by each user. The sum of total fee paid by users should ideally be equal to the actual crosschain fee required.However, there is no cross-check between the provided
ccFeeat the time of engine signature and the actual required fee at the time of message execution.Even if the engine performs a quote and provides the correct fee, it is highly likely that the required fee at execution time will differ.
To ensure successful execution, the engine must include a buffer and provide more fee than required.
Since the cross-chain message in this flow is sent without a fee refund mechanism, any excess amount will not be refunded.
Recommendation
Be aware that discrepancies may occur due to the time gap between signature and execution, and document this behavior for users since they might have to pay slightly more than required.
Resolution
Orderly Team: Pending.
-
I-02 Informational Unused IAccessControl Import Best Practices Pending
Description
AccessControlRevised.solimportsIAccessControlbut the contract never references that interface anywhere in the file, so the compiler strips it while still emitting metadata about it:import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";Leaving unused imports behind clutters the source, confuses reviewers about dependencies, and slightly inflates bytecode metadata for no benefit. Keeping the file lean avoids that noise.
Recommendation
Remove the
IAccessControlimport entirely so the file only declares dependencies that are actually used.Resolution
Orderly Team: Pending.
-
I-03 Informational Duplicate VaultEnum Definitions Warning Pending
Description
Both
EventTypes.solandVaultTypes.soldeclare their ownVaultEnum, yet the two enums drive different halves of the withdraw pipeline: ledger code checksEventTypes.VaultEnum(LedgerImplD.sol) while the on-chain vault consumesVaultTypes.VaultEnum(vaultSide/Vault.sol).Any future reordering or addition must be mirrored in both files manually; a mismatch would silently corrupt cross-chain withdraw routing because signed payloads would deserialize to the wrong enum branch without compiler errors.
Recommendation
Keep a single definition (e.g., declare the enum in
VaultTypes.soland import/useVaultTypes.VaultEnumeverywhere, or move it into a shared base) so both ledger signatures and vault execution reference identical ordinals.Resolution
Orderly Team: Pending.
-
I-04 Informational Ledger Carries Unused Helper Imports Best Practices Pending
Description
Ledger.solimportsUtils.solandSignature.soland appliesusing AccountTypePositionHelper forAccountTypes.PerpPosition;, yet the contract never callsUtils.*,Signature.*, or any extension methods defined inAccountTypePositionHelper.These declarations therefore drag in unused dependencies.
Recommendation
Drop the unused imports and
usingdirective so theLedgercontract only depends on the libraries it truly needs.Resolution
Orderly Team: Pending.
-
I-05 Informational Dead VaultEnum.UserVault Entry Best Practices Pending
Description
VaultEnum.UserVaultis declared in bothVaultTypes.solandEventTypes.sol, yet no contract ever checks that value—withdraw flows invaultSide/Vault.solandLedgerImplD.solonly handleProtocolVaultandCeffu, falling through torevert NotImplemented()otherwise.As a result, any signature that encodes
UserVaultwill always revert, and the enum member serves purely as dead code that suggests a non-existent path.Recommendation
Remove the
UserVaultmember (and its mirror inEventTypes) unless the user path is fully implemented end-to-end; otherwise callers may believe a vault type exists when it cannot execute.Resolution
Orderly Team: Pending.
-
I-06 Informational Unused Base Margin Setters Warning Pending
Description
MarketTypeHelperdefinessetBaseMaintenanceMargin,setBaseInitialMargin, andsetLiquidationFeeMax, yet there are no call sites for any of the three functions elsewhere in the repo.Unlike the other setters in the helper, these add no behavior—just extra bytecode and cognitive overhead—so the library exports dead helpers that are never run.
Recommendation
Remove the unused setters (or actually integrate them wherever base maintenance/initial margins and max liquidation fees should be updated) so the helper only exposes live functionality.
Resolution
Orderly Team: Pending.
-
I-07 Informational Unused Constants In OmnichainLedgerV2 Best Practices Pending
Description
OmnichainLedgerV2importsDEFAULT_UNSTAKE_LOCK_PERIOD,DEFAULT_BATCH_DURATION,VESTING_LOCK_PERIODandVESTING_LINEAR_PERIOD, but the contract never references those constants anywhere else in the file (they are only used inside tests such ascontracts/test/LedgerTestV2.sol).Keeping these unused imports triggers compiler warnings and marginally inflates the artifact without adding functionality.
Recommendation
Remove the unused constant imports (or actually apply them during initialization) so the production contract only depends on values it consumes.
Resolution
Orderly Team: Pending.
-
I-08 Informational VaultFactory Imports Unused ERC1967Proxy Best Practices Pending
Description
VaultFactoryimportsERC1967Proxy, yet the factory never instantiates proxies. It deploys bytecode exclusively throughCREATE3.deployDeterministic. Leaving the proxy import in place adds an unused dependency.Recommendation
Remove the
ERC1967Proxyimport so the factory’s dependencies reflect the actual implementation (CREATE3+Ownable2Step) and the compiled artifact stays minimal.Resolution
Orderly Team: Pending.
-
I-09 Informational TODO Comment Can Be Removed Informational Pending
Description
The
// TODO: check escrowBalancecomment at line 209 of theLedgerImpDcontract can be removed, as the function now includes an escrow balance check after the updates.Recommendation
Remove the comment.
Resolution
Orderly Team: Pending.
-
I-10 Informational StakingValorRevenueImpl Fuzzing Suite Warning Pending
Description
We designed
test/forge/StakingValorRevenueFuzz.t.solto act as a state-machine fuzz harness aroundStakingValorRevenueImplTest.The harness bootstraps the full staking + Valor + revenue stack with production-like parameters, exposes private accounting getters via
StakingValorRevenueFuzzHarnessand then drives 13 pseudo-random operations (stakes, immediate/delayed unstakes,Valor1andValor2redemptions, USDC/esORDERclaims, revenue updates, batch preparations, operator sweeps, andesORDERunstake) across three chain IDs.After each of the 20–60 actions per run, the suite enforces conservation invariants including total stake equality, pending-unstake mirrors, treasury coverage, user reward fairness, batch totals per chain, emission caps, and
Valor1freeze semantics post-switch.We executed the suite for more than one million iterations (combining multiple seeds and high
FOUNDRY_FUZZ_RUNSconfigurations) and observed zero invariants reverting or unexpected harness reverts.This demonstrates strong confidence that cross-chain accounting, revenue batching and the
Valor1- >
Valor2transition logic are internally consistent under randomized stress.
Recommendation
Adopt this fuzz harness in CI with a mid-sized iteration budget (e.g., 25k–50k runs) so regressions in accounting logic are caught automatically, and continue scheduling periodic > 1M-run campaigns before major releases to preserve the observed invariant stability.
Resolution
Orderly Team: Pending.
- >
No findings match.
More from Orderly
All 8 reports-
Solana Vault, Sol-CC and EVM Updates
53 findings1 critical · 6 high 53 findings: 1 critical, 6 high, 5 medium, 22 low, 19 informational -
Strategy Vault Updates
30 findings1 high 30 findings: 1 high, 5 medium, 16 low, 8 informational -
Solana Staking
35 findings1 critical · 1 high 35 findings: 1 critical, 1 high, 4 medium, 29 low -
Cross-Chain Yield Vault
64 findings7 critical · 6 high 64 findings: 7 critical, 6 high, 16 medium, 35 low
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.
