Orderly engaged Guardian to review the security of their updates to Orderly Solana Vault, Sol-CC, and Contract-EVM. From the 25th of August to the 3rd of September, a team of 3 auditors reviewed the source code in scope.
- Published
- Review window
- August 25 to September 3, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity, Rust
- Chains
- Ethereum, Arbitrum, Optimism, Base, Solana
- Sector
- Perpetuals
- 1 Critical
- 6 High
- 5 Medium
- 22 Low
- 19 Informational
Scope
-
gitlab.com/orderlynetwork/orderly-v2
575f56341d4582ca7c7d2106b8a45fe565fcd3792adf41413951a7a28af3
Overview
Orderly engaged Guardian to review the security of their updates to Orderly Solana Vault, Sol-CC, and Contract-EVM. From the 25th of August to the 3rd of September, a team of 3 auditors reviewed the source code in scope.
Findings 53
Main Review
42 findings-
C-01 Critical Any Allowed Token Can Be Used Validation Resolved
Description
Proof of concept: PoC
The
deposit_solfile includes a check to ensure that the token account derived from the provideddeposit_params.token_hashis an allowed token.However, it does not verify that the hash specifically corresponds to SOL. As a result, the hash of any other allowed token can be provided when depositing SOL.
When a user invokes
deposit_solwith a USDC hash, 1e9 native SOL will be transferred from the user to the vault on Solana, while 1000e6 USDC will be credited to the user on the ledger chain, due to SOL having 9 decimals and USDC having 6 decimals.This results in an immediate ~5x profit for the user at current market rates.
Recommendation
Only allow the SOL/wSOL hash to be provided in
deposit_sol.Resolution
Orderly Team: The issue was resolved in commit 2beba85.
-
H-01 High LZ Messaging Channel Can Be Blocked DoS Resolved
Description
The Vault contract now supports native deposits and withdrawals. During a native withdrawal, the receiver address is called using
payable(receiver).sendValue(amount).The receiver address can intentionally revert to block the messaging channel or consume the entire gas provided by the LZ endpoint.
The
CrossChainRelaycontract uses_blockingLzReceive, meaning that subsequent messages cannot be executed until the previous message has been successfully delivered.When the receiver intentionally reverts during
_ethWithdraw, thelzReceivecall reverts, and the message is stored instoredPayloadon theLayerZeroendpoint.As a result, the messaging channel is blocked until
forceResumeReceiveis called.Recommendation
Wrap the
_ethWithdrawin a try/catch block and emit an event to allow manual resolution later if the receiver cannot accept native tokens.Resolution
Orderly Team: The issue was resolved in commit c57806a.
-
H-02 High EVM/Solana Fee Collectors Must Be Separate Logical Error Acknowledged
Description
Both EVM and Solana withdrawals from the
Ledgercontract attribute any fees earned to the same fee collector - the one that was collecting all the EVM fees until now.This approach worked until now since EVM accounts can withdraw from every other supported EVM chain, but only accounts that deposited on Solana can withdraw on Solana.
Look at the following example:
- Alice deposits 1000 USDC on Solana
- Bob deposits 1000 USDC on Arbitrum
- Alice withdraws 1000 (900 for her and 100 for fee collector)
- The fee collector claims them on Arbitrum
- Bob cannot access the Solana funds which means he loses them forever, can only withdraw 900
Furthermore, any SOL fees will be lost since they cannot be withdrawn on an EVM chain.
Recommendation
Add a new fee collector which collects only Solana fees in the
FeeManagercontract.Resolution
Orderly Team: Acknowledged.
-
H-03 High Insufficient Manager Role Validation Access Control Resolved
Description
Proof of concept: PoC
The Solana Vault implements a role based approach to let certain accounts change the configuration. The vault owner can grant and revoke roles by executing the
set_manager_roleinstruction.A
manager_rolePDA is derived for the given role and user account. Thismanager_rolestores a few fields, the most important of which is theallowedone - it shows whether the given user account has the role.When executing state changing instructions, that PDA is derived and the
allowedflag is checked to be true. For example, in theset_brokerinstruction#[account( seeds = [ACCESS_CONTROL_SEED, params.broker_manager_role.as_ref(), broker_manager.key().as_ref()], bump = manager_role.bump, constraint = manager_role.allowed = true @VaultError:ManagerRoleNotAllowed, )]There is a problem with the derivation. One of the used seeds -
broker_manager_role- is read directly fromparams. This allows the caller to supply any other role that they have and still execute the current action.For example, if they don't have the
broker_manager_role, but instead thetoken_manager_role, they can use thetoken_managerPDA to bypass the check and still change the broker.Recommendation
Consider hardcoding the
role_hashinstead of reading it from the user parameters.Resolution
Orderly Team: The issue was resolved in commit 196e1f2.
-
H-04 High Frozen ATAs Can't Ever Recover Funds Logical Error Acknowledged
Description
The very first thing that happens in
OAppLzReceive:apply()is a CPI (cross-program invocation) to theOAppLzReceiveprogram to clear the message. Unlike in the EVM LZ implementation, the LZ user is expected to clear the message. If the transaction fails "loudly" later, all changes get reverted.The problem is if the transaction fails silently, which can happen in the if
ata_account.is_frozen(){} code block. At the point when this line is executed, the program already validated the LZ message and the withdrawal as a whole. It is effectively ready to transfer the funds.If the receiver’s ATA is frozen, neither a transfer to the user or an escrow occurs, nor does the transaction revert. Instead, the code only emits a
FrozenWithdrawnevent.There is no instruction available for owners or managers to manually withdraw funds from the vault to recover them for the receiver if the frozen ATA becomes unfrozen at a later date.
Recommendation
Consider leaving the balance on the destination chain if the receiver is frozen, and add a function to allow the owner/manager to manually withdraw it in case the receiver later becomes unfrozen.
Alternatively, consider reverting the transaction and not clearing the LZ message to allow replayability.
However, this may introduce DoS scenarios when message delivery is ordered, requiring manual resolution by the OApp manager/owner.
Resolution
Orderly Team: Acknowledged.
-
M-01 Medium Can DOS Oapp_lz_receive.rs DoS Resolved
Description
The
oapp_lz_receive.rsexpects nonces to be ordered if theorder_deliveryis true. (vault_authority.check_nonce(params.nonce)on line 70). The function transfers native SOL to an arbitrary address (receiver).However, native SOL transfers can fail for a number of reasons like rent exemption, the receiver being executable, or a write-demotion happening (Reference article). These are things that happen in practice (Jito had a bug reported for this on their bug bounty, for example).
If the SOL transfer fails for any of the above reasons while
order_deliveryis set to true, it will not only revert the current transfer but also block any future messages because the nonce does not increment.This can be easily fixed by setting
order_deliveryto false inset_order_delivery.rs. However, if it occurs, users may be blocked from receiving their funds until the admins disableorder_deliveryto allow the problematic nonce to pass.It is also worth noting that an attacker could, at any time while
order_deliveryis true, exploit this to DoS the program by initiating a withdrawal of less than the rent-exempt amount or by setting the receiver address to one of the reserved accounts.Recommendation
It would be safer (albeit more complex) to allow the user to withdraw funds and handle the transfer in a separate function, rather than directly transferring Sol to the user within
oapp_lz_receive.rsResolution
Orderly Team: The issue was resolved in commit 4582ca7.
-
M-02 Medium Incorrect EIP-712 Typehash Validation Acknowledged
Description
The
DELEGATE_SWAP_TYPEHASHdefined in theSwapSignaturecontract is keccak256("DelegateSwap(uint256 swapNonce,uint256 chainId,bytes32 inTokenHash,uint256inTokenAmount,address to,uint256 value,bytes swapCalldata)").The first parameter here is
uint256 swapNonce. However, the first parameter used in thevalidateSwapSignaturefunction isbytes32 tradeIdfrom theDelegateSwapstruct.This discrepancy causes the signatures to be non-compliant with
EIP-712, or it would cause the signature check to fail if the signer correctly signs according to theEIP-712standard.Recommendation
Update the
DELEGATE_SWAP_TYPEHASHto match the actual struct used in validation.Resolution
Orderly Team: Acknowledged.
-
M-03 Medium Ordered Execution Option Is Not Added Unexpected Behavior Acknowledged
Description
Like already described in this issue of the Sherlock report,
SolConnector.withdraw()doesn't calladdExecutorOrderedExecutionOption()when it builds the message options.The newly added
withdraw2ContractV2()function doesn't use this option as well. This means there is no guarantee that the messages will be executed in order.If they aren't and the Solana vault has turned it's ordered delivery feature on, the messages will fail and will have to be resubmitted manually once the previous ones have been executed.
Recommendation
- Add new state variable that tracks the ordered delivery status of the Solana Vault.
- If this state variable is
true, useaddExecutorOrderedExecutionOptionwhen sending the
crosschain message.
Resolution
Orderly Team: Acknowledged.
-
M-04 Medium rebalanceBurn() Should Not Revert Logical Error Acknowledged
Description
A new check has been added to
Vault.rebalanceBurn()if (_rebalanceEnableTokenSet.contains(data.tokenHash)) revert NotRebalanceEnableToken();This will result in failed attempts to burn disabled tokens, which:
- blocks the communication channel because the crosschain manager defaults to the blocking
behavior
- causes the funds to be permanently frozen on the Orderly Network's Ledger because
rebalanceBurnFinish()is not invoked, resulting in a loss of these fundsRecommendation
Instead of reverting, send back a failure
rebalanceBurnFinish()message to the Ledger and stop the execution of the function.Resolution
Orderly Team: Acknowledged.
-
L-01 Low Only Onchain Success Changes Vault Balances Warning Resolved
Description
During executeSwapResultUpload, the user's balances are always updated, but the chain ones - only if the result of the swap was onchain success.
This creates a risk of an accounting mismatch for offchain swaps because the Vault contract will hold different funds that what is actually recorded in the manager.
Recommendation
Consider if
offchain successshould also cause update of thevaultManagerbalanceResolution
Orderly Team: Resolved.
-
L-02 Low Sol_vault Must Be Rent Exempt DoS Acknowledged
Description
Proof of concept: PoC
Accounts on Solana must hold lamports value below their rent exemption value. The rent exemption value depends on the data stored. Since empty accounts still store metadata, there is a minimum amount of rent exemption that has to be paid. At the moment of writing that amount is 890880.
The sol_vault account in
deposit_solis a PDA with no data. Because of that, no initialization happened and no lamports were transferred to the account.During deposits the
sol_vaultreceivestoken_amountof lamports. If the first deposit'stoken_amountis less than the rent exemption value, the transaction will fail.During withdrawals, lamports are withdrawn from the
sol_vault. If it's not the full amount being withdrawn and the balance of thesol_vaultdrops below the rent exemption value, the withdrawal will fail, the channel will be blocked if ordered delivery is enabled and the user won't receive their funds.Recommendation
To solve the issue, consider sending the minimum rent exemption value to the
sol_vaultwhen you deploy the app.Resolution
Orderly Team: Acknowledged.
-
L-03 Low Internal Transfers Warning Warning Acknowledged
Description
The internal transfer functionality of the
Ledger, for exampleexecuteBalanceTransfer()andexecuteFeeDistribution()should be executed with caution between EVM and Solana accounts because the liquidity is not shared, so transferring from one chain to another can result in insolvency.Recommendation
The internal transfer functionality of the
Ledger, for exampleexecuteBalanceTransfer()andexecuteFeeDistribution()should be executed with caution between EVM and Solana accounts because the liquidity is not shared, so transferring from one chain to another can result in insolvency.Resolution
Orderly Team: Acknowledged.
-
L-04 Low Deposit Fee Is Disabled In Paused State Unexpected Behavior Partially resolved
Description
The
enableDepositFee() onlyOwnerfunction and thegetDepositFee()view functions have thewhenNotPausedmodifier.This restricts the flexibility the owner of the Vault has during periods of emergencies when the contract is paused. It also causes failures for offchain integrators that call
getDepositFee().Recommendation
Consider removing the modifier from these 2 functions.
Resolution
Orderly Team: The issue was resolved in commit f769643.
-
L-05 Low Vault ATA Must Be Initialized DoS Acknowledged
Description
When the
lz_receiveinstruction is executed for withdrawal,vault_token_accountis ATA withmint =token_mint. ForSOL, thetoken_mintisWSOL.If nobody deposited
WSOLthrough the vault before that, thevault_token_accountwill most likely be uninitialized, which will lead to a failed withdrawal message that has to be retried manually and blocked pathway if ordered delivery is turned on.Recommendation
Initialize the
WSOL vault_token_accountright after callinginit_oapp().Resolution
Orderly Team: Acknowledged.
-
L-06 Low Native Token Swaps Will Always Fail Unexpected Behavior Acknowledged
Description
Vault.
delegateSwap()swaps the desired vault token for another one and the result is being uploaded by callingLedger.executeSwapResultUpload().The
Vaultsupports native tokens as well, but any swaps where a native token is the output token will be failing because theVaultdoesn't have a payablereceive()orfallback()function.This causes DOS for such on chain swaps and is even more dangerous for offchain swaps, since it can cause accounting mismatch.
Recommendation
Add a payable
receive()function to theVault.Resolution
Orderly Team: Acknowledged.
-
L-07 Low Lack Of Event Emissions Events Acknowledged
Description
The
set_ordered_deliveryandset_vaultinstructions change important configuration of the Vault, but no events are emitted. The same is true for the instructions in theoapp_instr/directory.Recommendation
Consider adding event emissions for at least the two vault instructions.
Resolution
Orderly Team: Acknowledged.
-
L-08 Low Avoiding Precision Loss For Withdrawals Rounding Acknowledged
Description
When withdrawals are uploaded on the Ledger side, the amounts used are in Ledger decimals and will be converted to destination decimals before the crosschain message is sent. For example, look at the SolConnector.
convertWithdrawDecimals(tokenHash, _withdrawData.tokenAmount), convertWithdrawDecimals(tokenHash, _withdrawData.fee),In case that the destination chain's decimals are lower than the ledger ones, both the
tokenAmountand the fee will experience precision loss.Whenever the
tokenAmountloses precision, the withdrawing user receives less funds and whenever the fee loses precision, the available liquidity for the fee collector decreases. However, on the ledger sides these values are stored in ledger decimals which will lead to accounting mismatch overtime.For example, if the fee collector accrues fees from several withdrawals, the final sum it tries to withdraw may be greater than what's available on that chain.
This problem is also present for the rest of the EVM chains, but for Solana it's more serious since a withdrawal failure may block future requests if ordered delivery is turned ON.
Recommendation
The most simple fix to avoid the precision loss would be to make the
BEadjust the withdrawal amount depending on the desired destination chain.For example, if ledger token has 3 decimals, but destination token has 2 decimals and the user wants to withdraw 123 tokens, the BE (and the FE) will instead give the user to withdraw only 12 tokens to avoid the precision loss. Same thing should be applied to the fee
Resolution
Orderly Team: Acknowledged.
-
L-09 Low Discrepancy About SYMBOL_MANAGER_ROLE Access Control Partially resolved
Description
SYMBOL_MANAGER_ROLEhas access to most functions related to deposit tokens and allowed tokens, such assetDepositLimit,setNativeTokenHash,setNativeTokenDepositLimit, anddisableDepositToken.However, the
enableDepositToken,setRebalanceEnableToken, andchangeTokenAddressAndAllowfunctions remain restricted toonlyOwner.Recommendation
Consider whether this is the expected behavior. If not, change
onlyOwnertoonlyRoleOrOwner(SYMBOL_MANAGER_ROLE)for these functions as well.Resolution
Orderly Team: The issue was resolved in commit e45a1bf.
-
L-10 Low Warning Related To delegateSwap Warning Acknowledged
Description
The
delegateSwapfunction in the Vault contract allows theswapOperatorto transfer the entireERC20token balance and native balance of the contract to an arbitrarytoaddress.Additionally, the function is not payable, which means native swaps will always use the contract’s own balance.
While the
swapOperatoris a trusted role, this issue serves to inform users about the capabilities of the role.Recommendation
No fix is required if the
swapOperatoris expected to use the entire funds in the Vault contract.Resolution
Orderly Team: Acknowledged.
-
L-11 Low 0 Deposits Are Allowed Validation Resolved
Description
The
depositanddeposit_solinstructions don't enforce positive amount when depositing which allows anyone, including accounts with 0 balance, to invoke the instructions and send meaningless messages to the Ledger side that won't update any amounts, but will update the account's metadata, likelastDepositEventIdand etc.This is not possible on the EVM chains because of the following validation
if (data.tokenAmount = 0) revert ZeroDeposit();Recommendation
Don't allow deposits with 0 amounts.
Resolution
Orderly Team: The issue was resolved in commit a81bc89.
-
L-12 Low Handling Ordered Delivery Transition Configuration Acknowledged
Description
Both the Solana Vault and
SolConnectorare OApps that can toggle the ordered delivery feature. This is a guide to how to transition from unordered delivery to ordered delivery should be handled.Let's look at an example where ordered delivery was turned
ONuntil nonce15. Then it was switchedOFFand messages until nonce20have been verified, but not executed. Because the order is not enforced, nonce20can be executed before all the others. If the Orderly team decides to go back to ordered delivery, nonces16-19will become invalid, which will result in loss of funds for users since they cannot finish their deposit/withdrawal.To solve this problem, it's best to first wait for all of the unexecuted messages to be delivered and only then switching to ordered delivery. However, there is no guarantee that new messages won't appear while waiting for the old ones to be executed, which will extend the waiting time. This can continue forever and lead to inability to go back to ordered delivery.
For this reason, there is a need of a way to pause source messages from being initiated
- The
SolConnectorcan be paused for that goal, but that would not only prevent it from sending messages, but also
from receiving, which may not be ideal. It's best to be able to pause receiving and sending separately.
- Technically deposits in the Solana Vault can be paused by setting
allowed_token.allowed = falsefor each of the
supported tokens, but you should add a global deposit flag that you can toggle for both
depositanddeposit_sol.Even if all of the steps above are taken, the inbound nonce on both chains is updated unconditionally whenever a message is received. Let's take a look at our example again and assume the five messages were executed while we are waiting in a paused state, but the last one had
nonce = 15. BecauseinboundNonceis unconditionally updated to15, when we switch back to ordered delivery, the app will expect nonce16to be executed, but the real one will be21and the Orderly team will need to manually update theinboundNonce.NOTE:Other changes of configuration should be handled like that as well, for example changing token indexes.Recommendation
- Make the pause feature in
SolConnectormore granular - Add
is_deposit_pausedflag toSolanaVaultand use it indepositanddeposit_sol. - Update the
inboundNonceonly if the new one is greater than the old one (issue exists in EVM and Solana contracts) - When transitioning from unordered to ordered delivery:
- pause the sending functionality of the other chain
- wait until all messages are executed
- unpause the sending functionality of the other chain
Resolution
Orderly Team: Acknowledged.
- The
-
L-13 Low Duplicate lastEngineEventId Assignment Logical Error Resolved
Description
In the
executeWithdraw2ContractV2function,account.lastEngineEventIdis updated at line 225 for both the EVM and SOL chain types, and then updated again at line 266 within theelse ifblock for the SOL chain type.Recommendation
Remove the redundant assignment.
Resolution
Orderly Team: The issue was resolved in LedgerImplD.sol#L59.
-
L-14 Low Incorrect Receiver Check Validation Resolved
Description
In the
executeWithdraw2ContractV2function, the zero-address check for the receiver is performed onwithdraw.senderinstead ofwithdraw.receiver.Recommendation
Use
withdraw.receiverin the check if the sender and receiver are not enforced to be the same at all times.Resolution
Orderly Team: The issue was resolved in LedgerImplD.sol#L188.
-
L-15 Low No ETH Refund When depositFee Is Disabled Unexpected Behavior Acknowledged
Description
Vault._deposit()andVault._ethDeposit()don't refund the excessmsg.valuesent ifdepositFeeEnabled = false.While it's not expected for users to send additional funds in that case, such transaction may be executed because of a configuration change.
For example:
- User deposits when fee is enabled
- Owner disables the fee
- User's transaction is executed and they don't get refund
Recommendation
Consider refunding the user if deposit fee is not enabled.
Resolution
Orderly Team: Acknowledged.
-
L-16 Low SolConnector Balance Cannot Be Withdrawn Logical Error Resolved
Description
The
withdrawandwithdraw2ContractV2functions inSolConnectorare not payable. However, these functions need to sendLayerZeromessaging fees during_lzSend.Therefore, the contract must hold a native token balance to cover the required fees.
However, the contract does not provide a function that enables the owner or admin to withdraw its balance if necessary.
Recommendation
Consider adding a withdraw function that allows a trusted role to withdraw the balance.
Resolution
Orderly Team: The issue was resolved in commit d3897a2.
-
L-17 Low Mixed Events In LedgerImplD Events Resolved
Description
LedgerImplD.executeWithdraw2ContractV2()emits wrong events:- if state = 0, it emits AccountWithdrawSolFail without checking whether the destination chain type
is EVM or SOL
- for EVM withdrawals it emits AccountWithdrawSolApprove which is the wrong event.
Recommendation
To fix
1)manually check the chain type and emit the correct event - eitherAccountWithdrawApproveorAccountWithdrawSolApprove.To fix
2)change the event emission toAccountWithdrawApproveResolution
Orderly Team: The issue was resolved in LedgerImplD.sol#L59.
-
L-18 Low Weaponizing Swaps To Drain The Ledger Logical Error Acknowledged
Description
The Vault.delegateSwap() function opens up a window between executing the swap and and uploading it which can be leveraged by an attacker to drain the Ledger.
Example: Initial state: Alice deposits 100k USDC + 10 WETH Bob deposits 3 WETH Vault USDC = 100k; Vault WETH = 13
A swap is initiated via
Vault.delegateSwap(). This swaps Bob's 3 WETH for 9000 USDC- The vault now holds 109k USDC + 10 WETH (ledger not updated)
- The moment the swap was signed, Bob submitted a withdrawal request of their 3 WETH. Because the
result of the swap is still not uploaded, Bob's WETH balance on the Ledger side is still 3, the balance check passes and a withdrawal message is successfully sent to the Vault
- The withdrawal is executed, Bob receives 3 WETH on the EVM chain and the Ledger updates their WETH
balance to 0, as well as the chain's WETH balance to 10 WETH in accountWithdrawFinish
- The swap event is finally uploaded, executeSwapResultUpload() is called and Bob's and the chain's WETH
balances are reduced by 3. Bob now has -3 WETH and the chain has 7 WETH.
- However, the USDC balances will also be updated. Bob's new USDC balance will become 9000 USDC and
the chain's one - 109k USDC
- Now Bob can submit an USDC withdrawal request and withdraw 9000 USDC from the
VaultFinalstate:
Bob withdraws 3 WETH + 9000 USDC
Vault USDC = 100k USDC; Vault WETH = 7 WETHIn result, Bob stole 3 WETH from Alice.
Furthermore, Alice's balance on the Ledger is still 10 WETH so an attempt to withdraw it will revert on the EVM chain and block the messaging channel.
Recommendation
Consider reworking the swap mechanism to be initiated from the Ledger side - just like a withdrawal, by deducting the amount to be swapped from the account and the chain and then uploading a final result.
Resolution
Orderly Team: Acknowledged.
-
I-01 Informational Consider Having Rescue Functionality Informational Acknowledged
Description
User funds are transferred from the user to the Vault on Solana during deposits, and the user's ledger balance increases after the cross-chain message is delivered.
However, there is no mechanism to recover funds if the cross-chain message fails.
Recommendation
Consider implementing an admin-only functionality to recover funds in cases where the source chain execution succeeds but the message delivery fails on the destination chain.
Resolution
Orderly Team: Acknowledged.
-
I-02 Informational Include System_program During Invoke Informational Resolved
Description
The
account_infosfield in invoke andinvoke_signeddoes not include thesystem_programin the codebase, only includes the from and to accounts.However, according to the Solana and Rust documentation, this field should also include the
system_program.Example 2 from the [Solana documentation](https://solana.com/docs/core/cpi#anchor-framework)
A similar pattern can be seen for invoke_signed as well, in Example 2 [here](https://solana.com/docs/core/cpi#anchor-framework-1)
Lastly, this can also be seen in the [Rust documentation](https://docs.rs/solana-cpi/latest/solana_cpi/fn.invoke.html#examples).
Recommendation
Include
system_programinaccount_infoswhen usinginvokeandinvoke_signed.Resolution
Orderly Team: The issue was resolved in commit 2e851ec.
-
I-03 Informational Incomplete bizType Comment Description Informational Resolved
Description
The comment next to the the
bizTypefield of theEventUploadDatastruct describes the event from 1 to 13.struct EventUploadData { uint8 bizType; // 1 - withdraw, 2 - settlement, 3 - adl, 4 - liquidation, 5 - fee distribution, 6 - delegate signer, 7 - delegate withdraw, 12 - balance transfer, 13 - swap result upload uint64 eventId; bytes data; }As we can see events
8-12are not described and event 14 -withdraw2contractV2is not present as well.Recommendation
Consider adding the missing
bizTypesin the commentResolution
Orderly Team: The issue was resolved in commit b97cb8e.
-
I-04 Informational Settlement Balance Check Is Commented Warning Acknowledged
Description
The code from
LedgerImplA.executeSettlement()changes the user's balance according to the settled amount.Before, there was a check that reverts the transaction if the new user balance is negative, but now it's commented out. At the same time, the comment explaining the check is not removed.
// check balance + settledAmount > 0, where balance should cast to int128 first int128 balance = account.balances[settlement.settledAssetHash]; // if (balance + ledgerExecution.settledAmount < 0) { // revert BalanceNotEnough(balance, ledgerExecution.settledAmount); // } account.balances[settlement.settledAssetHash] = balance + ledgerExecution.settledAmount;If
insuranceTransferAmount = 0, the check from the above if statement will be sufficient, but otherwise this will allow users' balances to go below 0.Recommendation
Revisit if the check should stay commented out.
Resolution
Orderly Team: Acknowledged.
-
I-05 Informational OApps Don't Call Skip(), Burn() Or Clear() Best Practices Acknowledged
Description
The
SolConnectorand theSolana Vaultare OApps that can opt-in for ordered delivery. Neither of the two has a way to callskip(),burn()orclear(), they should instead be called by the delegate and the nonce should be updated after that.LayerZerorecommends as a best practice to have the calls to these functions and the nonce update inside the contract to minimize the chances of failed synchronization.Recommendation
Consider adding a way to call these functions and update the nonce afterwards.
Resolution
Orderly Team: Acknowledged.
-
I-06 Informational Hardcoded Roles Best Practices Acknowledged
Description
The newly added
SYMBOL_MANAGER_ROLEandBROKER_MANAGER_ROLEare used inFeeManager,VaultManagerandVault.They are hardcoded in each contract instead of having one central place to fetch them from. This increases the risk of misspelling a role and makes updating the code harder.
Recommendation
Use constants instead and import them in the appropriate files.
Resolution
Orderly Team: Acknowledged.
-
I-07 Informational Deposit Nonce Can Be Changed Configuration Resolved
Description
The
deposit_noncefield can be changed by calling theset_vaultinstruction. If the new value set is less than the current one, nonces will start repeating, defeating its purpose and causing confusion to offchain listeners.Recommendation
Either remove the ability to modify the
deposit_nonceentirely or revert if the new value is below the current one.Resolution
Orderly Team: The issue was resolved in commit e040137.
-
I-08 Informational Outdated Comment Informational Resolved
Description
The comment "
Msg.Type: Withdraw(currently at most 13 accounts, otherwise tx oversize)" in oapp_lz_receive_types.rs is outdated and misleading.Recommendation
Consider removing the comment, as it pertains to a previous version of the codebase.
Resolution
Orderly Team: The issue was resolved in commit 23778dc.
-
I-09 Informational SwapSignature Library Is Not Used Informational Acknowledged
Description
The
contract-evmrepository contains theSwapSignaturelibrary contract. However, this library is not used in the codebase; instead,DelegateSwapSignatureis utilized.Recommendation
Consider whether two different libraries are required for the signature verification.
Resolution
Orderly Team: Acknowledged.
-
I-10 Informational Can Set Arbitrary Hashes For Roles Best Practices Resolved
Description
Both
set_manager_role.rsandset_broker.rstake in hashes with type[u8; 32]for roles. These roles are used for functions likeset_token()to validate the caller has the right role.There is no validation on that hash when assigning someone a role so it would be very easy to have a typo.
Recommendation
Either add on-chain validation by defining potential role hashes in constants or an enum, or make sure to use off-chain validation (like a frontend that only allows you to set certain roles) to avoid typos.
Resolution
Orderly Team: The issue was resolved in commit 196e1f2.
-
I-11 Informational Executor Needs Funds To Initialize ATA Informational Resolved
Description
In case the
receiver_token_accountin thelz_receiveinstruction is empty, it will be initialized and the Executor will have to pay for that. This should be considered when setting themsgOptions.valuein theSolConnector.LayerZeroadvices to haveextraOptionspassed by the user if the recipient account will require initialization. This prevents overcharging on each withdrawal (in case the account is not empty).However, letting the user provide value is not a good idea because they can send a reverting transaction on purpose to DOS the app if it is using ordered delivery.
Recommendation
Account for this possible ATA initialization when setting
msgOptions.value.Resolution
Orderly Team: Resolved.
-
I-12 Informational Withdrawals May Drain The Connector Informational Acknowledged
Description
Since
SolConnectorpays for each relayed message to Solana Vault, frequent withdrawals may cause it to run out of funds.The existing fee mechanism will probably disincentivize users from spamming such requests, but only if the withdrawn amount is high enough.
Recommendation
You can introduce minimum amount to be withdrawn and control the fee accordingly so users don't spam requests.
In addition, you can consider adding a mechanism which makes the user pay for their withdrawal fee.
Resolution
Orderly Team: Acknowledged.
-
I-13 Informational Technically Possible Underflow Best Practices Resolved
Description
The let
amount_to_transfer = withdraw_params.token_amount - withdraw_params.fee;on line 141 ofoapp_lz_receive.rscan theoretically underflow.It won't underflow because of this check in the source chain:
a Guardian proof of concept
0547ce37361f70eb765160e26421/src/LedgerImplC.sol#L86
But it would still be a best practice, to use
checked_subhere. Specifically, notsaturating_subbutchecked_sub(read here).Recommendation
It would be a best practice, to use
checked_subhere. Specifically, notsaturating_subbutchecked_sub(read here).Resolution
Orderly Team: The issue was resolved in commit 47ffbf4.
-
I-14 Informational ATA Check Is Not Used For The Logical Forks Superfluous Code Resolved
Description
Look for check if the
receiver_token_accountis the correct associated token account inoapp_lz_receive.rs(lines of code).That block of code where the program gets the receiver ATA is not needed when transferring SOL. It is not used in the if
token_index = TOKEN_INDEX_SOL{ logic fork.Recommendation
Move the
get_associated_token_addresscheck into theelselogical fork of theif token_index =TOKEN_INDEX_SOL.Resolution
Orderly Team: The issue was resolved in commit 6c6fa50.
-
I-15 Informational SOL Mint_account Should Stay 0 Informational Resolved
Description
In order to use a
deposit_tokenin thedeposit.rsinstruction the following condition must be true:deposit_token.key() = allowed_token.mint_account allowed_token.allowed = true.The
allowed_tokenis a PDA which will haveallowed = truefor all the supported SPLs and the nativeSOLtoken.The
SOLdeposits are handled in a different instruction nameddeposit_sol.rs, where themint_accountfield is not used.If
mint_accountwere ever set to a validmint_accountit would enable executing thedepositinstruction with that token.Recommendation
You should never set the
mint_accountfield to a value different than 0.Resolution
Orderly Team: The issue was resolved in commit f8cd1b9.
Remediation Review
11 findings-
H-01 High Delegated Withdrawals Cannot Be Executed DoS Resolved
Description
OperatorManagerImplB._processEventUpload()was modified to fetch the needed function selector for the current operation depending on thebizType.After that
dataOffset = 32is executed if the function to be executed includes a dynamic struct in its parameters. The actual call is then performed and the calldata construction differs betweendataOffset = 0anddataOffset = 32.The functions related to
bizType 1andbizType 7have different names, but both of them accept theEventTypes.WithdrawDatadynamic struct. However, thedataOffsetis assigned onlyfor bizType 1.Because of this, the calldata will be incorrectly encoded and any attempts to execute
executeDelegateWithdrawAction()will either revert, or worse, execute with unexpected inputs if the calldata can be decoded into the needed fields.If
executeDelegateWithdrawAction()revert not only users won't receive their funds, but the whole batch upload will be blocked.Recommendation
Include
bizType = 7in the if statement• if (data.bizType = 1 || data.bizType = 2 || data.bizType = 4 || data.bizType = 9 || data.bizType = 10) { // dynamic event types: withdraw, settlement, liquidation, liquidationV2, withdrawSol + if (data.bizType = 1 || data.bizType = 2 || data.bizType = 4 || data.bizType = 7 || data.bizType = 9 || data.bizType = 10) { // dynamic event types: withdraw, settlement, liquidation, delegateWithdraw, liquidationV2, withdrawSol dataOffset = 32; // 0x20 for dynamic event types }Resolution
Orderly Team: The issue was resolved in commit 6c1db46.
-
H-02 High Withdrawals Can Block The Channel DoS Resolved
Description
Proof of concept: PoC
The M-01 issue of the main review has to be fixed because if not, anyone can block the EVM > Solana pathway, especially when
inbound_noncecannot be modified anymore because of the change in theset_vaultinstruction.As discussed in the previous issue, if the receiving account is executable or its balance after the transfer will be below what's required to keep that account rent exempt, the transaction will revert.
In addition, if the receiver is a reserved account, for example the System program or the
SysVar, Solana will demote that to read-only even if it was passed as writable: true in the transaction.This will cause a failure when Anchor applies the accounts constraints, before any of the program code is executed, because of the mut constraint of the receiver.
Recommendation
To fix the problem with the executable accounts and also the rent exemption issue, execute the lamports transfer only if the account will be exempt and is not executable. However, the write demotion issue is not easily fixable.
Consider adding a list of reserved accounts to the BE and perform checks before submitting a Solana withdrawal on the Ledger side. This will reduce the possibility of the
receiverbeing demoted.The list of reserved accounts has to be updated as Solana updates these accounts. In addition, be ready to skip a given nonce if such account slipped through.
Resolution
Orderly Team: The issue was resolved in commit 4582ca7.
-
M-01 Medium Escrowed Balance Can Be Withdrawn Validation Acknowledged
Description
LedgerImplD.executeWithdraw2ContractV2()allows users to withdraw their whole balance, including the escrowed one which has still not been fully transferred. There is a TODO comment in the_executeWithdraw2SOLthat talks about this check.else if (account.balances[tokenHash] < withdrawV2.tokenAmount.toInt128()) { // TODO: check escrowBalance revert WithdrawBalanceNotEnough(account.balances[tokenHash], withdrawV2.tokenAmount); } Deploying without this check will allow anyone to withdraw their escrowed balance which doesn't align with the behavior for normal withdrawals.Recommendation
Consider implementing the check in both
_executeWithdraw2SOL()and_executeWithdraw2EVM()Resolution
Orderly Team: Acknowledged.
-
L-01 Low Changed Schema For Event Uploading Validation Acknowledged
Description
Before the fix review, the
EventUploadData.datafield was the abi encoded struct for the given function.However, the new code in
OperatorManagerImplB._processEventUpload()cuts the firstdataOffsetbytes out if thedatafieldbytes memory dataWithoutOffset = abi.encodePacked(data.data[dataOffset:]);This means the schema for the
datafield has to be also changed forbizTypewheredataOffset = 0in such a way that the encoded struct starts from adataOffsetposition.Recommendation
Consider if this change in the schema is desired. If not, you can use the raw
datafield without slicing.Resolution
Orderly Team: Acknowledged.
-
L-02 Low Duplicate lastEngineEventId Assignment Superfluous Code Resolved
Description
In the
_executeWithdraw2SOLfunction of theLedgerImplDcontract,lastEngineEventIdis assigned twice, once at line 236 and again at line 250.Recommendation
Remove one of the assignments.
Resolution
Orderly Team: The issue was resolved in commit 51a7a28.
-
L-03 Low batchGetUserLedger Doesn't Include Escrow Best Practices Acknowledged
Description
The
Ledger.batchGetUserLedgerreturns a snapshot of the user balances, one of which fields isAccountTokenBalances tokenBalances.This struct includes the
balanceandfrozenBalanceof the user, but not theirescrowBalance. This missing piece of information may result in the BE making wrong decisions, depending on how it's used.struct AccountTokenBalances { // token hash bytes32 tokenHash; // balance & frozenBalance int128 balance; uint128 frozenBalance; }Recommendation
Consider adding
escrowBalancesin theAccountTokenBalancesstructResolution
Orderly Team: Acknowledged.
-
L-04 Low Missing Funds Because Of Balance Transfers Unexpected Behavior Acknowledged
Description
LedgerImplC.executeBalanceTransfer()executes transfers between users, but it allows the receiver to use the funds in the Orderly system before they have been removed from the sender account.This approach is not efficient because it allows the sender of the funds to also use these funds, or even withdraw them after they have been credited, but yet not debited.
Then, when the transfer is being finalized,
_applyDebit()will reduce the sender balance and it will drop to a negative value if it cannot cover the initially transferred amount.fromAccount.subBalance(tokenHash, amount);Funds have been stolen from the system in result of using the balance transfer feature.
Recommendation
Reconsider the addition of this feature.
Resolution
Orderly Team: Acknowledged.
-
I-01 Informational Redundant Comment Best Practices Acknowledged
Description
When
vault_deposit_paramsare declared indeposit_sol:apply(), there is a redundant empty comment afteruser_address.let vault_deposit_params = VaultDepositParams { account_id: deposit_params.account_id, broker_hash: deposit_params.broker_hash, user_address: deposit_params.user_address, // token_hash: SOL_TOKEN_HASH, src_chain_id: ctx.accounts.vault_authority.sol_chain_id, token_amount: deposit_params.token_amount as u128, src_chain_deposit_nonce: ctx.accounts.vault_authority.deposit_nonce, };Recommendation
Remove the comment
Resolution
Orderly Team: Acknowledged.
-
I-02 Informational Deposit Transaction Sizes Can Be Decreased Best Practices Acknowledged
Description
The
deposit_solinstruction doesn't readDepositParams.token_hashanymore, but the field is still needed to initiate the transaction which causes a larger overall transaction size.Recommendation
You can use different struct for
deposit_solwhich doesn't includetoken_hashin order to reduce the tx size.Resolution
Orderly Team: Acknowledged.
-
I-03 Informational Typo Informational Resolved
Description
The name of the
Vault.attempTransferETH()function is spelled incorrectly. It should beattemptTransferETH().Recommendation
Fix the typo.
Resolution
Orderly Team: The issue was resolved in commit fca2f00.
-
I-04 Informational Unused _checkAccount() Function Best Practices Resolved
Description
The
_checkAccount()function inLedgerImplD.solis not used.Recommendation
Remove the function.
Resolution
Orderly Team: The issue was resolved in commit 5303ac3.
No findings match.
More from Orderly
All 8 reports-
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 -
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.
