Guardian's review of Contract Updates for Ethereal, published October 2025. The report records 59 findings across 2 review rounds, including 5 high and 4 medium.
- Published
- Review window
- September 24 to October 16, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Ethereum, Converge, Offchain
- Sector
- Perpetuals
- 0 Critical
- 5 High
- 4 Medium
- 22 Low
- 28 Informational
Scope
Findings 59
Main Review
56 findings · September 24 to October 1, 2025-
H-01 High ProcessActions Blocked By Malicious Contract DoS Resolved
Description
The
isValidSignaturefunction is intended to not revert to allow theprocessActionsloop to complete when a_handleRevokeLinkedSigneror_handleLinkSigneraction is performed.However
isValidSignatureexecution can be forced to revert by the sender account address if it houses bytecode and implements aisValidSignaturefunction that returns an invalid return payload.For example, if the
isValidSignaturefunction returns empty returndata but successfully executes, the_verifyEip1271Signaturefunction will still attempt to decode the returndata and revert with anabi.decodeerror.This will block the
processActionsqueue from processing the batch of actions and hold up the exchange.Recommendation
Decode the
magicValueby casting thereturnDatato abytes4object to capture only the first 4 bytes of thereturnData, and produce an emptybytes4object if noreturnDataexists:bytes4 magicValue = bytes4(returnData); -
H-02 High
rescueFundsFails Due To MissingpayableLogical Error ResolvedDescription
When a withdrawal remains pending for an extended period, the rescueFunds function is invoked to finalize the withdrawal and recover the funds.
However, the
rescueFundsfunction is not payable, while the_completeWithdrawalfunction called during this process requiresmsg.value. As a result, funds cannot be rescued if withdrawn via LayerZero OFT.Recommendation
Make the
rescueFundsfunction payable. -
H-03 High processActions Blocked By Gas Griefing Gas Griefing Resolved
Description
The
processActionsfunction may invoke an untrusted external call when it attempts an ERC1271 contract signature validation for the_handleLinkSigneror_handleRevokeLinkedSigneractions.This call is protected as much as possible, using staticcall and success handling to ensure no reverts can occur. However the amount of gas that is forwarded to the
senderaddress is only limited by the 63/64 rule and therefore allows the untrustedsenderaddress to expend an enormous amount of gas, leaving an insufficient amount for the completion of theprocessActionsloop.Furthermore, all of the untrusted
sender'sreturnDatais loaded in, even if a cap is made on the amount of gas that is forwarded to thesender, thesenderaddress can cause an unexpected amount of gas to be used by loading in a large amount ofreturnData. This can additionally cause unexpectedly high gas usage which may prevent the processing of theprocessActionsloop, even if a mere gas forwarding limit is put in place.Recommendation
Put in place a limit on the amount of gas forwarded to the
senderaddress with the invocation of theisValidSignaturefunction. And furthermore, only load in the necessary 4 bytes ofreturnDatafrom thesenderwith a low level call. -
H-04 High
finalizeWithdrawalsDoesn't Work As Expected Logical Error ResolvedDescription
The
finalizeWithdrawalsfunction in the Claimer sendsmsg.valueinside a for loop. During the first iteration, the entiremsg.valueis sent to theexchangeGateway, leaving no balance for subsequent iterations. As a result, only the first withdrawal succeeds, while the remaining ones fail.Recommendation
Transfer only the required amount of value in each iteration.
-
H-05 High Insolvent Accounts Break Balance Accounting Logical Error Acknowledged
Description
The addition of the
fundingCoverageChargeindicates that the protocol intends to support insolvent liquidations, particularly in the case where the cumulative funding across all products for the account is greater than the available account usde margin balance. However across many cases, the accounting for this breaks down.The
userEquityAfterFundingis determined as the usde token balance of the account onchain less the cumulative funding across all products. This amount is then assigned as thereferenceFundingCoverageChargewhich must match the providedaction.fundingCoverageChargewithin a precision threshold of 13 wei, although the comment purports this to be exact, not requiring this threshold.In many cases, the user will not be able to cover their funding charges net of PnL, however this will not be captured by the userEquityAfterFunding as it does not include negative PnL from positions.
Consider the following example:
- Trader A has 100 USDe available margin balance
- Trader A has -$80 PnL
- Trader A has a pending funding of $30 to pay
- Trader A certainly qualifies for the insolvent fundingCoverageCharge case, however the computed userEquityAfterFunding is $70 which does not qualify as it is positive
Furthermore, accounts that are liquidated while they have a net profit overstate their equity after funding and deduct more than they should from the liquidator:
- Trader A has 20 USDe available margin balance
- Trader A has $30 PnL
- Trader A has a pending funding of $30 to pay
- Trader A can technically cover this funding charge if all is settled, however they still appear as if they are insolvent as measured by the userEquityAfterFunding which reports an equity of -$10
Recommendation
One solution to this would be to settle the account PnL right before liquidation every time, however this still comes with rounding and potential price discrepancy concerns.
The funding coverage charge logic should instead be updated to account for the account’s net balance factoring in PnL to cover true account insolvency cases when and account cannot cover funding net of their PnL.
-
M-01 Medium finalizeWithdrawals Gas Griefing Gas Griefing Resolved
Description
In the
finalizeWithdrawalsfunction, the whitelistedwithdrawalClaimersor theownerprocess batches of withdrawal objects to send these funds to users.The execution of
finalizeWithdrawmakes an untrusted external call to the account with thesafeTransferUSDefunction.The
safeTransferUSDesafely does not load in anyreturnDatafrom theaccountaddress invocation, however it does forward the entire gas available at the time of the call execution.As a result, if the
accountaddress is home to a contract or an EOA that has associated bytecode through EIP 7702 it can expend an unexpected amount of gas and use up the entire 63/64 remaining gas that was forwarded to it.It is unlikely that the
finalizeWithdrawalsfunction would be able to complete execution with roughly 1/64th of the initial gas provided, especially if there are other withdrawals to process. So this results in a DoS of thefinalizeWithdrawalsloop for any withdrawal batches that would include malicious actions.Recommendation
Create a version of the
safeTransferUSDefunction that only forwards a configured safe gas amount so that the account cannot gas grief the executor and potentially halt the processing of other withdrawals. -
M-02 Medium Invalid Signatures Block processActions DoS Resolved
Description
For EOA signature validation the
ECDSA.recoverfunction from the Solady library is used to verify the signer of the message, however this recover function reverts upon receiving malformed signatures.The
verifierLibisValidSignaturefunction intends to not revert but instead return a boolean indicating successful verification to prevent stopping theprocessActionsloop.As a result if a malformed signature is provided to the signer update actions this can result in a block of the
processActionsloop.Recommendation
Instead of
ECDSA.recover, use theECDSA.tryRecoverfunction from the Solady library. -
M-05 Medium realizePnl Gives Traders Infinite Liquidity Gaming Acknowledged
Description
In the
realizePnlfunction the trader’s pending unrealized PnL is moved into their account balance without having to be matched against other orders on the orderbook. This allows a trader to realize their on-paper gains on a perp position without experiencing any market impact.In some cases with very large positions or especially thin order books near the market price, a trader may net more from the (realizePnl action - initial margin) than compared to actually exiting their position and withdrawing the resulting usde balance that remains after experiencing price impact in the market.
Recommendation
When realizing the PnL for a trader with the
realizePnlfunction, be sure to only realize profit to a conservative market price, taking into account the current market price and the orderbook depth relative to the size of the position being realized. -
M-06 Medium newCost Underflow Edge Case DoS Rounding Acknowledged
Description
In the FLIP case in the
_settleLiquidatorPositionfunction thenewCostis calculated based on:newCost = eTransferredNotional - (eLiquidatorRealizedPnl * oldSideSign) - cOldCostWhich is then cast to a
uintvalue. However in some cases, where the flipped amount is small this can lead to an underflow revert. Consider the following scenario, which requires a small lotSize.Index token price is $5. The liquidator has a 100,000 index token long position, the cost is $500,000
Liquidated Position collateral = 2505 USDe
Liquidated Position size = 100000e9 + 1, short
Liquidated Position Notional = (100000e9 + 1) * $5 = 500000000000005
Liquidated Position cost = 500000000000005
maxLeverage = 100x
Margin factor = 0.5x
The index token price stays at $5
A Funding of $10 is charged
The position is left with:
Position Notional = (100e9 + 1) * 1,000 = 100000000001000 Pending PnL = 0 Net Margin = 2495e9 Maintenance margin is 500000000000005 * 1/200 = 2500000000000
Liquidation Occurs
rt.oldSize = 100000e9 rt.newSize = -1 rt.quantity = 100000e9 + 1 eTransferredNotional = (100000e9 + 1) * 5 + 2495e9 = 502495000000005 cOldCost = 500000e9 cCloseNotional = 100000e9 * 502495000000005 / (100000e9 + 1) = 502494999999999 oldSideSign = 1 eLiquidatorRealizedPnl = 502494999999999 - 500000e9 = 2494999999999 (with exact accuracy to the contracts reference) newCost = 502495000000005 - (2494999999999) - 500000e9 = 6The
eLiquidatorRealizedPnlinaccuracy threshold is however(eTransferredNotional >> 52) + 13, which is trivially larger than 6, thus allowing the newCost resulting value to often be negative in this case and resulting in a casting underflow revert.Not only will this revert prevent the liquidation from occurring, but it will also halt the batch that is actively being processed.
Recommendation
Firstly, do not configure products with a
lotSizeless than an order of magnitude of 100 wei to be safe and consider adding validation at the contract level to enforce this configuration.Furthermore, consider flooring the newCost to zero in the event that this case does arise to avoid harmful reverts.
-
L-01 Low lastWithdrawRequestId Increased Even When Failed Logical Error Acknowledged
Description
The
finalizeWithdrawalsfunction in the Claimer contract advanceslastWithdrawRequestIdto the last ID in the batch, even if some withdrawals in the batch fail. Since the function expects IDs to be strictly increasing, the failed withdrawals cannot be retried by the claimer.Recommendation
Consider tracking the IDs of failed withdrawals and allowing the claimer to retry them if necessary.
-
L-03 Low Lacking _completeWithdrawal Refund Logic Unexpected Behavior Resolved
Description
In the
_completeWithdrawalfunction there is validation to ensure that the providedmsg.valueis greater than or equal to the necessarymessagingFee.nativeFeefee. However there is no logic to refund themsg.senderin the event that the providedmsg.valueis greater than the necessarymessagingFee.nativeFee.The layerzero
oft.sendcall does use themsg.senderas the refund recipient, however thisoft.sendcall will not provide any refund ever since the forwarded value is simply themessagingFee.nativeFee + amountLD.Recommendation
Implement refund logic at the end of the
_completeWithdrawalfunction that refunds the excess msg.value, notice that theoft.sendrefund cannot be used since it requires exact payment. -
L-05 Low Expired Actions Halt processActions Warning Acknowledged
Description
In the exchange.verifyBeforeActionExpiry function, if the expiry of the action has come to pass then the operation reverts, blocking subsequent action processing.
This is in contrast to other areas where non-reverting failures are preferred to allow other actions to continue processing such as invalid signatures with the isSignatureValid function.
Be aware that it may be possible for actors to block stuff for a short period of time to delay a processActions batch until one of these expiries are breached.
Recommendation
Consider if actions should silently be skipped or revert the whole processActions batch if they are expired. This has implications for how an action might affect others in the batch if they are unsuccessful.
-
L-06 Low RevokeLinkedSigner Ineffective Expiry Check Validation Resolved
Description
The
verifyBeforeActionExpiryvalidation comes after the failed signature validation early return case where thependingSignerRevokeentry is assigned to true. As a result, signatures that are made invalid by the sender after sending them, such as a gnosis safe consuming the related nonce, can be marked as pending in the pendingSignerRevoke mapping.These actions cannot be replayed due to the nonce check in the
revokeSignerfunction, however to avoid any unexpected behavior the expiry validation should be carried out before the invalid signature early return case.Recommendation
Move the
verifyBeforeActionExpiryvalidation to before the!isSignatureValidcase handling. -
L-07 Low Lacking Funding Update Guardrails Trust Assumptions Acknowledged
Description
In the updateFunding function there are no guardrails which prevent the sequencer from calling the function with a completely invalid fundingDelta that either awards users with a large sum of USDe, or causes them to immediately become insolvent. This may be a mechanism through which a compromised sequencer account drains the funds in the ExchangeGateway contract.
Recommendation
Be aware of this risk and consider adding guardrails to how much the
fundingDeltaUsdcan change over time. -
L-08 Low Missing receive In Claimer Logical Error Resolved
Description
During the
finalizeWithdrawalsflow, theClaimersendsmsg.valueto theexchangeGateway, but this value is not based on the LayerZero quote. This means the Claimer must send more than enough to theexchangeGateway. However, theexchangeGatewaytransfers only the exact required amount to LayerZero, and the excess is never refunded to the Claimer.Additionally, the Claimer does not implement a receive function to accept refunds, even if a refund mechanism were introduced.
Recommendation
Add a receive function to the Claimer contract and refund any excess back to the Claimer.
-
L-10 Low Off By One Error In ActionDecoder Unexpected Behavior Resolved
Description
When
action.length == MATCH_ORDER_TAKER_SIGNATURE_LENGTH, the if condition below evaluates to false and the code proceeds. However, accessingaction[MATCH_ORDER_TAKER_SIGNATURE_LENGTH]is out of bounds, since valid indices are0..action.length-1. This results in a panic error instead of reverting with the intendedInvalidActionEncodingcustom error.// There must be enough data to read both signature lengths if (action.length < MATCH_ORDER_TAKER_SIGNATURE_LENGTH) { (ActionId actionId, ActionType actionType) = decodeActionHeader(action); revert InvalidActionEncoding(actionId, actionType); } uint16 makerSignatureLength = uint8(action[MATCH_ORDER_MAKER_SIGNATURE_LENGTH]); uint16 takerSignatureLength = uint8(action[MATCH_ORDER_TAKER_SIGNATURE_LENGTH]);Recommendation
Revert if
action.length <= MATCH_ORDER_TAKER_SIGNATURE_LENGTH -
L-11 Low Unsafe Casting Validation Acknowledged
Description
Throughout the Ethereal exchange casts are made without the use of
SafeCastLib.In particular the cast of the
currentCosttoint128is at risk of overflow in the realizePnL function in the rare case where the currentCost exceeds the int128.max value.int128 newCost = int128(currentCost) + costDelta;Recommendation
Use
SafeCastfor this cast in therealizePnLfunction. -
L-12 Low Unexpected OOB Decoding Revert Validation Resolved
Description
When
action.length < 9,decodeMatchOrdersActioncallsdecodeActionHeaderto populateInvalidActionEncoding, butdecodeActionHeaderreadsbytes8(action[:8])andaction[8], which will revert with an out-of-bounds panic. This produces an unexpected generic revert rather than the intendedInvalidActionEncodingerror, complicating upstream error handling.Recommendation
Before calling decodeActionHeader for error reporting, first check
action.length >= ACTION_HEADER_OFFSET(9). If shorter, revert InvalidActionEncoding with zeroed id/type or a dedicated error that does not attempt to parse the header. -
L-13 Low Removal Misses Pending Withdrawals Validation Acknowledged
Description
The
removeTokenfunction only checksAccount.DataGlobal.totalBalanceandtotalPendingBalance, but does not verify whether any withdrawals for the token are pending. Removing a token zeroes itsToken.Data(including tokenAddress and oftAddress), which can causefinalizeWithdrawto lack required metadata (e.g., ERC20 address or OFT adapter) and revert or behave incorrectly for already-initiated withdrawals.Recommendation
Before allowing removal, also verify there are no pending withdrawals for the token across all accounts, perhaps by maintaining this count in storage per token.
-
L-14 Low Lacking Pyth Lazer Channel Validation Validation Acknowledged
Description
In the
_parsePythPriceFeedfunction there is no validation that confirms the channel that is being used for the Pyth lazer price.The Pyth Lazer example project includes a channel validation to validate that the channel is the RealTime channel.
Recommendation
Consider adding an explicit validation at the contract level that the channel is as expected and matches the RealTime channel:
if (channel != PythLazerLib.Channel.RealTime) { revert("expected update from RealTime channel"); }
-
L-15 Low Mismatch Event Missed For 1 Wei Inaccuracy Logical Error Resolved
Description
Throughout the PerpEngine and Liquidation contracts the verification functions which assess the tolerance difference between the provided offchain value and the value computed onchain emit an event when there is a mismatch between the offchain and onchain values that is below the tolerance threshold and thus acceptable.
However if the mismatch is 1 wei then no event is emitted, even though a mismatch was found, this is because the event emission case always uses
else if (absDiff > 1).Recommendation
Compare the
absDiffto see if it is greater than zero:else if (absDiff > 0). -
L-16 Low Missing updateDelegateDepositMaxBatchSize Event Events Resolved
Description
In the
updateDelegateDepositMaxBatchSizefunction there is no event emitted to denote the update as described in theREADMEand as is done in other configuration functions.Recommendation
Consider adding an event to signal the update made in the
updateDelegateDepositMaxBatchSizefunction. -
L-17 Low Pyth Lazer Refunds Are Stuck Warning Acknowledged
Description
In the liquidation flow the pythFee is forwarded to the pythLazer contract which refunds excess native that might have been sent. There is however no logic to process refunds that may have been sent back by Pyth Lazer on the liquidation action.
Recommendation
Consider adding a function that allows the contract owner to sweep any native assets which may have been refunded.
-
L-18 Low Used Nonce Permanently Prevents Signer Revoke DoS Acknowledged
Description
In the
_handleRevokeLinkedSignerfunction thependingSignerRevokemapping entry for the subaccount and signer is set astruewhen the signature is invalid as reported by theisValidSignaturefunction. When this case occurs, therevokeSignerfunction will be called in thefinalizeRevokeLinkedSignerfunction to clear this pending withdrawal.While the pending withdrawal is present, the signer cannot be revoked for that subaccount with any other signature.
As a result, if a message is ever submitted that uses a nonce that was already consumed for the
RevokeLinkedSigneraction type, then the revocation will be stuck forever, as the finalizeRevokeLinkedSigner also validates that the nonce was not previously used in therevokeSignerfunction.Recommendation
Since the signature verification is being skipped in the
finalizeRevokeLinkedSignerfunction, consider if the nonce verification can be skipped in this instance as well, while still marking it idempotently as used after the finalization. Otherwise ensure that this case can never arise through the sequencer. -
L-19 Low destinationAddress Ignored For Native Withdrawal Logical Error Resolved
Description
In the
_completeWithdrawalfunction when withdrawals are made using the native L3 chain instead of OFT withdrawals, thewithdraw.destinationAddressis ignored and the withdrawal is always sent to the account.However for some accounts, especially those that use the EIP 1271 integration, they may not expect to receive the USDe funds and are expecting them to go to the
destinationAddressthey specified.In some cases this could end up trapping USDe for account addresses that cannot handle it.
Recommendation
Either honor the
withdrawal.destinationAddressduring the_completeWithdrawalflow for native chain withdrawals, or explicitly validate that thedestinationAddressis not configured for withdrawals that will use adestinationEndpointIdof0or the exchangelzEndpointId. -
L-21 Low Inexact Balance Updates Perturb Total Balance Rounding Acknowledged
Description
Throughout the Exchange contracts, in liquidation and order matching, the expected pnl of the action is returned from the settlement action. The expected pnl of the action can however have a discrepancy of a handful of wei, this can cause drift from the totalBalance recorded, breaking the invariant that the sum of user balances is the same as the totalBalance.
This drift from the totalBalance can cause issues when removing a token or when the final depositor for a token is attempting to withdraw.
Firstly, it may not be possible for the totalBalance to reach zero due to the drift between the individual balances entries. Notice that the
removeTokenfunction validates that thetotalBalancesmapping is exactly 0 for the token before carrying out the removal.Secondly, a final depositor may encounter a revert when attempting to withdraw all of their balance due to an underflow when deducting the withdrawal amount from the totalBalance. Or the fee claimer may experience a revert when attempting to claim their final fee amount.
Recommendation
Consider accounting for these last depositor/shutdown cases by maintaining track of the number of accounts with deposits for a token. If that amount of accounts would become zero, then reset the totalBalances mapping entry for the token to zero.
-
L-22 Low Precision Threshold Inconsistency Unexpected Behavior Resolved
Description
The
_verifyFillNotionalfunction performs precision validation on the fill notional amount with a tolerance of (internalValue >> 52) + 1.However elsewhere in the liquidation transferred notional validation, the imprecision validation is performed with (eTransferredNotional >> 52) + 13.
This yields inconsistent precision thresholds in these cases and across the codebase which may result in unexpected tolerance invalidations if the offchain system is meeting the lower precision threshold across the board.
Recommendation
Consider standardizing on a precision constant, or document why these cases deviate from each other.
-
L-23 Low maintenanceMargin Precision Preservation Rounding Resolved
Description
In the
liquidateSubaccountfunction, the calculation of the maintenance margin divides theMAINTENANCE_MARGIN_RATIOby theproduct.maxLeveragebefore ultimately performing the multiplication withmarkNotionalX18.This does not favor the highest precision possible and may contribute to some precision inaccuracies and even reverts outside of the prevision tolerance threshold.
Recommendation
Consider refactoring the
maintenanceMargincomputation such that it maintains as much precision as it possible can. -
L-24 Low ETH Trapped In Claimer Unexpected Behavior Resolved
Description
Because the Claimer contract does not revert but continues in the loop for any failed withdrawal finalizations, the ETH that was intended to be sent to these finalizations may become trapped in the Claimer contract if not carefully refunded.
Recommendation
Along with the fix to H-04, consider adding a refund mechanism to refund the caller for any ETH that went unused due to a failed withdrawal finalization.
-
I-01 Informational removeProtected Typo Documentation Resolved
Description
In the comment for the removeProtected variable there is a typo which confuses the purpose of removeProtected:
/// If the token is used by a product (as either a quote or base asset), it cannot never be removed.When in fact the product cannot
everbe removed.Recommendation
Correct the comment to
/// If the token is used by a product (as either a quote or base asset), it cannot ever be removed. -
I-02 Informational finalizeWithdrawals Typo Documentation Resolved
Description
In the Claimer finalizeWithdrawals function finalize is misspelled as finialize.
Recommendation
Correct it to finalize.
-
I-04 Informational Duplicate Proxy Contracts Best Practices Resolved
Description
The project includes two proxy contracts,
EtherealProxy.solandEtherealClaimerProxy.sol. Both contracts are identical implementations ofERC1967Proxy. Maintaining duplicate proxy contracts adds unnecessary complexity to the codebase.Recommendation
Use a single proxy contract to avoid duplication. If two proxies are needed, clearly document their different roles.
-
I-05 Informational Unnecessary _delegateCallVerifier Function Superfluous Code Resolved
Description
In the
ActionHandlercontract the_delegateCallVerifieris unused as the verifier has been turned into a library.Recommendation
Remove the
_delegateCallVerifierfunction. -
I-06 Informational Domain Separator Version Cannot Be Changed Warning Resolved
Description
The
_hashTypedDatafunction in the SoladyEIP712library only re-computes the domain separator if the_domainNameAndVersionMayChangefunction returns true.However the Ethereal Exchange does not override and return true for the
_domainNameAndVersionMayChangefunction, therefore when the version of the exchange is bumped, or if the name of the exchange for the domain separator is changed then the corresponding signing domain will not be updated. This is actually the correct behavior, as an updated signing domain would change the resultingwithdrawDigestthat keys pending withdrawals and cause them to be zero’d out.As a result, the initial value that is provided as the
VERSIONconstant must always remain as the version once in production.Recommendation
Be aware of this behavior and do not plan on updating the
VERSIONconstant as 1) it will not update the signing domain due to a lacking override of_domainNameAndVersionMayChangeand 2) even if this override is made, it would be catastrophic to update theVERSIONas it would re-key pending actions. -
I-07 Informational finalizeRevokeLinkedSigner Has No Delay Validation Acknowledged
Description
The
finalizeRevokeLinkedSignerfunction has no delay applied to the validation performed in the function.As a result a signer could be immediately revoked by the owner and sequencer without providing a valid signature from the user.
Recommendation
Consider if this should be allowed on the exchange without a delay, and if not, consider adding delay validation similar to the rescue funds function.
-
I-08 Informational Unused Errors Error Resolved
Description
In the
Errorslibrary there exist theWithdrawFounderror is unused.Recommendation
Consider removing the
WithdrawFounderror or moving it into theDeprecatedErrorslibrary as needed. -
I-09 Informational Missing Product Id Sanity Check Validation Resolved
Description
In the
realizeFundingfunction the provided list of productIds is iterated over andPerpProduct.load(productIds[i])is used to load in the relevant product storage pointer.On the following line the productId is assigned to the
product.idfrom the storage pointer, however there is no validation performed to ensure as a sanity check that the resultingproductIdis the same as theproductIdsentry of the current index.Recommendation
Consider adding a validation to ensure that the declared
productId == productIds[i]. -
I-10 Informational Max Quantity Is Not Always Abided Warning Acknowledged
Description
In the PerpEngine contract the _verifyOrder function validates that the order.quantity is not above the product’s specified max quantity. However the max quantity can be surpassed by an order which assigns a quantity of 0 and is treated as a full position close.
In the case where the position size exceeds the product’s specified maxQuantity, this order can be larger than expected. Especially if matched with another 0 quantity order for a position of the opposite side, this can allow for unexpectedly large matches to occur.
Furthermore, during liquidation there is no validation of the sizeDelta against the maxQuantity. Though this is expected, this finding notes that the maxQuantity does not apply to liquidations.
Recommendation
Be aware that the
maxQuantityis not respected for orders with quantity zero that close the entire existing position, or liquidations. -
I-11 Informational Liquidations Allowed For Pending Products Warning Resolved
Description
In the Liquidation flow there is now validation against the status of the product being liquidated, as a result liquidations could technically be carried out for products that are not currently live.
Recommendation
Be aware of this possibility and consider explicitly validating that a product is in the
ACTIVEstatus before carrying out the liquidation. -
I-12 Informational Missing lotSize Validation Validation Resolved
Description
In the
_verifyProductSizesfunction there is no validation that explicitly prevents thelotSizefrom being 0.This will result in unexpected and unclear panic division by 0 reverts when validating modulo
lotSize.Recommendation
Consider adding an explicit
lotSizenonzero validation before the modulo validation. -
I-13 Informational Liquidator Configuration Check Validation Resolved
Description
In the
liquidateSubaccountfunction it may be beneficial to add a liquidator configuration check that ensures that the liquidator account and subaccount are not zero values.Recommendation
Consider adding a liquidator account and subaccount address to confirm the liquidator is properly configured as a sanity check.
-
I-14 Informational finalizeRevokeLinkedSigner Ignores Expiry Unexpected Behavior Resolved
Description
The
finalizeRevokeLinkedSignerfunction does not validate that the action expiry has not passed at the time of finalization, this may be unexpected for a user if one of their pending revoke actions is executed by the owner long after their action has technically expired.Recommendation
This behavior may be acceptable, however be aware of this nuance and consider documenting it for users.
-
I-15 Informational msg.value Allowed For Local Withdrawals Validation Resolved
Description
In the local network withdrawal flow there is no usage of msg.value although it is used within a payable function flow.
Recommendation
To avoid any chance of potentially trapping Ether value by calling the complete withdrawal flow with ether value for native chain withdrawals, consider reverting if the msg.value is nonzero in the native withdrawal case in
_completeWithdrawal. -
I-16 Informational Withdrawals Stuck For Users Due To Wrong Token Warning Acknowledged
Description
In the
_completeWithdrawalfunction when the withdrawToken does not match the usdToken and the destination chain is different from the native network, the function reverts with theInvalidDepositTokenerror.This pending withdrawal cannot be cleared by either finalizeWithdrawal or rescueFunds, and therefore holds the account’s pending withdrawal balance hostage.
Recommendation
For now, only the usdToken in the balance mapping is supported, so this may be acceptable currently. However in the future consider validating that the pending withdrawal will not fail this validation upon creation to avoid trapping the withdrawal for users.
-
I-17 Informational Small Withdrawals Can Be Stuck Warning Acknowledged
Description
In the withdrawal flow it’s possible for a user to specify a withdrawal amount that is larger than the required withdrawal fee, but rounds to a cross-chain transfer amount of zero due to the decimalConversionRate flooring.
For example:
- The withdrawal fee is 1e9
- User A initiates a withdrawal USDe of 1e9 + 1 in normalized amounts
- This passes the withdrawal fee validation, the amountMinusFee is 1 wei in normalized form
- On withdraw finalization, the denormalized amount is 1e9
- The amountLd value rounds to zero due to the decimalConversionRate being 1e12
- The amount sent is ultimately 0 value and this results in a revert
Such pending withdrawals cannot be processed and will always remain stuck.
Recommendation
Be aware of this and consider validating that the remaining amount after fees is greater than or equal to the decimalConversionRate after being denormalized when a cross-chain transfer is to be executed.
-
I-18 Informational PnL Realization Is Not Restricted To Profits Warning Acknowledged
Description
In the
realizePnlfunction there is no validation that ensures that thecostDeltabeing realized as supposed profits to the account’s liquid margin balance in theaccountData.balance[subaccount][usdToken]entry are actually from position profits.The
costDeltacould be putting the position into extreme losses and allowing the balance mapping to be inflated with profits that the account has not actually received.It is up to the sequencer to carefully invoke the realizePnl function with a cost that represents the actual current market price and the position’s actual equity value.
Recommendation
Be aware of this lack of validation at the Smart Contract level and ensure that the sequencer currently and in future iterations considers the current profit of positions carefully when assigning the cost for the profit realization.
-
I-19 Informational Incorrect Error Used For Token Removal Error Resolved
Description
When total balances are non-zero,
removeTokenreverts withP_ERR_AMT_ZERO, which semantically indicates an 'amount zero' error. However the issue is that the token balance is non-zero rather than zero.Recommendation
Consider creating a
P_ERR_AMT_NON_ZEROerror or using theP_ERR_BAD_VALerror here. -
I-22 Informational Misleading emergencyUnpause Revert Error Resolved
Description
The
emergencyUnpausefunction reverts with theExchangePausederror in the case that the owner calls the function when the exchange is not paused.This error may be misleading to the caller in the event that it is surfaced, since it claims that the failure is due to the fact that the exchange is paused when in fact it is due to the fact that the exchange is not paused.
Recommendation
Consider introducing an
ExchangeUnpausederror for this case. -
I-23 Informational Intermediate Overflow Is Not Handled Best Practices Resolved
Description
In the partial reduction case of the
_settlePositionfunction, thecFillData.quantityandcOldCostare multiplied together using uint128 operations.In some rare edge case scenarios this could be at risk of overflowing the maximum
uint128value, however this is unlikely.Recommendation
Out of an abundance of caution, consider using a Math library
mulDivfunction that handles intermediate overflow for the calculation of thecReduceCostvalue. -
I-25 Informational Unnecessary Uint128 Cast Gas Optimization Resolved
Description
In the partial reduction case of the
_settleLiquidatorPositionfunction thert.quantityis casted to auint128to deduct from theproduct.openInterest. However thert.quantityis already auint128and therefore does not need to be casted as such.Recommendation
Remove the unnecessary
uint128cast. -
I-26 Informational Unnecessary _revert Return Value Best Practices Resolved
Description
The
_revertfunction specifies astring memoryreturn value, yet never returns any data and instead always reverts.Recommendation
Remove the
string memoryreturn value from the_revertfunction. -
I-27 Informational Misleading _delegateCallPerpEngine Comment Documentation Resolved
Description
The documentation for the
_delegateCallPerpEnginefunction states that itPerforms a delegate call to the ExchangeConfig with the data argument.However, it instead makes a delegate call to the
PerpEnginecontract.Recommendation
Correct the documentation for the
_delegateCallPerpEnginefunction. -
I-28 Informational Misleading ProductNotFound Revert Unexpected Behavior Resolved
Description
In the
realizeFundingfunction when an entry in theproductIdsarray does not map to an existing configured product, the function reverts with theProductNotFound(id, productId, EngineType.PERP)error.However the productId here is assigned as the resulting memory struct's
product.id. That corresponds to theproductIdsentry. This product id will always be zero and therefore does not allow the consumer of theProductNotFounderror to debug whichproductIdsentry failed the validation.Recommendation
Correct the error to revert with
ProductNotFound(id, productIds[i], EngineType.PERP). -
I-29 Informational Unnecessary Addition in Maker Signature Slice Gas Optimization Resolved
Description
In the
decodeMatchOrdersActionfunction, the decoding ofmakerSignaturecurrently uses the boundary expression:makerSignature: bytes(action[MATCH_ORDER_MAKER_SIGNATURE:MATCH_ORDER_MAKER_SIGNATURE + makerSignatureLength]),However, the value of
MATCH_ORDER_MAKER_SIGNATURE + makerSignatureLengthis already computed and stored in the variabletakerSignatureIndex. Recomputing the addition introduces unnecessary repetition and slightly reduces code clarity.Recommendation
Use the precomputed variable
takerSignatureIndexwhen decodingmakerSignatureto improve readability and consistency:makerSignature: bytes(action[MATCH_ORDER_MAKER_SIGNATURE:takerSignatureIndex]), -
I-30 Informational Settling Must Be Completed In The Correct Order Warning Acknowledged
Description
In the _settlePosition function in the order match flow as well as the realizeFunding in the funding realization flow, the funding for the account is computed and settled into the account balance mapping. The balance mapping stores values as a uint, meaning that an account’s liquid margin balance cannot go below zero.
However there can arise situations where the funding amount exceeds the liquid margin of an account, for example consider the following scenario:
- Trader A holds a 20 ETH long with an entry price of $5,000 and cost basis of $100,000
- Trader A has 1,000 USDe of liquid margin, opening their position at an initial leverage of 100x
- The price of ETH goes to $6,000, the profit of Trader A’s position is now $20,000 making their total margin value $21,000
- Trader A incurs a funding charge of $1,250, however the realizeFunding and _settlePosition functions will attempt to deduct this $1,250 from the Trader’s liquid margin balance of just 1,000 USDe, which results in an underflow panic revert
As a result, these actions cannot take place for traders in such a scenario, and result in a DoS of the processActions loop.
Similarly, the settling of PnL can also produce an underflow revert when attempting to settle it into the balances mapping if the PnL from other products and funding from other products are not settled beforehand.
Recommendation
Ensure that realizePnl and realizeFunding are invoked as needed in the correct order to allow an account’s pending PnL and pending funding amounts to fit into the account balance mapping in a positive integer and allow order matches to go through.
-
I-31 Informational Lacking Address Code Validation Validation Acknowledged
Description
In the
ExchangeGatewaycontract actions use the_delegatecallinternal function to delegatecall the relevantAddressRegistryrecord address.The
_delegatecallfunction checks the returnedsuccessboolean and reverts if a call was unsuccessful.However the
target.delegatecall(data)operation will return true always for addresses which are EOAs and do not implement any code at their address.Therefore, in the event of a misconfiguration in the
AddressRegistry, if a record is assigned to an incorrect address that happens to be an EOA, the_delegatecallfunction will not revert.Many actions in the
ExchangeGatewaycontract thatdelegatecallto a record are payable and pass along ether to the target address. Therefore in the event of a misconfiguration this Ether could be accidentally trapped.Recommendation
Consider adding a code length validation to ensure that the
targetaddress to in the_delegatecallfunction has bytecode.
Remediation Review
3 findings · October 15 to 16, 2025-
L-01 Low Overallocated Refunds Lost Acknowledged
Description
In the
CollateralManagercontract_completeWithdrawalfunction there are several ways that excess nativemsg.valueis refunded to the caller.If the withdrawal is a native chain withdrawal and does not require native funds to be sent then the entire
msg.valueis refunded. Furthermore, if an excess of msg.value is sent for the cross-chain fee, then the excess is refunded.Both of these refund cases are not accounted for in the Claimer contract and therefore will become trapped, because the
Claimercan't even sweep it's own balance due to themsg.value < totalOftFeescheck.Recommendation
Consider adding a
rescueNativefunction callable by the owner to theClaimercontract. Or consider refunding the entire Ether balance of theClaimercontract at the end of thefinalizeWithdrawalsexecution, instead of trying to keep track of the refunds. -
L-02 Low Incorrect Refund Amount in
_completeWithdrawalLogical Error AcknowledgedDescription
The refund logic in
_completeWithdrawalis intended to return any excess native tokens sent along with the transaction. These tokens cover LayerZero’s OFT messaging fees during cross-chain withdrawals. When no OFT transaction occurs, the full msg.value is correctly refunded to the caller. However, when an OFT transaction is executed, the refund amount is calculated asmsg.value - (messagingFee.nativeFee + amountLD).The issue arises because amountLD is not part of msg.value, it is provided by the collateral manager by unwrapping WUSDe to USDe. Consequently, any excess native tokens sent to finalizeWithdraw or rescueFunds are not refunded, since the calculation incorrectly subtracts amountLD from msg.value. This results in excess native tokens getting stuck in CollateralManager.Recommendation
Adjust the refund calculation to subtract only the
messagingFee.nativeFeefrom themsg.value. -
I-01 Informational Unnecessary MSTORE Operation Gas Optimization Acknowledged
Description
In the
_verifyEip1271Signaturefunction the mstore operation after the external call was determined successful sets the length of thereturnDatato thebytesToCopy. However this length has already been set in the declaration of thereturnDatabytes so this operation is redundant and has no effect.Recommendation
Consider removing the
mstore(returnData, bytesToCopy)inside the success block as it is a wholly unnecessary gas expenditure.
No findings match.
More from Ethereal
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.
