Trueo engaged Guardian to review the security of their Trueo Uni V4. From the 22nd of August to the 16th of October a team of 4 auditors reviewed the source code in scope. Note: Fixes to the Remediations V3 Findings have not been reviewed by Guardian in this report.
- Published
- Review window
- August 22 to October 16, 2025
- Rounds
- Main Review, Remediation Review, Remediation Review V2, Remediation Review V3
- Language
- Solidity
- Chains
- Base
- Sector
- Derivatives
- 0 Critical
- 8 High
- 15 Medium
- 10 Low
- 4 Informational
Scope
Overview
Trueo engaged Guardian to review the security of their Trueo Uni V4. From the 22nd of August to the 16th of October a team of 4 auditors reviewed the source code in scope. Note: Fixes to the Remediations V3 Findings have not been reviewed by Guardian in this report.
Findings 37
Main Review
15 findings-
H-01 High Deferred Orders Can Be Executed Out Of Bounds DoS Resolved
Description
If there are enough limit orders so that the number of orders exceeds
maximumExecutionCount, the execution of the orders is deferred until the admin calls theresolveDeferredExecution()function.However, when the admin calls this function, there is no check that the current tick of the pool falls within the deferred orders' range, meaning they will be executed out of range and the order owners will receive a different distribution of
token0/token1than expected.This can occur naturally or intentionally via griefing attack.
Recommendation
In the
resolveDeferredExecution()function, add a check that the current pool tick falls within the specified orders' range.Resolution
Trueo Team: Resolved.
-
H-02 High Down-Move Pushback Missing In _executeOrder Logical Error Resolved
Description
Proof of concept: PoC
The
_executeOrderfunction should push an order back into the order book when a swap crosses its boundary tick but nothing fills. This works on upward moves, but the downward path is not handled.For a down move (
fromTick > toTick),moveTickscans[toTick, fromTick + 1)and clears boundary entries in that range._executeOrderthen only checks the opposite interval[fromTick, toTick)to decide whether to push the order back, which never matches on a down move.As a result, the order is removed from the
orderBookwithout being executed or deferred. It still exists inpendingOrders(so it can be cancelled) but it will not execute.In practice a malicious swapper can “cancel” out a
zeroForOneorder by moving price down across its upper boundary, stripping it from the order book. Example: order[60, 120]. Start at tick0; swap up0→120somoveTickuses[0, 120)and leavesorders[120].Then swap down
120→60;moveTickuses[60, 121)and clearsorders[120]._executeOrderchecks the other interval for the down move, skipspushOrder, and the order is no longer in theorderBookwhile still listed inpendingOrders.Recommendation
Update
_executeOrderto handle the down-move case correctly by ensuring that orders crossing their boundary without execution are consistently pushed back into theorderBook.Resolution
Trueo Team: Resolved.
-
H-03 High Native ETH And Blacklist Tokens Can Cause DOS DoS Resolved
Description
If the Truemarket pool involves the use of either native ETH or a blacklistable token such as USDC, a single user can cause the execution of all orders to fail.
When a swap moves the tick past the orders' tick range, the
OrderManagerwill attempt to execute them.However, if a single user is on the token blacklist or places the order from a contract that reverts when it receives ETH, the execution of the entire order stack will revert.
The revert takes place in the
poolManager.take()function which attempts to send the native or blacklistable token to the user.This may occur naturally if contracts attempt to place orders but have not implemented a receive or fallback function.
Recommendation
It is recommended to wrap the call to the
take()function in a low-level call. If a revert is detected, the tokens can be stored in the contract for later retrieval.Resolution
Trueo Team: Resolved.
-
H-04 High Off-By-One Tick Can Prevent Order Execution Logical Error Resolved
Description
Proof of concept: PoC
Uniswap’s swap logic has a known quirk: when
result.sqrtPriceX96 = step.sqrtPriceNextX96at the end of a swap step and the direction iszeroForOne, the pool sets the current tick totickNext - 1. This means the pool’s current tick may differ from the tick derived fromsqrtPriceX96.In
TruthMarketHook, thetoTickvalue is obtained usingTickMath.getTickAtSqrtPrice(sqrtPriceX96)from:(uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId()); int24 toTick = TickMath.getTickAtSqrtPrice(sqrtPriceX96);If the above Uniswap quirk is triggered,
toTickmay be off by one compared to the pool’s actual tick. This value is later passed intoOrderBook.moveTick, which can cause limit orders at the affected tick to remain uncleared and unexecuted.As a result, users may miss their intended execution at the desired price, losing an opportunity to receive the tokens they expected.
Recommendation
In
TruthMarketHook, obtain the tick directly fromslot0rather than re-calculating it fromsqrtPriceX96, and account for this Uniswap tick adjustment behavior.Resolution
Trueo Team: Resolved.
-
M-01 Medium Inconsistent Minimum Amount Validation Validation Resolved
Description
The
validateMinimumAmountfunction, used increateOrder, enforces that the order amount exceeds the contract’sminimumLiteralAmount. This is done by dividingamountInby thetokenIn’s decimals and comparing it to the threshold.The issue is that a single
minimumLiteralAmountvalue (currently set to1) is applied to all tokens, regardless of their market value.As a result, the effective minimum differs drastically between tokens. For example, if
tokenInis USDC, the minimum is only slightly above one USDC, while iftokenInis ETH, the minimum becomes one whole ETH, which is significantly higher in value.This inconsistent enforcement can unintentionally restrict orders with certain tokens while being too permissive with others.
Recommendation
Consider implementing token-specific minimums, or scale the minimum by relative value, to ensure consistent and fair minimum order requirements across different tokens.
Resolution
Trueo Team: Resolved.
-
M-02 Medium Deferred Payment Hash Collision Risk Logical Error Resolved
Description
When payment is deferred, a
DeferredPaymentstruct is hashed to generate ahashId. However, thehashIdonly depends on(currency, amount, to, timestamp).This can lead to collisions — for example, if Pool A and Pool B both defer a payment of 100 USDC to the same user in the same block, the resulting
hashIdwill be identical.As a result, one payment silently overwrites the other, causing loss of funds to the affected order owner.
Recommendation
Consider incorporating
poolIdand a unique increasing nonce into thehashId. For example:bytes32 hashId = keccak256( abi.encode(payment, poolId, deferredPaymentNonce++) );Resolution
Trueo Team: Resolved.
-
M-03 Medium Griefing Via Forced Range Shift In Partial Order DoS Acknowledged
Description
Proof of concept: PoC
In
_partialFillOrder, when the recalculated tick range collapses (i.e., the new lower and upper ticks are equal), the order’s limit range is shifted upward by one tick spacing:if (newTickLower > newTickUpper) { newTickUpper = newTickLower + key.tickSpacing; }This introduces a griefing vector. An attacker can repeatedly swap to exactly one tick above the order’s lower tick (for a
zeroForOneorder).Each forced adjustment shifts the order’s range upward, effectively altering the execution level over time.
As a result, the order may eventually be fully filled at an unintended, less favorable price. The order owner could therefore receive an unexpected amount of tokens once the order completes.
Recommendation
One possible approach is to settle the order immediately when the range collapses, rather than automatically recreating it at a shifted range.
However, this introduces a different risk where orders may be settled prematurely. At minimum, this behavior and its implications should be clearly documented so that users understand the possibility of griefing when enabling partial fills.
Resolution
Trueo Team: Acknowledged.
-
M-04 Medium cancelOrder Lacks Minimum Output Protection Logical Error Resolved
Description
Users can call the
cancelOrderfunction to cancel their order, which withdraws their liquidity position from the pool. However, no user-defined minimum output is specified.As a result, if the price moves against the user before the call, the withdrawn amounts may consist of a different token mix than intended or result in a strictly lower overall value. This exposes users to unexpected execution outcomes when canceling an order.
Recommendation
Add user-specified minimum output parameters to
cancelOrder, similar to how the Uniswap v4 periphery enforces slippage protections when modifying liquidity.Resolution
Trueo Team: Resolved.
-
M-05 Medium Off-By-One Tick Can Block Valid Orders Logical Error Resolved
Description
During order creation, the tick is derived from
sqrtPriceX96usingTickMath.getTickAtSqrtPricebefore being passed intovalidateTickThreshold:int24 tick = TickMath.getTickAtSqrtPrice(sqrtPriceX96); OrderValidation.validateTickThreshold( tick, params.tickLower, params.tickUpper, tickThreshold, params.zeroForOne );Because of the Uniswap quirk where, after a
zeroForOne swap, the pool sets the tick totickNext - 1whensqrtPriceX96 = step.sqrtPriceNextX96, the derived tick may be off by one relative to the actual pool tick.This can cause valid
zeroForOneorders to be incorrectly rejected. For example:- A prior
zeroForOneswap moved the price exactly to thetick = -120boundary. Due to the quirk, the
pool’s current tick becomes -121.
- A user creates a limit order to buy t
oken0between ticks-120 and -60.This should be valid since the
order’s
fromTick (-120)is greater than the pool tick (-121).- However, since tick was derived from
sqrtPriceX96(returning -120), the validation incorrectly
blocks the order.
As a result, users may be prevented from submitting valid
zeroForOneorders in these circumstances.Recommendation
When validating ticks, rely on the pool’s current tick from slot0 rather than recalculating it from
sqrtPriceX96.Resolution
Trueo Team: Resolved.
- A prior
-
M-06 Medium unlockCallback Missing Deferred Payment Handling Logical Error Resolved
Description
The
_takeOrSettlefunction includes an edge case where, if thepoolManagerdoes not have sufficient deposits, it returnsResolveResult.InsufficientDepositswithout sending owed tokens to the user.In most places where
_takeOrSettleis called, this case is handled by passing the owed amount to_deferPayment, which creates a deferred claim for later resolution.In the
unlockCallbackfunction, however, this defer step is missing. If_takeOrSettlereturnsInsufficientDeposits, no deferred claim is created and the user’s owed amount goes unaccounted.Furthermore,
unlockCallbackcalls_clearDeltaat the end, which clears the user’s expected amount as “dust delta,” making it permanently unrecoverable.Recommendation
Modify the
unlockCallbackfunction to defer payments via_deferPaymentwhenever_takeOrSettlereturnsInsufficientDeposits, ensuring users’ owed amounts are accounted for.Resolution
Trueo Team: Resolved.
-
L-01 Low Rescue Function Clears All Deferred Payments Warning Resolved
Description
The
rescueTokenfunction in theOrderManagercontract withdraws all minted tokens from Uniswap. As a result, any outstanding deferred payments, including those from other pools, will fail whenresolveDeferredPaymentis later called.These payments would then need to be handled manually through the
rescueTokenfunction instead. Additionally, unlike the normal resolution flow, thehashIdentries in thedeferredPaymentsmapping are not deleted, leaving stale records that no longer correspond to valid claims.Recommendation
If this behaviour is intended, ensure it is clearly documented and that operators are aware deferred payments will need to be managed manually after calling
rescueToken.Resolution
Trueo Team: Resolved.
-
L-02 Low Incompatible Blacklist Checks Can Block Transfer DoS Acknowledged
Description
In the
_transferBondFromMarketand_transferRewardfunctions, the receiver is validated against a blacklist using:try IBlacklistable(address(token)).isBlacklisted(_account) returns (bool result)This works only if the token implements the
isBlacklistedinterface. However, if the token uses a different mechanism for blacklist functionality—such as WLFI’s Usd1 token, which usesfrozeninstead—the transfer would still fail.In such a case, critical settlement operations could be blocked, even though the blacklist check was not applicable.
Recommendation
Replace the current
try/catchimplementation to ensure that failures in the blacklist check or transfer do not block the system.try token.safeTransfer(_account, _amount) {} catch { token.safeTransfer(marketManager.safeBoxAddress(), _amount); }Resolution
Trueo Team: Acknowledged.
-
L-03 Low Unhandled InsufficientDeposits In cancelOrder Logical Error Resolved
Description
When
cancelOrderis called, the user’s liquidity is removed from the pool. During the callback,_takeOrSettleis responsible for transferring the owed amounts back to the user.However, unlike order execution, the cancel flow does not properly handle the case where the owed amount exceeds the balance available in the pool manager.
In execution paths, this condition is handled with
InsufficientDeposits, and a deferred payment is recorded.In the cancel flow, if this condition were to occur, the
unlockCallbackproceeds to_clearDelta, which clears all deltas and results in the user potentially losing all tokens they were owed.Although no concrete scenario was identified where the pool manager would have insufficient funds during cancellation, the current design leaves the risk of permanent loss in such a situation.
Recommendation
Update the cancel flow to handle insufficient deposits. For example:
if (_takeOrSettle(key.currency0, amount0, owner, address(this)) = ResolveResult.InsufficientDeposits) { _deferPayment(key.currency0, uint256(uint128(amount0)), owner); }Resolution
Trueo Team: Resolved.
-
L-04 Low Incorrect Handling Of Non-PartialFill Orders Logical Error Resolved
Description
At the end of
_executeOrder, orders that are not executed are pushed back into the order book. These orders are currently pushed at bothtickLowerandtickUpper.However, for orders with
enablePartialFill = false, only a single order should be reinserted at thetickThreshold.This behavior is correctly implemented in
createOrderbut not mirrored in_executeOrder, leading to inconsistent handling of non-partial-fill orders.Recommendation
Update
_executeOrderto push only a single order attickThresholdwhenenablePartialFill = false, ensuring consistent logic withcreateOrder.Resolution
Trueo Team: Resolved.
-
L-05 Low Inefficient Max Execution Handling DoS Resolved
Description
The
movePoolTickfunction defers execution whenever the number of in-range orders exceeds the contract’s configuredmaximumExecutionCount. Once deferred, the orders are later resolved in smaller batches by a backend service.However, the current implementation either defers all orders or executes all orders, rather than partially executing up to the maximum allowed. This introduces inefficiency since orders that could have been executed immediately are instead unnecessarily deferred.
Additionally, orders in the opposite direction are still included in the in-range order count even though they are not eligible for execution and will ultimately just be pushed back. This inflates the count and increases the chance of triggering deferral.
With a low
minAmountrequirement when creating orders, this mechanism can be abused by malicious users to spam the system with many minimal orders, forcing deferrals on order execution.Recommendation
Update the
movePoolTicklogic to execute orders up to themaximumExecutionCountand consider stricter minimum order requirements to prevent spam.Resolution
Trueo Team: Resolved.
Remediation Review
11 findings-
H-01 High User Can Arbitrarily Move Order Ticks To DoS DoS Resolved
Description
There is a small error when handling partial fill orders that allows an arbitrary user to cause orders with
partialFillEnabledto be unfillable. Essentially, the attacker is able to move an order's ticks up or down to extreme values, ensuring that the orders are never executed.Take the following example:
- Current tick is 0
- Alice creates a limit order with partial fills enabled for [180, 300]
- Bob swaps the tick up to 181. This initiates the partial fill logic which will create a new limit order for Alice at [240, 300].
- Bob then swaps the tick up to 241. This creates a new limit order for Alice at [300, 360].
Bob can continuously push all limit orders with partial fills enabled to extreme tick levels.
The issue occurs because the partial fill logic simply creates a new limit order at the next tick spacing above the
currentTick. If the newlowerTickis greater than or equal to the previousupperTick, then theupperTickis increased by one tick spacing.if (newTickLower > newTickUpper) { newTickUpper = newTickLower + key.tickSpacing; }Therefore, the order can be pushed an unlimited amount of ticks.
It's also important to note that the attacker would swap to 1 tick larger than the lower boundary to ensure that minimal token amounts are settled for the user.
Recommendation
When checking if the order is partially fillable, simple return false if
tickUpper - tickLower <TICK_SPACING. This will ensure that partial orders that only span 1TICK_SPACINGcan only be fulfilled fully.Resolution
Trueo Team: Resolved.
-
M-01 Medium Deferred Payments Still Target Blacklisted User Logical Error Resolved
Description
In
_handleDeltaResolveResult, if the resolve result isTakeFailed(likely caused by a blacklisted address), the payment is deferred:} else if (result = ResolveResult.TakeFailed) { // Handles blacklist scenarios to prevent DoS attacks _deferPayment(currency, amount, taker, PaymentDeferredReason.UnableToTransfer); }However, the deferred payment is still assigned to the same taker who could not receive funds due to being blacklisted. This does not resolve the underlying issue, as the funds remain stuck in the contract and may be permanently unclaimable.
Recommendation
If take fails due to blacklisting, consider deferring the payment to an admin-controlled safe address instead of the blacklisted taker. This allows funds to be recovered or manually remediated, avoiding permanent lockup.
Resolution
Trueo Team: Resolved.
-
M-02 Medium Off-By-One Tick Issue In validateAfterSwap Logical Error Resolved
Description
The Uniswap off-by-one tick issue persists in
validateAfterSwapwherecurrentTickmay be one tick higher than actual tick:(uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey.toId()); int24 currentTick = TickMath.getTickAtSqrtPrice(sqrtPriceX96);As a result, in edge scenarios,
currentTickcould have moved belowtickLowerBoundarybut the validation logic fails to catch it.Recommendation
Use the tick reported directly from
slot0rather than recomputing it fromsqrtPriceX96.Resolution
Trueo Team: Resolved.
-
M-03 Medium Deferred Orders Stranded If Tick Range Invalid Logical Error Resolved
Description
The
_resolveDeferredExecutionfunction was modified to check tick range validity before executing orders. If the tick range is not valid, the order execution is skipped entirely:if (isValid) { _executeOrders(key, deferred.orderIds, adjustedFromTick, adjustedToTick); emit IOrderManager.DeferredExecutionResolved(poolId, hashId, adjustedFromTick, adjustedToTick); } else { emit IOrderManager.DeferredExecutionSkipped(poolId, hashId, deferred.fromTick, deferred.toTick); }However, this is incorrect because the order has already been removed from the order book at this stage. While it is true that liquidity should not be pulled out in this case, the order must be reinserted back into the order book.
Failing to do so results in the affected orders becoming stranded, no longer present in the order book with the only recourse to cancel the order.
Recommendation
Initially we considered setting all conditions in
_validateAndAdjustTickRangeto be valid such that_executeOrderswill always be called to handle order re-insertion to order book.However, it may be quite tricky to correctly adjust tick range such that
_isTickInRangewill pass and ensure the order is re-inserted.Instead, we could consider in
_resolveDeferredExecution, if order is invalid, to do the re-insertion to order book there directly:if (isValid) { _executeOrders(key, deferred.orderIds, adjustedFromTick, adjustedToTick); emit IOrderManager.DeferredExecutionResolved(poolId, hashId, adjustedFromTick, adjustedToTick); } else { << re-insert order to orderBook >> emit IOrderManager.DeferredExecutionReinserted(poolId, hashId, deferred.fromTick, deferred.toTick); }Resolution
Trueo Team: Resolved.
-
M-04 Medium _executeOrder Pushback Includes Settled Orders Logical Error Resolved
Description
The
_partialFillOrderfunction calls_settleOrder, which deletes the order from thependingOrdersmapping and returnshasNewOrderas false whennewLiquidityis zero.In
_executeOrder, becausehasNewOrderis false, the early return is skipped and the function proceeds to the pushback logic. This can result in pushing back an order that no longer exists.Although the order is skipped when eventually processed, it still contributes to
totalOrders. This can artificially inflate the order count, potentially exceedingmaximumExecutionCountand causing deferred batches that would not otherwise be necessary.Recommendation
Ensure
_executeOrderdoes not include nonexistent orders in pushback logic to prevent unnecessary batch deferrals.Resolution
Trueo Team: Resolved.
-
M-05 Medium Deferred Orders Misplaced Due To Stale Tick Logical Error Resolved
Description
The
_resolveDeferredExecutionfunction resolves a batch of deferred order executions. The batch’sfromTickandtoTickare validated against the current pool state, and if still in range,_executeOrdersuses the original batch range.The issue arises because
_executeOrderspassestoTickto_partialFillOrder, which then uses it ascurrentTickto calculatenewTickLowerandnewTickUpperfor the remaining order. For regular swaps,toTickmatchescurrentTick, so this works correctly.However, for deferred batches, the batch’s
toTickmay differ from the pool’s actual current tick even when in range. As a result, thenewTickLowerandnewTickUppercalculation is based on the batch’stoTickrather than the truecurrentTick, potentially misplacing the remaining order in the orderbook.Recommendation
Adjust
_partialFillOrderto ensurenewTickLowerandnewTickUpperare always calculated using the actual current tick of the pool rather than the batch’stoTick.Resolution
Trueo Team: Resolved.
-
L-01 Low Reentrancy Best Practice For Deferred Actions Best Practices Resolved
Description
In
_resolveDeferredPayment, external calls (poolManager.burn,poolManager.take) are called before the state updates of deletingdeferredPayments[hashId]anddecreasing _deferredAmounts.While it is unlikely for the external call to re-enter the contract, it is generally best practice to update state variables before the external calls.
This issue also applies to
_resolveDeferredExecutioninExecutionDeferrer.Recommendation
Consider reordering the flow in
_resolveDeferredPaymentto:uint256 amt = p.amount; address curId = p.currency.toId(); delete deferredPayments[hashId]; _deferredAmounts[curId] -= amt; poolManager.burn(...); poolManager.take(...);and for
_resolveDeferredExecutionto:delete deferredExecutions[poolId][hashId]; _validateAndAdjustTickRange(...); _executeOrders(); ...Resolution
Trueo Team: Resolved.
-
I-01 Informational Unused Error In FeeCollector Error Resolved
Description
The
InsufficientPoolBalanceerror is never actually used anywhere in the current code—it’s only declared at line 49 ofFeeCollector.soland never thrown or referenced after that.Recommendation
Consider removing the error.
Resolution
Trueo Team: Resolved.
-
I-02 Informational Redundant Timestamp In DeferredPayment Hash Gas optimization Resolved
Description
In the
DeferredPaymentstruct, a 160-bit timestamp is stored and included in the hash:DeferredPayment memory payment = DeferredPayment(currency, amount, to, uint160(block.timestamp), currentNonce);However, it is not necessary to include timestamp in the hash, now that we have introduced a nonce for hash uniqueness.
Recommendation
Remove the timestamp field from the
DeferredPaymentstruct if it is not required for business logic or accounting. Relying on (currency, amount, to, nonce) is sufficient for uniqueness, while reducing per-entry storage costs.Resolution
Trueo Team: Resolved.
-
I-03 Informational Invalid Comment On _validateAndAdjustTickRange Documentation Resolved
Description
In the
_validateAndAdjustTickRangefunction, this comment which is mentioned twice is not accurate:// If current tick has already passed the entire range, execution is invalidIf the current has passed an entire range, the order is properly filled and execution should be valid instead of invalid.
Recommendation
Consider removing the comment altogether as each case has its own own specific comment.
Resolution
Trueo Team: Resolved.
-
I-04 Informational Line Numbers In Comments Don't Line Up Best Practices Resolved
Description
In
OrderManager.sol, there are some comments that reference functionality by line number, however these references seem to be outdated.Recommendation
Fix the line references.
Resolution
Trueo Team: Resolved.
Remediation Review V2
8 findings-
H-01 High Incorrect Price Used To Compute newLiquidity Error Resolved
Description
Proof of concept: PoC
In
_partialFillOrder, when calculating new liquidity to be added, the current implementation uses price obtained from current tick://@audit wrong way to obtain current price uint160 sqrtPriceX96 = TickMath.getSqrtPriceAtTick(currentTick); .. newLiquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, TickMath.getSqrtPriceAtTick(newTickLower), TickMath.getSqrtPriceAtTick(newTickUpper), amount0, amount1 );However, this is incorrect as actual current price can lie between ticks. As a result,
getLiquidityForAmountsmay return anewLiquiditythat requires moreamount0 /amount1than is available.This leads to an underflow during settlement, causing a revert and disrupting the entire execution process.
Recommendation
Use price obtained from
slot0instead:- uint160 sqrtPriceX96 = TickMath.getSqrtPriceAtTick(currentTick); + (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(key.toId());Resolution
Trueo Team: Resolved.
-
M-01 Medium Cancelled Orders Removed From Wrong Tick Logical Error Resolved
Description
A recent update introduced an adjustment where partial fill orders are pushed one tick spacing away from their
tickLower/tickUpperbounds:if (params.enablePartialFill) { orderBook.pushOrder( params.zeroForOne ? params.tickLower + params.poolKey.tickSpacing : params.tickLower, orderId ); orderBook.pushOrder( params.zeroForOne ? params.tickUpper : params.tickUpper - params.poolKey.tickSpacing, orderId );However,
cancelOrderstill attempts to remove orders using the originaltickLower/tickUpper, without accounting for the tick spacing offset.This mismatch leaves stale references in the order book. Cancelled orders are not properly removed, causing the order book to accumulate empty entries.
Over time, this creates unnecessary bloat and larger order ID batches that swaps must iterate through, increasing gas costs and reducing efficiency.
Recommendation
Update
cancelOrderto mirror the same tick placement logic used increateOrderfor partial fills.Resolution
Trueo Team: Resolved.
-
M-02 Medium Inconsistent Pushback Handling Logical Error Resolved
Description
The
_executeOrderfunction pushes back only a single order into the orderbook when handling partial fills, that end up with the same tick thresholds.However, this specific case is not accounted for in the
_executeOrdersfunction’s pushback logic. As a result, the system can push back two identical orders into the orderbook for the same partial order, creating unintended duplication.Recommendation
Consider aligning the pushback logic in
_executeOrderswith the behaviour in_executeOrderto ensure partial orders are handled consistently and to prevent duplicate entries in the orderbook.Resolution
Trueo Team: Resolved.
-
M-03 Medium Unaccounted Zero-Case In Tick Adjustment Logical Error Resolved
Description
The
_partialFillOrderfunction adjusts tick ranges after a partial fill.Specifically, it deducts
tickSpacingfromnewTickLowerin thezeroForOnecase whennewTickLoweris less than zero, and incrementsnewTickUpperbytickSpacingin theoneForZerocase whennewTickUpperis greater than zero.However, the scenario where
newTickLowerornewTickUpperequals exactly zero is not handled in either branch. As a result, the recalculated tick range may exclude the current tick.Recommendation
Consider handling the condition where
newTickLowerornewTickUpperis zero to ensure that the adjusted tick range always encompasses the current tick.Resolution
Trueo Team: Resolved.
-
L-01 Low Unclear Behavior & Comment In _partialFillOrder Logical Error Resolved
Description
In
_partialFillOrder, there are two early return cases: Case 1 – Tick movement too small:if (newTickLower = oldTickLower) { return (false, 0, 0); }Case 2 – Ticks hitting the max boundary:
if (newTickLower > maxUsableTick || newTickUpper > maxUsableTick) { return (false, 0, 0); }Both cases return
(false, 0, 0). As a result,_executeOrderalso returns false. However, the comment above is misleading:// if both ticks are 0, it means order will move out of range // so no new order is created if (hasNewOrder & newTickLower = 0 newTickUpper = 0) { return false; }In practice, when false is returned,
executeOrderssetsexecuted = false, which then triggers the pushback logic to create a new order.This creates ambiguity:
- Case 1 (movement too small): unclear whether a new order should be pushed back.
- Case 2 (ticks at min/max): seems clear that no new order should be created.
Recommendation
Re-examine the logic in
_executeOrderto ensure that Cases 1 and 2 are handled correctly. Update the comments to correctly describe the behavior—particularly whether false should lead to a new order being created or not in each scenario.Resolution
Trueo Team: Resolved.
-
L-02 Low Potential Order ID Exhaustion In getNextOrderId Warning Acknowledged
Description
The
getNextOrderIdfunction incrementsnextOrderIdas auint32with unchecked arithmetic and skips over ID0. WhennextOrderIdreachestype(uint32).max(4,294,967,295), it wraps around to0, skips it, and reuses ID1.If an existing order with ID
1is still active, the subsequentcreateOrdercall will revert due to thependingOrders[poolId][orderId].owner = address(0)check, blocking all future order creation.This behavior can cause collisions once the ID space is exhausted, especially in high-throughput pools where large numbers of orders are created and cancelled.
Recommendation
Consider expanding the ID space (e.g., to
uint64oruint128) to make exhaustion practically unreachable. implement a recycling mechanism to safely reuse old order IDs once cancelled, ensuring no collisions with active orders.Resolution
Trueo Team: Acknowledged.
-
L-03 Low _takeOrSettleAll Omits Settle After Payment Error Resolved
Description
_takeOrSettleAllhandles positive and negative delta paths. In the negative-delta branch (where the contract owes tokens to the pool) the code calls_pay(...)but does not subsequently callpoolManager.settle(...).Omitting this
settleleaves the pool’s accounting unresolved and subsequently causes reverts (for example, in Uniswap V4’s unlock callback) because the pool expects a settle step after payments.Currently this bug is not triggered by
OrderManager:_executeOrdersbecause that flow only produces positive deltas for fee transfers.However, if
_takeOrSettleAllis ever reused for contexts that produce negative deltas, this omission can leave pool state inconsistent and halt operations that rely on a proper settle (order execution, payment resolution, callbacks).Recommendation
Call
poolManager.settleafter_payto resolve outstanding delta.Resolution
Trueo Team: Resolved.
-
L-04 Low Unsafe Casting Of Negative Amounts Warning Resolved
Description
In both
_safeTakeOrSettleand_safeTakeOrSettleAll, whenamountis negative (int256 < 0), it is cast directly touint256before being passed to_handleDeltaResolveResult. This causes underflow wrapping — e.g.,uint256(-1)becomes2*256 - 1.While this wrapped value is currently harmless (since
_handleDeltaResolveResultdoes not defer or act on negative deltas), it introduces latent risk:- Future logic changes (e.g. deferred settlements, batching, or accounting extensions) might treat
the wrapped value as a legitimate positive amount.
- This could lead to excessive token transfers, accounting corruption, or DoS if the system attempts
to handle
uint256.maxsized values.Recommendation
Add an explicit check before casting to
uint256to prevent unintended wrapping:if (amount < 0) { return; } else { _handleDeltaResolveResult(result, uint256(amount), currency, taker); }Resolution
Trueo Team: Resolved.
Remediation Review V3
3 findings-
H-01 High Tick Misordering In _partialFillOrder DoS Resolved
Description
Proof of concept: PoC
The
_executeOrderfunction determines whether a partial order can be fulfilled by calling_isTickInRange, which checks if the tick movement crossed thefulfillThreshold.When resolving a deferred execution, however, the function uses the batch’s toTick even if the current tick has already moved beyond it.
As a result, a partial order that should be fully filled is incorrectly treated as still partial, and execution proceeds to the partial fill logic.
In this path, the function recalculates the new tick range adjusting
newTickUpperbased on the current tick while keeping the previousnewTickLowerfor theoneForZerocase.If the current tick has already passed the threshold, this results in misordered tick bounds (
tickLower> tickUpper), causing the system to incorrectly attempt re-adding liquidity for an already-filled order which will also ultimately fail on the Uniswap side due to invalid tick ordering.Recommendation
Modify
_executeOrderto compare the livecurrentTickagainst the order’sfulfillThreshold. If the current tick is already beyond the threshold, treat the order as fully filled rather than performing a partial re-add.Resolution
Trueo Team: Resolved.
-
H-02 High Missing Tick Boundary Checks In partialFillOrder Logical Error Resolved
Description
Proof of concept: PoC
In previous versions,
_partialFillOrderincluded boundary checks to prevent re-adding liquidity whennewTickLower = newTickUpperor when the new tick range exceeded usable bounds. These checks were recently removed:if (zeroForOne) { if (newTickLower > newTickUpper) { newTickUpper = newTickLower + key.tickSpacing; } if (newTickLower > maxUsableTick || newTickUpper > maxUsableTick) { return (false, 0, 0); } } else { if (newTickLower > newTickUpper) { newTickLower = newTickUpper - key.tickSpacing; } if (newTickLower < minUsableTick || newTickUpper < minUsableTick) { return (false, 0, 0); } }These checks ensured that new liquidity ranges were valid and non-overlapping. Without them, if the
currentTickis close to an order’s tick boundaries,newTickLowercan equalnewTickUpper, causinggetLiquidityForAmountsto revert when attempting to compute liquidity for a zero-width range.Recommendation
Reinstate the previous validation logic to ensure safety at tick boundaries.
Resolution
Trueo Team: Resolved.
-
M-01 Medium Duplicate Pushback On Deferred Partial Orders Logical Error Resolved
Description
The
resolveDeferredExecutionfunction processes deferred orders in smaller batches when they could not be executed in the_afterSwaphook due to exceedingmaximumExecutionCount.If an order is no longer in range, it is pushed back to the orderbook. For partial fill orders, both ticks are pushed back if they are in range and
thresholdLower = thresholdUpper.An edge case occurs when a partial order has its
tickLowerandtickUpperdeferred within the same range.If, at resolution time, the current tick is out of range, both sides push back the
tickLowerand thetickUpperresulting in the same order being pushed back twice.Recommendation
Consider adding a condition to prevent duplicate pushbacks when both deferred ticks originate from the same order.
Resolution
Trueo Team: Resolved.
No findings match.
Invariants 17
The review's fuzzing suite asserted 17 invariants. 15 held and 2 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
CREATE-01 | After creating an order, the pending orders count should increase by 1 | Held |
CREATE-02 | After creating an order, the order should have positive liquidity | Held |
CREATE-03 | After creating an order, the user's balance should decrease | Held |
ORDER-01 | Expected order count should match total active orders | Held |
ORDER-02 | A limit order zeroForOne should never be set at a tick below or equal to current tick and | Broken |
ORDER-03 | vice versa pendingOrder liquidity should always match with Uniswap position | Held |
ORDER-04 | Order owner's balance of token0/1 (or ERC6906 claims) should always increase | Held |
ORDER-05 | after an order is deleted All pending orders should have liquidity > 0 | Held |
ORDER-06 | Tick range should be valid for all orders | Held |
ORDER-07 | OrderBook active orders should account for partial fills and deferred orders | Broken |
ORDER-08 | Cancelled orders should be completely removed from pending tracking | Held |
ORDER-09 | Deferred execution orders should still exist in pending orders | Held |
ORDER-10 | Highest and lowest active ticks should always be within valid tick range | Held |
DEFERRED-01 | Sum of deferredPayments.amount should be less than or equal to OrderManager's | Held |
DEFERRED-02 | ERC6909 claims balance If deferred nonce increased, then OrderManager's balance should increase | Held |
FEE-01 | After a swap, if fees are enabled accumulated fees should increase in | Held |
FEE-02 | FeeCollector After withdrawPoolFees, fee recipients should have increased balances | Held |
More from Trueo
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.
