Orderly engaged Guardian to review the security of their cross-chain, share-based yield aggregator smart contracts. From the 2nd of January to the 20th of January, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- January 2 to 20, 2025
- Language
- Solidity
- Chains
- Ethereum, Arbitrum, Optimism, Base, Solana
- Sector
- Perpetuals
- 7 Critical
- 6 High
- 16 Medium
- 35 Low
- 0 Informational
Scope
Overview
Orderly engaged Guardian to review the security of their cross-chain, share-based yield aggregator smart contracts. From the 2nd of January to the 20th of January, a team of 6 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 13 High/Critical issues were uncovered and promptly remediated by the Orderly team.
Security Recommendation Given the number of High and Critical issues detected as well as additional code changes made after the main review, Guardian recommends that an independent security review of the protocol at a finalized frozen commit is conducted before deployment.
Findings 64
-
C-01 Critical Missing Access Control In sendMessage Function Configuration Resolved
Description
The
VaultCrossChainManagercontract implements thesendMessagewhich is used to send cross-chain messages.This function does not implement any type of access control and, therefore, any malicious user can craft arbitrary cross-chain payloads and transmit them causing the receiving chain’s contract logic to execute unverified operations.
This could be easily exploited to perform unauthorized withdrawals or deposits.
Recommendation
Introduce strict access control to
sendMessageso that only trusted contracts, such as whitelisted vaults, can invoke cross-chain operations.Resolution
Orderly Team: The issue was resolved in commit 8540bc3.
-
C-02 Critical Some ProtocolVault Functions Do Not Validate Properly The PayloadType Validation Resolved
Description
In the
ProtocolVaultcontract, both the deposit and withdraw functions accept aPayloadTypethat is never validated against the function’s intended usage. As a result, a user could call the withdraw function but submit a payload indicatingLP_DEPOSITorSP_DEPOSIT.On the ledger side, this is interpreted as a deposit even though no tokens were transferred to the
ProtocolVault, artificially increasing the user’s balance. This leads to unbacked shares on the ledger and allows users to perform “free” deposits.Recommendation
Add strict checks in each function to require that deposit only accepts deposit payloads (
LP_DEPOSITorSP_DEPOSIT) and withdraw only accepts withdrawal payloads (LP_WITHDRAWorSP_WITHDRAW). Any otherPayloadTypeshould always revert.Resolution
Orderly Team: The issue was resolved in commit 3275584.
-
C-03 Critical handleOpFromVault Function Does Not Scale Decimals Validation Resolved
Description
In the
handleOpFromVaultfunction, theProtocolVaultLedgercontract treatsoperationData.amountas a direct integer without adjusting for the underlying token’s decimals.Assets and shares are supposed to be stored with 6 decimals precision. As per the code comments, this
accountTokenwill be USDC and USDC has 6 decimals in most of the chains. However USDC has 18 decimals instead of 6 on the following chains:- Oasys
- BNB
- OKX Chain
- Sora
- Kucoin Chain
- Telos
- Conflux
- Bitgert
If
accountTokenhas a different number of decimals than 6 theProtocolVaultLedgercontract calculations end up over-counting or under-counting actual token amounts which would totally break the accounting in the contract.Recommendation
Normalize
operationData.amountaccording to the token’s decimals before updatingProtocolVaultLedger’s state. A robust approach is to store each token’s decimal information on-chain and adjust incoming amounts consistently.Resolution
Orderly Team: The issue was resolved in commit 872f355.
-
C-04 Critical State Variables Are Updated After A Failed Check Logical Error Resolved
Description
In the
ProtocolVaultLedgercontract, the_checkWithdrawfunction merely emits an event and returns early if a user attempts to withdraw more shares than they hold, instead of reverting the entire transaction. Because control flow proceeds after this early return, the contract continues to update its state variables (e.g., incrementingfrozenSharesor proceeding with other post‐withdraw steps) even when the user’s withdrawal request is invalid.Recommendation
In case that the
_checkWithdrawfunction require check was not passed, ensure that thefrozenSharesstate variable is not updated.Resolution
Orderly Team: The issue was resolved in commit 3d698ab.
-
C-05 Critical Initial DoS State Contract Multiple Divisions By Zero Logical Error Resolved
Description
In the
ProtocolVaultLedgercontract, when a strategy fund has not yet minted any shares,strategyFundTokenInfo[strategyProviderId][USDC_HASH].totalSharesremains zero, leading to a guaranteed division by zero during future certain end-of-period operations.In
settleMainAndStrategyFunds, the code invokes_calculateHWMto update the High Water Mark, which executes the linehwm = strategyFundToken.fundAssetsAfterFee * 10 * priceDecimal /totalSharesreverting becausetotalSharesis zero.A similar issue appears in
updateStrategyFundAssetsiffundShares(i.e.,strategyFundToken.totalShares) is zero, again causing a division-by-zero revert.As a result, the very first call to these functions in the
ProtocolVaultLedgercontract will always fail, blocking any attempt to settle or update strategy fund assets when no shares are yet in circulation.This breaks the normal lifecycle flow for the initial period, preventing operators from correctly finalizing and advancing the ledger state.
Recommendation
Introduce specialized handling for the zero‐shares scenario in
settleMainAndStrategyFundsandupdateStrategyFundAssets. WhenevertotalShares = 0, skip or defer the HWM calculation and other division‐based logic until at least one share exists.Resolution
Orderly Team: The issue was resolved in commit 4d1464d.
-
C-06 Critical rebalanceMint May Corrupt Ledger State Logical Error Resolved
Description
The ledger can perform
rebalanceBurnandrebalanceMintof tokens. This effectively burns the tokens on one chain and mints them on another one by using Circle's tokenManager.The flow is as follows:
Ledger.executeRebalanceBurn(). This will deduct the burnt amount from the chain's balance in
VaultManagerand will add it to thefrozenBalancesin case the burn fails.- A cross chain message is sent to
Vault.rebalanceBurn(). Vault.rebalanceBurn()callstokenMessengerContract.depositForBurn()- If the call fails, we send a failed rebalanceBurnFinish message to the ledger, to increase the chain's
balance back from the frozen tokens.
- If the call succeeds, the tokens are burnt from the vault and, event is emitted and a successful
rebalanceBurnFinishmessage is sent to reduce the frozen tokens.- Once enough attestations confirm the message, it can be executed on the destination chain by calling
messageTransmitterContract.receiveMessage().- If the receive is successful, the tokens are minted and a successful
rebalanceMintFinishis sent to the
Ledger to increase the balance of the destination chain.
- Otherwise, if the receive fails, a failed
rebalanceMintFinishwill be sent to the Ledger.
The problem with this flow is that
messageTransmitterContract.receiveMessage()is permissionless. If anyone calls it before the Vault, the message's nonce will be consumed and even though the mint is successful, the Vault will treat it as failed.In result, the tokens will be deducted from the source chain, but won't be credited to the destination chain, leading to loss of funds.
Recommendation
If the nonce is already used,
messageTransmitterContract.receiveMessage()will revert withNonce alreadyused. You can catch that and send a successfulrebalanceMintFinishto update the state correctly.Resolution
Orderly Team: The issue was resolved in the merge request 332.
-
C-07 Critical Compilation Error Due To Naming Mismatch Code Best Practices Resolved
Description
EventTypes.Withdraw2Contractstruct in theorderly-contract-evmrepo has auint256 clientIdparameter, which is used instead ofperiodId.However, the
orderly-evm-cross-chainrepo attempts to readperiodIdfrom this struct in theVaultCrossChainManagerUpgradeable.receiveMessagefunction, causing aTypeErrorcompilation error.Recommendation
Update the
orderly-evm-cross-chainrepo to reflect the changes in theorderly-contract-evmrepo.Resolution
Orderly Team: The issue was resolved in commit 7d71522.
-
H-01 High withdraw2Contract Crosschain Flow Pays Withdrawal Fee Twice Configuration Resolved
Description
In the
withdraw2Contractcrosschain flow, theLedgercredits the fee collector’s account with the withdrawal fee inexecuteWithdraw2Contract, then credits the same fee again inaccountWithDrawFinish.As a result, a single user withdrawal leads to a double fee charge in the Ledger’s accounting, once when the funds are initially frozen and again when the ledger finalizes the withdrawal. This inflates the fee collector’s balance with inexistent funds.
Recommendation
Remove one of the two fee credits so that the fee is only applied once. For example, either credit the fee collector immediately on
executeWithdraw2Contractand avoid doing so inaccountWithDrawFinish, or defer the fee credit until final settlement.Resolution
Orderly Team: The issue was resolved in the merge request 327.
-
H-02 High depositToStrategy Function Will Always Revert Logical Error Resolved
Description
The
depositToStrategyfunction sends a deposit request to thedexVaultviaIDexVault(dexVault).depositTo(...), transferring USDC (or another token) in the process.However, there is no approval step for the vault to pull tokens from this contract. Without an ERC-20 approve call, the
dexVaulthas no permission to transfer tokens on behalf of theProtocolVault. As a result, the deposit call will always revert.Recommendation
Approve the
dexVaultbefore calling thedepositTofunction.Resolution
Orderly Team: The issue was resolved in commit f6381c3.
-
H-03 High Newly Added Strategy Provider Will Not Receive LP Deposits Logical Error Acknowledged
Description
Under the current proportional deposit logic in
allocatToFunds, the contract allocates newly deposited LP assets among strategies based solely on the main vault’s existing shares (i.e.,mainShares).When a new strategy provider is introduced at a later period (for example, in the period 5), it starts with zero main shares. Consequently, the formula:
portionForThisStrategy = pendingLpDepositAssets * (mainAssetsInFund / totalMainAssetsInFund)will yield zero for that strategy. The new strategy never accumulates any main vault capital automatically, as it isn’t part of the existing distribution ratio (which depends on
mainShares).Even if the new strategy invests its own capital (SP deposit), that action mints strategy provider shares, not main shares, thus it does not affect the main vault ratio or future LP deposit splits. As a result, this new strategy remains perpetually excluded from LP inflows.
Recommendation
Incorporate a method for the operator to allocate a desired seed amount of main vault capital to a newly added strategy (e.g., “rebalance from existing strategies” to ensure the newcomer starts with a non-zero main share).
Resolution
Orderly Team: Acknowledged.
-
H-04 High Wrong Cross-chain Manager Usage Configuration Resolved
Description
LedgerImplCholds the logic for Solana withdrawals andwithdraw2Contractwithdrawals. It correctly usescrossChainManagerV2AddressinsideexecuteWithdrawSolAction()to execute a Solana withdrawal.However, it uses the same
crossChainManagerV2AddressinsideexecuteWithdraw2Contract()while thewithdraw2Contract()function is implemented incrossChainManagerAddress. In result,withdraw2Contract()will always fail.Recommendation
Replace
crossChainManagerV2AddresswithcrossChainManagerAddressinsideexecuteWithdraw2Contract().Resolution
Orderly Team: The issue was resolved in the merge request 328.
-
H-05 High DOS Of updateLPAndStrategyFund Logical Error Resolved
Description
LP_WITHDRAWandSP_WITHDRAWinsideProtocolVaultLedger.handleOpFromVault()should only be allowed if the user has enough withdrawable shares.This is handled by
_checkWithdraw()- it ensures the amount to be withdrawn added to the current frozen amount doesn't surpass the share balance of the account.After that, the amount to be withdrawn is added towards
frozenSharessocheckWithdrawalwill continue to work properly for further withdrawal requests.The withdrawal request will be handled by
_handleLpWithdraw()and the amount to be withdrawn will be subtracted by the user'spendingSharesandfrozenShares. After some time,settleAccounts()will update the user's actual shares by setting them topendingShares.This creates a window between
_handleLpWithdraw()andsettleAccounts()wherefrozenSharesis decreased, butaccount.sharesis not updated.Any withdrawal request in that window will successfully performed if it doesn't exceed the user's shares because
_checkWithdraw()won't stop it. This will result in an increase infrozenShares, potentially doubling the current value.The withdrawal request inside this window will be processed inside
_handleLpWithdraw()for the next period.The amount to be withdrawn will be subtracted from
pendingSharesagain, however this timependingShareswill not cover it causing a revert and DOS of theupdateLPAndStrategyFundfunction.Recommendation
One possible solution may be to disallow withdrawal requests in that window.
Resolution
Orderly Team: The issue was resolved in commit 2ff9be9.
-
H-06 High Withdraw Message Sent Even If _validReceiver Check Fails Validation Acknowledged
Description
In the
Vault.withdrawfunctionIVaultCrossChainManager(crossChainManagerAddress).withdraw(vaultWithdrawData)is called before verifying that thereceiveris valid.Consequently, if
_validReceiver(data.receiver, address(tokenAddress))returnsfalse, the vault still sends a cross‐chain message to the ledger acknowledging a “successful” withdrawal.Meanwhile, the local code emits only a
WithdrawFailedevent and never transfers the tokens to theProtocolVault. This leaves the ledger believing the user’s withdrawal went through, while in reality no tokens were actually delivered.As a result, the user’s ledger state and on-chain vault state become out of sync. The ledger sees a final “withdraw finish,” but the vault never transferred tokens if the receiver check fails. This scenario could strand the user’s funds or require an off-chain correction.
Recommendation
If the vault does not transfer tokens because
_validReceiver(data.receiver, address(tokenAddress))returned false, consider sending a “withdraw failure” message back to the ledger or omit sending a “successful” cross‐chain call.This ensures the ledger and vault remain consistent, reflecting that the withdrawal did not finalize.
Resolution
Orderly Team: Acknowledged.
-
M-01 Medium SP Deposits Are Not Refunded Configuration Resolved
Description
In the
handleOpFromVaultfunction ofProtocolVaultLedger, whenever a deposit operation targets a strategy provider (spId) that is not marked as allowed (isAllowedStrategyProvider[spId] = false), the contract simply emits an event and returns.This silent return means that the deposited tokens, already locked on the
ProtocolVaultside are not refunded to the strategy depositor. As a result, funds end up stuck, creating a loss scenario for the depositor.Recommendation
Consider incorporating a refund logic for the deposited assets to cover this edge case.
Resolution
Orderly Team: The issue was resolved in commit 63d9a53.
-
M-02 Medium Missing Payable Modifier Function Configuration Resolved
Description
In the
depositToStrategyfunction, the contract attempts to callIDexVault(dexVault).depositTo{value:fee}(...)but the function itself is not declared aspayable. As a result, it cannot receive native assets in the current transaction, causing a revert if no native assets are already in the contract’s balance.This breaks the expected flow of paying a fee at runtime, preventing the contract from funding the
DexVaultdeposit call.Furthermore, in the
VaultCrossChainManagercontract’s_lzReceivefunction, theASSETS_DISTRIBUTIONcase callsIProtocolVault(vault).depositToStrategy(...)but does not supplymsg.valuefor the fee.Recommendation
Add the
payablemodifier todepositToStrategyso that it can accept native assets in the same transaction.At the same time, ensure that the cross-chain message in
VaultCrossChainManagerincludes the fee inmsg.valuewhen relayingASSETS_DISTRIBUTION, so the contract handles and transfers the deposit fee to theDexVault.Resolution
Orderly Team: The issue was resolved in commit e311386.
-
M-03 Medium Operators Could Replay Signatures In The Contract Configuration Resolved
Description
The
ProtocolVaultLedgercontract relies on its backend to provide signatures for state‐changing functions, such asupdateLPAndStrategyFund,allocatToFundsand similar functions, but it does not seem to enforce strict replay protection.Once a valid signature has been used, an operator can potentially reuse that same signature multiple times (even in different contexts) to reapply changes or to trigger previously authorized operations again within the same period.
This opens the door for double handling of deposit/withdraw operations or other malicious state transitions. On the other hand, the
vaultIdis derived from the vault address and broker hash.All protocol vaults are deployed to the same address using
CREATE3and thebrokerHashis a hardcoded value. As a result, thevaultIdsof all protocol vaults across every EVM chain are identical and their accounting is tracked as a single account on the ledger chain.None of the signature verifications include the chain ID. Since
vaultIds are identical across all chains, a valid signature on one chain will also be valid on another chain for the sameperiodId.Recommendation
Implement a nonce or sequential counter mechanism for each signature payload, incrementing a contract‐stored counter after each valid call. Moreover, consider adding
chainIds to engine signatures.Resolution
Orderly Team: The issue was resolved in commit 3275584.
-
M-04 Medium No Enforcement Of Required Bridging Fee In sendMessage Validation Resolved
Description
The
sendMessagefunction inVaultCrossChainManagercalculates aMessagingFeevia_quotebut never checks whether the user-suppliedmsg.valueactually matches the required bridging feeBecause there is no comparison between
messageFee.nativeFeeandmsg.value, the function can be underfunded without reverting.Recommendation
Enforce that
msg.valueis at least equal to the calculated bridging fee. For example, add a check likerequire(msg.value = messageFee.nativeFee, "Insufficient bridging fee");to ensure that the user has paid for the fee.Resolution
Orderly Team: The issue was resolved in commit b13ebd6.
-
M-05 Medium Strategy Deposit Might Fail Due To Limit Logical Error Acknowledged
Description
Asset distributions to strategies are initiated by the operators on the ledger chain. The message is then transferred to the vault chain, where the
depositToStrategyfunction triggers asset movements from theprotocolVaultto thedexVault.There is a time lag between an operator initiating the process on the ledger chain and the actual execution of the transfer on the vault chain.
Even if the operator initiates the process with valid distribution amounts, the
Vault.depositTofunction might revert with aDepositExceedLimiterror due to ongoing deposits during this time lag.For example:
dexVaultdeposit limit: 1000- Current balance of the
dexVault: 850 - Operator calls asset distribution with 100 on the ledger chain (valid amount at the time of
initiation).
- Regular users deposit 60 more until this message reaches to vault chain.
- New balance of the
dexVault: 910 - The distribution transaction fails with
DepositExceedLimit. - There is still 90 left in the deposit limit that is not filled.
Since asset distributions can only be called once per period, the operator cannot attempt to distribute assets with a lower value. As a result, assets in the
protocolVaultremain unused.Recommendation
Consider implementing a separate deposit function in the
dexVaultfor strategy deposits. This function should not revert if the full amount cannot be deposited; instead, it should deposit the available amount up to the limit.Resolution
Orderly Team: Acknowledged.
-
M-06 Medium FeeRate Changes Lead To Loss Of Yield Logical Error Resolved
Description
The
setFeeRatefunction allows changing performance fee rates during an active period, which can result in users being charged different rates than what they initially agreed to. When users deposit funds, they implicitly agree to the current fee structure.However, if the fee rate is modified mid-period via
setFeeRate, users will be charged the new rate when performance fees are calculated inupdateStrategyFundAssets, even though this wasn't the rate in effect when they deposited.For example: 1. User deposits when fee rate is 20% 2. Mid-period, owner calls
setFeeRateto change rate to 30% 3. At period end, performance fees are calculated using 30% rate 4. User pays higher fees than they agreed to when depositingRecommendation
Only allow fee rate changes to take effect in future periods. Or restrict fee rate changes to only occur after the current period's performance fees have been calculated.
Resolution
Orderly Team: The issue was resolved in commit 26e43f9.
-
M-07 Medium Native Funds Locked In Contract Logical Error Resolved
Description
In the
_lzSendfunction, the_refundAddressparameter is set to the contract's address, but the contract lacks functionality to withdraw or rescue these refunded ETH funds. WhenLayerZeroreturns excess fees to the contract address, they will be not be retrievableRecommendation
Consider implementing a pull method for users to receive their refund. Or add a rescue function so that admin can recover the locked ETH.
Resolution
Orderly Team: The issue was resolved in commit b13ebd6.
-
M-08 Medium Event Emitted With Incorrect Values Validation Resolved
Description
The
MainAndStrategyFundsSettledevent’s first parameter should represent themainAssetsamount. However, it is currently emitted with theperiodId. Since the protocol’s backend heavily relies on event emissions, this issue may cause incorrect operations on the backend.Recommendation
Update the event.
Resolution
Orderly Team: The issue was resolved in commit d4b54b2.
-
M-09 Medium A Portion Of Frozen Fees Will Remain Frozen Logical Error Acknowledged
Description
During the
withdraw2contractflow the amount of funds and the fee will be frozen. Then at the end of the flow these amounts are intended to be unfrozen. However, there will be small difference between the amount frozen and unfrozen.This will happen when
convertDecimalis called and the destination chain decimals are less than the sending chain. In this case there will be some precision loss and while X amount of funds are frozen at the beginning only X - Y will be unfrozen. Where Y equals the precision loss.Recommendation
Adjust fee amount and withdraw amount so that there is no precision loss prior to freezing the funds. This can be done by truncating the amount to the destination decimals and then expanding it back to the senders decimals. This will ensure that the amount frozen and unfrozen are the same.
Resolution
Orderly Team: Acknowledged.
-
M-10 Medium Period Update Breaks When Batching Logical Error Acknowledged
Description
When
updateStrategyFundAssetsis called it will iterate through all funds. However given that there is no hard cap on the number of funds the protocol can have and that fund creation will become permissionless it will eventually require multiple iterations to update all the funds.The issue with this is that
mainAssetsAfterFeeswill become much less than its actual value on the second call since it will not take into accountmainAssetsAfterFeesfrom the first call. The reducedmainAssetsAfterFeeswill drastically reduce users share value.Recommendation
Modify the
updateStrategyFundAssetsfunction so that it can be called multiple times without losing data from previousupdateStrategyFundAssetscalls.Resolution
Orderly Team: Acknowledged.
-
M-11 Medium Rounding Up Will DoS When Funds Are Withdrawn Logical Error Acknowledged
Description
When calculating
distributeWithdrawAssetsthe value is rounded up. Because of this the amount of assets being distributed can be larger than the actual amount of funds.In some cases
distributeWithdrawSharesrounding down will offset this and there won't be excess funds distributed.But in situations where
distributeWithdrawAssetsdoes have a remainder causing the value to round up anddistributeWithdrawSharesdoes not have remainders resulting in no amount being rounded down.More assets will be distributed then intended. During times where all funds are withdrawn transferring an amount that is greater than what is available will lead to a failed transaction.
Recommendation
Consider rounding down when calculating
distributeWithdrawAssets.Resolution
Orderly Team: Acknowledged.
-
M-12 Medium Enabling Tokens Breaks Protocol Configuration Acknowledged
Description
ProtocolVault.setAllowedToken()lets the owner of the contract enable or disable a new token for deposits. When users perform deposits with this token, they will pay anamountof that token.However, the whole system currently is setup to work with USDC. For example,
_getOperationDatahardcodes thetokenHashtoUSDC_HASH. If another token were to be enabled, users will be charged that token, but their balance of the USDC token will be increased on the Ledger side instead.Recommendation
If you should support multiple tokens, consider not hardcoding the token hashes. Be careful with this approach because some tokens may have different decimals across chains.
Resolution
Orderly Team: Acknowledged.
-
M-13 Medium Token Losses On Deposit Validation Acknowledged
Description
Users can use
Vault._deposit()to deposit tokens from the vault side to the ledger side. They will be charged an amount of these tokens on the vault side.Since this token may have different decimal precision on each chain, the amount added towards the user balance on the ledger side is adjusted by
convertDecimalIn case
srcDecimals > dstDecimals, the amount will be divided to convert it todstDecimalsand the rest will be lost. This adjusted amount will also be recorded in thevaultManagerfor the givensrcChainId. In result, users will lose part of their tokens.Recommendation
Consider adding a
convertDecimalfunction to theVaultas well and charging the user the newly adjusted amount.Resolution
Orderly Team: Acknowledged.
-
M-14 Medium Vault LZ Fee Can Be Lost Validation Resolved
Description
When
ProtocolVault.depositToStrategy()is called, thedexVault.getDepositFee()will be forwarded todexVault. depositTo(). The vault will then use that value to pay for LZ fees.However, the vault has a
depositFeeEnabledboolean. It will use themsg.valuesend todepositTo()to pay for the fees only if this flag is set to true. Otherwise, the sent native token will not be used and remain stuck in the contract.Recommendation
Forward the fee from
ProtocolVaulttoVaultonly ifdepositFeeEnabled = true.IMPORTANT: If you implement this fix, any value provided by the executor to pay the fees will now be stuck in
ProtocolVault. You should come up with a solution for these funds. You can:- transfer the fee to the vault if
depositFeeEnabled = true - otherwise transfer it to the
VaultCrossChainManagerUpgradeable
Resolution
Orderly Team: The issue was resolved in commit f7648f0.
- transfer the fee to the vault if
-
M-15 Medium Insufficient msgOptions Configuration Acknowledged
Description
VaultCrossChainManager.sendMessage()will use themsgOptionsmapping to determine what gas and value the executor should use for executinglzReceiveon the destination chain. The values used is chosen based on the message'spayloadType.However, they are the same for each chain. Some chains may require different parameters. While it may be fine for most EVM chains, sending messages to Solana is different.
Instead of
gas_limitandmsg.value, the values used for Solana will becompute_unitsand lamports which is quite different.Reference: https://docs.layerzero.network/v2/developers/solana/gas-settings/options
Recommendation
Consider having different
msgOptionvalues for different chains (or at least Solana).Resolution
Orderly Team: Acknowledged.
-
M-16 Medium Insufficient Validation In withdraw2Contract Validation Partially resolved
Description
LedgerImplC.withdraw2Contract()doesn't implement the withdrawal validation implemented inLedgerImplA.executeWithdrawAction(). This poses a significant risk because of thewithdrawNonce. Since its not validated, a lower nonce than the current last value may be used.This will then lead to overriding the last withdrawal nonce with the new value (which is way lower) and will enable past withdrawals to be executed again. The fee is also not validated which means it can exceed the maximum configured fee.
Recommendation
Consider implementing validation for the two things mentioned in the report.
Resolution
Orderly Team: The issue was resolved in the merge request 329.
-
L-01 Low Wrong Check In _convertToShares Function Configuration Resolved
Description
In the
_convertToSharesfunction, the condition currently checks whether (_toatlShares = 0) to decide if the vault is in a “first deposit” scenario.Recommendation
Update the
_convertToSharesfunction to compare_totalAssets = 0rather than_toatlShares = 0.Resolution
Orderly Team: The issue was resolved in commit 5d09f50.
-
L-02 Low quoteOperation Function Always Assumes a LP_DEPOSIT Payload Configuration Resolved
Description
Within
ProtocolVaultcontract, thequoteOperationfunction hardcodesLP_DEPOSITas thePayloadTypeto calculate bridging fees.Recommendation
Update the
quoteOperationfunction to accept apayloadTypeparameter or determine it dynamically if needed, thereby ensuring the calculated bridging fee matches the actual operation type.Resolution
Orderly Team: The issue was resolved in commit b61d5e4.
-
L-03 Low ProtocolVault Claim Function Can Transfer Any Locked Token Validation Resolved
Description
In the
claimfunction, the user provides a token address inclaimParams.tokenwithout any validation that it matches the asset they previously deposited.Because the contract simply performs
SafeTransferLib.safeTransfer(ERC20(claimParams.token),msg.sender, amount), a malicious user can specify any token owned by theProtocolVault, claiming funds that do not necessarily belong to them.Recommendation
Restrict the token being claimed to the actual asset recorded for the user in the internal ledger (e.g., by storing the token in
userClaimedById[id]and only transferring that one).Resolution
Orderly Team: The issue was resolved in commit 8ef737e.
-
L-04 Low Possible Inconsistent Decimal Precision Configuration Validation Resolved
Description
The
setDecimalfunction allows updating three separate decimal values—priceDecimal,shareDecimal, andassetsDecimal—independently. If these values are set to different scales, the accounting logic will be broken.Moreover,
accountTokendecimals should be always the same aspriceDecimal. Finally,assetsDecimalstate variable is not really used across the contract’s logic so it can simply be removed.Recommendation
Enforce that
_priceDecimal,_shareDecimal, and_assetsDecimalremain the same, or remove the function altogether if dynamic decimal reconfiguration is not a valid operational case.Ensure that
accountTokendecimals is equal topriceDecimal. Consider removing theassetsDecimalstate variable.Resolution
Orderly Team: The issue was resolved in commit 263831c.
-
L-05 Low Redundant Period ID Parameter Code Best Practices Acknowledged
Description
The
_check(uint256 periodId)function in theProtocolVaultLedgercontract comparesperiodIdagainstlatestPeriodId, but this extra parameter is superfluous.Since the contract already tracks the currently active period in
latestPeriodId, requiring an extra parameter in many functions that is later on validated through the_checkfunction introduces unnecessary complexity.Recommendation
Remove the
_checkfunction and the redundantperiodIdparameter for all the functions. Instead, directly referencelatestPeriodIdwherever period alignment is needed.Resolution
Orderly Team: Acknowledged.
-
L-06 Low Proportional Allocation Overfunds Past Performers Configuration Acknowledged
Description
In the
ProtocolVaultLedger's current design, new LP deposits are allocated proportionally to each strategy’s existing “main vault” share, rewarding historically successful strategies with a continually larger share of new deposits.This works well if a strategy’s outperformance persists, but it can backfire when a once-top performer’s yield dwindles or fails.
For example, if Strategy1 significantly outperforms
Strategy2over the first 20 periods, it ends up with a much larger share of the main vault’s capital.Consequently, even if
Strategy1’s yield drops to near zero afterward, it continues to receive a high fraction of new LP deposits for many subsequent periods—because the contract only looks at the legacy ratio of main shares in each strategy.This can cause a suboptimal capital deployment where fresh user funds flow into a no-longer-productive strategy.
Recommendation
Consider implementing an operator function to realign capital if a strategy’s yield clearly stagnates, preventing capital from staying locked in a once-top performer.
Resolution
Orderly Team: Acknowledged.
-
L-07 Low Minor Unallocated Remainders In Proportional LP Allocation Precision Loss Acknowledged
Description
When splitting
pendingLpDepositAssetsamong multiple strategies usingmulDiv(...,Math.Rounding.Floor), each proportional slice may be truncated downward.Summing these truncated allocations for all strategies often leaves a small leftover in
pendingLpDepositAssetsthat never gets allocated.Over many periods or multiple strategies, these tiny unallocated remainders can accumulate, causing a minimal mismatch between the total deposit intended and the amounts actually distributed.
Therefore a very small amount of user-deposited capital remains undistributed. This issue also applies to withdrawals(
pendingLpWithdrawShares).Recommendation
After allocating to all but one strategy, assign the final strategy whatever remains of
pendingLpDepositAssetsto ensure there is no remaining dust.Resolution
Orderly Team: Acknowledged.
-
L-08 Low Debugging Checks And Test Code Left In Production Code Code Best Practices Acknowledged
Description
In multiple contracts, there are code snippets which are presumably for debugging or testing. Such debug checks should not remain in the live, production version of the contract.
Recommendation
Remove all these code snippets. If they are valuable for testing, maintain them in a separate test‐only version of the contracts.
Resolution
Orderly Team: Acknowledged.
-
L-09 Low Lack Of A Double Step TransferOwnership Pattern Code Best Practices Resolved
Description
The current ownership transfer process for all the contracts inheriting from the
OwnableorOwnableUpgradeablecontracts involves the current owner calling thetransferOwnershipfunction.If the nominated EOA account is not a valid account, it is entirely possible that the owner may accidentally transfer ownership to an uncontrolled account, losing the access to all functions with the
onlyOwnermodifier.Recommendation
It is recommended to implement a two-step process transfer ownership process where the owner nominates an account and the nominated account needs to call an
acceptOwnershipfunction for the transfer of the ownership to fully succeed.This ensures the nominated EOA account is a valid and active account. This can be easily achieved by using OpenZeppelin’s Ownable2Step contract.
Resolution
Orderly Team: Resolved.
-
L-10 Low CCTP depositForBurn Has Maximum Burn Per Transaction Validation Resolved
Description
In the
rebalanceBurnflow, the contract relies on Circle’s CCTP methoddepositForBurnfor transferring tokens from one chain to another.However, CCTP enforces a per‐transaction burn limit (a maximum USDC amount that can be burned at once) to mitigate risk and manage capacity on the Circle side.
If the protocol attempts to deposit and burn an amount exceeding that limit, the call to
ITokenMessenger(tokenMessengerContract).depositForBurn()will revert, preventing the rebalancing from succeeding.Recommendation
Make clear in the protocol’s user interface that a single rebalancing transaction is constrained. Operators should plan rebalancing flows accordingly.
Resolution
Orderly Team: Resolved.
-
L-11 Low Floating Pragma Code Best Practices Acknowledged
Description
Contracts should be deployed with the same compiler version and flags used during development and testing. Locking the pragma helps to ensure that contracts do not accidentally get deployed using another pragma.
For example, an outdated pragma version might introduce bugs that affect the protocol negatively. All the contracts in scope are using the following floating pragma:
pragma solidity ^0.8.18;Recommendation
Consider locking the pragma version in all the smart contracts. It is not recommended to use a floating pragma in production. For example:
pragma solidity 0.8.28.Resolution
Orderly Team: Acknowledged.
-
L-12 Low Incompatibility With CREATE2 On ZkSync Configuration Acknowledged
Description
The
VaultFactorycontract relies onCREATE2deterministic deployments (orCREATE3via solady library) to produce predictable addresses.However,
zkSynchas its own nuances forCREATE2instruction usage, documented at zkSync’s “Differences in EVM instructions”. Therefore, this version ofVaultFactoryshould not be used inZkSync.Recommendation
If planning to deploy in
ZkSyncconsult the official zkSync docs to adapt or replace the currentVaultFactorylogic with a mechanism that is officially supported and yields consistent results.Resolution
Orderly Team: Acknowledged.
-
L-13 Low Griefing Of Vault Deposits Validation Acknowledged
Description
The
_deposit()function inVault.solwill revert if the balance of the contract after the deposit would exceed thetokenAddress2DepositLimitset by the owner. This can be manipulated by external party by sending tokens directly to the vault and DOS-ing deposits.Recommendation
Introduce internal token deposits tracking instead of using
balanceOf.Resolution
Orderly Team: The issue was resolved in the merge request 330.
-
L-14 Low Unfair Performance Fee Distribution Logical Error Acknowledged
Description
In situations where a SP underperforms while there is an increase in shares it will take a weighted average of the previous HWM and the HWM of the incoming increase. Although this does lower the HWM it will still be greater than the share price that the incoming depositors are entering at.
Because of this incoming depositors can experience an increase in share price (profit) without paying any performance fee if the higher HWM is not exceeded. This essentially gives any user the opportunity to participate in the protocol profit off the SP's strategy without paying any fees.
Recommendation
Document that the performance fee burden is not always fair amongst LP’s when the fund moves from underperforming to not underperforming. Additionally monitor activity if the attack becomes an issue consider implementing incentives for LP’s that start and stay with underperforming funds.
Resolution
Orderly Team: Acknowledged.
-
L-15 Low No Paused Check For Withdrawals Configuration Acknowledged
Description
Withdrawals in the
Vaultcontract check if the receiver of the token is blacklisted and if they are, theWithdrawFailedevent will be emitted to credit the sender back their tokens on the Ledger side.However, there is no check if the token contract is currently paused. If it is, the sender will have to wait until the contract gets unpaused even though the token may not be paused on other Orderly supported chains.
Recommendation
Check if the contract is paused, just like you are checking if the receiver is blacklisted.
Resolution
Orderly Team: Acknowledged.
-
L-16 Low Centralization Risks Configuration Acknowledged
Description
The share price is calculated based on the total assets value provided by the backend. Operators can set the share price to arbitrary values by providing incorrect total asset amounts.
Additionally, the
setFeeRatefunction does not have an upper limit for fee rates, allowing them to be set even above 100%. Users also cannot access their funds that were deposited into theprotocolVaultuntil the deposits are handled in the period logic.Because of this the protocol can delay or not perform period updates and cause the users funds to be stuck in the
protocolVault.Recommendation
Users of the protocol should be aware of these centralization risks.
Resolution
Orderly Team: Acknowledged.
-
L-17 Low Multiple Typos Code Best Practices Resolved
Description
ProtocolVaultcontract L235: “cal dex” should be “call dex”.ProtocolVaultLedgercontract lines 685, 826 and 834: “_toatlShares” should be “_totalShares”.VaultFactorycontract line 38: "with keythe deployer" should be "with the deployer".Recommendation
Fix typos.
Resolution
Orderly Team: The issue was resolved in commit a528c91.
-
L-18 Low updateUnclaimed Does Not Have Duplicate Check Validation Acknowledged
Description
The
updateUnclaimedfunction checks unhandledrequestIds and createsuserClaimInfosarray based on their length. However, it does not perform a duplicate check for therequestIds.In the case of a duplicate entry, an unhandled
requestIdwill be counted twice when calculating the array length but will only be added once to the array, as the first entry will mark thatrequestIdas handled. This will result in theuserClaimInfosarray containing empty elements.Recommendation
Consider implementing a check to prevent duplicate
requestIdentries.Resolution
Orderly Team: Acknowledged.
-
L-19 Low Salt Is Not Hashed With Deployer Address Configuration Acknowledged
Description
According to the comments in the code, each deployer should have its own namespace, which is obtained by hashing the salt with the deployer's address. However, this is not the case in the actual code, where the salt is hashed by itself.
Recommendation
Consider hashing the salt with the deployer's address, or update the comments to reflect the current implementation.
Resolution
Orderly Team: The issue was resolved in commit a528c91.
-
L-20 Low Unused Errors And Events Code Best Practices Resolved
Description
NotAllowedTokenandNotEnoughFeeerrors inIProtocolVault,InsufficientBalance,AlreadyAllocatedShare,InvalidTotalAssetserrors andStrategyExecutedevent
in
IProtocolVaultLedgerare not used in the codebase and can be removed.Recommendation
Consider removing unused errors.
Resolution
Orderly Team: The issue was resolved in commit 8b6b665.
-
L-21 Low Warning About Paused DexVault Configuration Acknowledged
Description
The
distributeAssetsflow invokesProtocolVault.depositToStrategy, which subsequently calls theDexVault.getDepositFeeandDexVault.depositTofunctions. TheProtocolVaultLedgercontract on the Ledger chain does not implementPausable, whereas theDexVaulton the EVM chain is pausable.If the broker initiates the
distributeAssetsflow while theDexVaultis paused, the Ledger chain transaction will succeed, but the EVM part of the transaction will revert.Consequently,
isAssetDistributedwill be set to true on the Ledger chain, even though the assets remain undistributed.Recommendation
Be aware of this situation and avoid initiating a transaction on the Ledger chain when the receiver on the EVM chain is paused.
Resolution
Orderly Team: Acknowledged.
-
L-22 Low Unvalidated Params In setAllowedStrategyProvider Validation Resolved
Description
The
setAllowedStrategyProviderfunction accepts multiple parameters, includingspId,vaultId, thevaultaddress, andbrokerHash. It sets thespIdand emits theAllowedStrategyProviderSetevent with these parameters.Normally,
spIdis derived from these parameters. However, there is no check to ensure that the provided spId matches the ID derived from these parameters.If the provided values and the
spIddo not match, the function will still execute and emit an event containing misleading values for the backend.Recommendation
Consider adding a check to ensure that the provided values match the
spId.Resolution
Orderly Team: The issue was resolved in commit 27dc0db.
-
L-23 Low Some Functions Can't Cover Fee Costs Configuration Acknowledged
Description
distributeAssetsandupdateUnclaimedboth will send a message which requires a fee amount. But The functions depend on there being a existing amount in the cross chain contract. This means that bothdistributeAssetsandupdateUnclaimedcant send its ownmsg.valueto over the fee.Recommendation
Consider making these functions payable and give the operator the option to supply some
msg.value.Resolution
Orderly Team: Acknowledged.
-
L-24 Low A Hard-fork Can Disrupt Messaging Configuration Acknowledged
Description
If a hard fork occurs while a message is being sent it is possible that the chainId will change. If this were to happen there would be a mismatch in the chainID's impacting the messaging.
Recommendation
Monitor chain upgrades and in the rare cases that the chainId is going to change notify users or pause the protocol for a few blocks prior to the hard fork.
Resolution
Orderly Team: Acknowledged.
-
L-25 Low accountToken.assets Is Never Decreased Logical Error Resolved
Description
accountToken.assetsis increased in thehandleOpFromVaultfunction when users deposit. But there is no way for this value to decrease. So regardless if there are withdrawals or notaccountToken.assetswill continue to grow with each deposit.Recommendation
As
accountTokenis withdrawn consider decreasing theassetsamount.Resolution
Orderly Team: The issue was resolved in commit 474e339.
-
L-26 Low No Withdrawal Confirmation For Solana Configuration Acknowledged
Description
When withdrawal requests are processed from the Ledger side to the Vault side, the balance of the user is decreased and the amount is frozen. Upon successful confirmation, the frozen value is being zeroed out.
By tracking users' frozen balances, Orderly can increase their real balance back if the withdrawal action failed. This 2-step process is not happening for Solana withdrawals - everything is processed at once in
LedgerImplC.executeWithdrawSolAction().If the withdrawal fails, the frozen balance will be 0 and the user can't get their funds back. In addition, the fee collector is rewarded
feeamount with the decimal precision of the Ledger side. If there is a difference between these decimals on Solana, further problems may arise.Recommendation
Be aware of the potential risks
Resolution
Orderly Team: Acknowledged.
-
L-27 Low Market Manager Flag Not Cleared Validation Resolved
Description
At the end,
LedgerB.executeProcessValidatedFuturesBatch()loops over each trade and calls_writeBackLastFundingUpdatedTimestamp().This function updates the last funding timestamp of the manager and sets the
TSMarketManagerFlag()to true, which means no more updates for thattradeHash.This value is not cleared after the for loop ends. If multiple calls to
executeProcessValidatedFuturesBatch()are made in the same transaction, only the first call will update the timestamp, since the transient storage flag will be left astrue.Recommendation
Be sure to use the function correctly.
Resolution
Orderly Team: The issue was resolved in the merge request 333.
-
L-28 Low Slight withdrawAssets Discrepancy Precision Loss Resolved
Description
ProtocolVaultLedger._handleLpWithdraw()converts the current withdrawal shares to assets and adds them to the appropriateuserClaimInfo.Later in the flow, inside the
allocatToFunds()function, the sum of all withdrawal shares (pendingLpWithdrawShares) is converted the same way to assets and the result is subtracted frompendingState.pendingTotalAssets.Because Solidity truncates on division,
convertToAssets(pendingLpWithdrawShares)may not be equal toconvertToAssets(withdrawShares1) + convertToAssets(withdrawShares2) + ....This can lead to a slight discrepancy between the recorded assets to be withdrawn and the actual amount, potentially corrupting the flow because of a wrong result returned by
checkMainAndStrategyFund().Recommendation
Be aware of this behavior.
Resolution
Orderly Team: The issue was resolved in commit ff1ab3.
-
L-29 Low safeApprove() Deprecated Code Best Practices Acknowledged
Description
SafeERC20::safeApprove()has been Deprecated. The developer note in the function discourages using this function, and instead recommends usingsafeDecreaseAllowance()andsafeIncreaseAllowance().Recommendation
Use
safeIncreaseAllowance()instead ofsafeApprove().Resolution
Orderly Team: Acknowledged.
-
L-30 Low Token Deposits Cannot Be Disabled Configuration Acknowledged
Description
Vault.deposit()has a validation that checks iftokenAddress2DepositLimitfor a token is not zero, before seeing if the deposit limit has been exceeded. This prevents disabling deposits for a specific token, since a deposit limit of zero will bypass the second condition.Recommendation
Consider adding a flag that will revert if the token is currently disabled for deposits.
Resolution
Orderly Team: Acknowledged.
-
L-31 Low enableDepositFee Can Be Paused Configuration Acknowledged
Description
Vault.enableDepositFee()is a function which changes configuration, but it has thewhenNotPausedmodifier. This will stop the owner of updating the flag when the contract is paused.Recommendation
Consider removing the modifier.
Resolution
Orderly Team: Acknowledged.
-
L-32 Low Mixed Decimals Configuration Resolved
Description
It's expected that
assetPerShareandhwminProtocolVaultLedger.solwill be withpriceDecimals, but currently they will be withassetsDecimals + priceDecimals - shareDecimals.Recommendation
Be aware of that.
Resolution
Orderly Team: The issue was resolved in commit 263831c.
-
L-33 Low Unable To Reinitialize Contracts Configuration Acknowledged
Description
The
initializefunctions of the already deployed contracts won't be executed successfully because they are already initialized. For example,CrossChainRelayUpgradeable.solhas added logic in itsinitialize()function.Recommendation
Remove the
initializermodifier from theinitializefunction and add thereinitializeandonlyOwnermodifiers.Resolution
Orderly Team: Acknowledged.
-
L-34 Low Lack Of onlyProxy Modifier Code Best Practices Acknowledged
Description
LedgerCrossChainManagerUpgradeableandVaultCrossChainManagerUpgradeableareUUPSUpgradeablecontracts withupgradeTo()functions.In these contract the
upgradeTo()function is overridden, but there is noonlyProxymodifier to it. This allows direct upgrades to the implementation.Recommendation
Consider adding the
onlyProxymodifier.Resolution
Orderly Team: Acknowledged.
-
L-35 Low Cross-chain Communication May Be Blocked Configuration Acknowledged
Description
The
CrossChainRelayUpgradeableis a blocking OApp which means that once initiated, a message has to be successfully executed on the destination chain in order for any subsequent message to be received.If the receiving transaction reverts, the communication channel between the two chains will be blocked until the owner call
forceResumeReceive().A transaction can revert if one of the
requirechecks which confirms the correct data is sent reverts, the contract the vault interacts with become paused and etc...Recommendation
Consider switching to a non-blocking
Oapp.Resolution
Orderly Team: Acknowledged.
No findings match.
Invariants 32
The review's fuzzing suite asserted 32 invariants. 26 held and 6 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
PV-01 | Deposit to ProtocolVault should deduct user tokens | Held |
PV-02 | Receiver account token info unallocatedAssets should increase by amount when deposit is | Held |
PV-03 | called on the ProtocolVault Receiver account token info assets should increase by amount when deposit is called on | Held |
PV-04 | the ProtocolVault Deposit to ProtocolVault should deduct strategy tokens | Held |
PV-05 | Strategy token info unallocatedAssets should increase by amount when deposit is called on | Held |
PV-06 | the ProtocolVault Receiver account token info frozenShares should increase by amount when withdraw is | Held |
PV-07 | called on the ProtocolVault NotEnoughWithdrawShares check should not be bypassed | Broken |
PV-08 | Strategy token info frozenShares should increase by amount when withdraw is called on | Held |
PV-09 | the ProtocolVault User asset balance should increase by unclaimed assets when claiming assets | Held |
PV-10 | Strategy asset balance should increase by unclaimed assets when claiming assets | Held |
PV-11 | If asset per share > strategy hwm fundAssetsAfterFee must be less than total pending fund assets when calling | Held |
PV-12 | updateStrategyFundAssets If asset per share > strategy hwm pendingStrategyProviderShares must increase when calling | Held |
PV-13 | updateStrategyFundAssets If asset per share > strategy hwm pendingTotalShares must be greater than total pending fund assets when calling | Held |
PV-14 | updateStrategyFundAssets AccountId PendingShares must be converted to accountId Shares after settleAccounts | Held |
PV-15 | latestPeriodId should increment by 1 after updatePeriodId | Held |
PV-16 | pendingLpDepositAssets should increment be set to 0 after updatePeriodId | Held |
PV-17 | pendingLpWithdrawShares should increment be set to 0 after updatePeriodId | Held |
PV-17-REM | pendingLpWithdrawAssets should be set to 0 after updatePeriodId | Broken |
PV-18 | Protocol Vault token balance should decrease by asset distribution | Broken |
PV-19 | Dex Vault token balance should increase by asset distribution | Broken |
PV-20 | Dex Vault Ledger token balance should increase by asset distribution | Broken |
PV-21 | User unclaimable assets in ProtocolVault should increase by user claim info assets in | Held |
PV-22 | ProtocolVaultLedger On executeWithdrawAction accountId ledger balance should decrease by amount | Held |
PV-23 | On executeWithdrawAction receiver token balance should increase | Held |
PV-24 | On executeWithdrawAction protocolVault accountId ledger balance should decrease by | Held |
PV-25 | amount On executeWithdrawAction protocolVault token balance should increase | Held |
PV-26 | feeCollector account balance should increment by fee amount after | Broken |
PV-27 | withdraw2Contract Protocol Vault balance should increase by amount minus fee after withdraw2Contract | Held |
PV-28 | When depositing to the DexVault user token balance should decrease by amount | Held |
PV-29 | User account balance for the Ledger should increase by tokenAmount when depositing to | Held |
PV-30 | the DexVault globalEventId should increment by 1 after a deposit from the DexVault | Held |
PV-31 | globalDeposittId should increment by 1 after a deposit from the DexVault | Held |
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 -
Solana Vault
41 findings1 high 41 findings: 1 high, 3 medium, 18 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
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.
