Guardian's review of Protocol Review for FlowRouter, published September 2026. The report records 28 findings, including 10 low and 18 informational.
- Published
- Review window
- September 3 to 21, 2026
- Language
- Solidity
- Chains
- Ethereum, Arbitrum, Optimism, Base, Polygon, BNB Chain
- Sector
- DEXs and AMMs, Infrastructure
- 0 Critical
- 0 High
- 0 Medium
- 10 Low
- 18 Informational
Scope
6 files in scope · 1,973 nSLOC
| File | nSLOC | Lines |
|---|---|---|
contracts/FlowRouter.sol | 218 | 321 |
contracts/libraries/EkuboV3Operation.sol | 41 | 49 |
contracts/libraries/TokenFlowProgram.sol | 65 | 82 |
contracts/OnlyApproved.sol | 20 | 27 |
contracts/TokenFlowExecutorV2.sol | 1606 | 1879 |
contracts/TokenHelper.sol | 23 | 39 |
Findings 28
-
L-01 Low First upper half referral code rejects fees Logical Error Resolved
Description
The referral namespace reserves the lower half of
uint32values for informational codes and allows upper half codes to apply a fixed fee.REFERRAL_WITH_FEE_THRESHOLDequals1 << 31, so0x80000000is the first value with its high bit set. However,registerReferralCode()checks_referralCode <= REFERRAL_WITH_FEE_THRESHOLD, which rejects every nonzero fee for that first upper half code._swap()repeats the misclassification because it reads and applies a referral fee only whenreferralCode > REFERRAL_WITH_FEE_THRESHOLD.An owner or referral allocator that assigns
0x80000000as the first fee bearing code cannot configure the intended fee for it. Registration reverts withInvalidFeeForCode, so the integration either fails or must skip an otherwise valid identifier. If the code is registered with a zero fee, swaps using it also omit the fixed fee and prevent the intended protocol and referrer fee collection.Recommendation
Treat
0x80000000consistently as part of the upper half. Change the registration comparison from<=to<and the settlement comparison from>to>=. -
L-02 Low Zero amount bypasses spender revocation Access Control Resolved
Description
The owner can revoke a persistent token and spender pair by clearing its allowance and setting
persistentApprovalState[token][spender]to the revoked state. The documented lifecycle requires that state to remain closed until the owner explicitly resets it._ensurePinnedAllowance()instead returns immediately when the resolved operation amount is zero and reads the revocation state only afterward. Every fixed spender handler that resolves to zero can therefore continue into its external call despite the owner's revocation.This is externally reachable through the permissionless FlowRouter. For example, an attacker can declare a zero input Bebop operation for a revoked token and supply
invalidateNonce(uint256)as the opaque payload. The executor skips the revoked state check. The live pinned Bebop router records the chosen nonce against the executor because it sees the executor asmsg.sender. An unrelated one wei native currency operation that wraps to WETH supplies the final output. This allows the route and nonce mutation to commit whilepersistentApprovalStatestill reports the pair as revoked.The attacker can repeat the route for Bebop nonces used by executor quotes. Those quotes then revert even after the owner has invoked the documented administrative control. No token allowance is restored and this proof does not show protected fund loss. The impact is limited to defeating an explicit administrative revocation guarantee and enabling targeted quote denial of service.
Recommendation
Read
persistentApprovalState[token][spender]and reject the revoked state before returning for a zero amount. Keep the zero amount return only after the revocation check so fixed and dynamically admitted spenders enforce the same closed lifecycle. -
L-03 Low Native V2 multi hop quotes always revert Compatibility Resolved
Description
The Native V2 operation is intended to execute an exact input RFQT quote through the pinned Native router. The executor accepts
quote.multiHop, approves the router for the declared input and then requires the executor's input balance to decrease aftertradeRFQT()returns.The exact source that matches the deployed Native router uses a different input source for this quote mode. When
multiHopis false, the router callstransferFrom()againstmsg.sender, which consumes the executor's approved balance. WhenmultiHopis true, it callstransfer()from the Native router's own balance because that mode continues a sequence inside the Native router's multicall.Consequently, the two accounting models cannot compose. A Native router without enough prefunded seller tokens reverts during its transfer. Even if the router is fully prefunded and sends the signed buyer output to the executor, the executor's seller token balance remains unchanged.
_finishPulledInput()then reverts withInputNotConsumedand rolls back the complete swap. Every signed Native V2 quote withmultiHop = trueis therefore unusable through operation200, which wastes the submitter's transaction cost and makes the quoted route unavailable.Recommendation
Reject every Native V2 quote whose
multiHopfield is true before granting an allowance or calling the Native router. Restore a dedicatedNativeV2MultiHopUnsupportederror because the executor cannot supply Native router inventory within the venue's own multicall. Express multi step swaps as separate executor operations instead. -
L-04 Low V4 swaps cannot supply required hook data Compatibility Resolved
Description
Uniswap V4 passes caller-supplied
hookDatato a pool's hooks during each swap. Hooks can use this data to authenticate a trader, verify an offchain authorization, enforce a whitelist, or receive other pool-specific input. The local PoolManager interface exposes this argument, and the route schemas let the compiler select the complete pool key, including its hook address.Neither V4 route schema can carry that payload.
UniswapV4PoolDatahas nohookDatafield, andUniswapV4BatchHophas no per-hop equivalent. The derived single and batch callback jobs also omit it._uniswapV4RunSingle()and_uniswapV4RunBatch()therefore pass a literal empty byte string toPoolManager.swap()for every pool and direction.Consequently, any otherwise valid swap that requires nonempty hook data is impossible through the executor.
Recommendation
Add versioned V4 single and batch operation encodings that include
bytes hookDatafor a single swap and onebytes hookDatavalue per batch hop. Propagate each value through the callback job and pass it unchanged toPoolManager.swap().Keep the current opcodes as explicitly empty-hook-data variants so existing version 2 route bytes retain their ABI meaning. A new opcode or route version is safer than appending dynamic fields to the existing structs because old encoders and decoders must continue to agree on the tuple layout. Preserve the current PoolManager pin, callback authentication, delta checks, debt settlement, balance floors, and output minima for the new variants.
-
L-05 Low Legacy Ekubo router rejects Ve33 pools Compatibility Resolved
Description
Ekubo operation
210accepts a complete pool configuration and builds an exact input call for the selected pool. The configuration includes the pool extension address, so the operation can represent the live Ve33 pool format.The executor always sends this call to
0xd26f20001a72a18C002b00e6710000d68700ce00. Ekubo's deployment reference identifies this address as the legacyv3.1.1MEV Capture router that was superseded by the configurable router. The officialv3.1.1_swap()implementation forwards only when the pool extension equals the router's configuredMEV_CAPTUREaddress. Every other extension is sent directly toCore.swap().Ve33 is a forward only extension. Its official
v3.2.0beforeSwap()implementation rejects every direct Core swap withSwapMustHappenThroughForward(). Consequently, every operation210route for a Ve33 pool reaches the legacy router's direct branch and reverts even thoughEkuboV3Operationcan encode that pool key.This affects a live pool on the target chain. At Robinhood block
54,617,722, Ekubo's current forwarded router quotes1 STONXfor763,332raw USDG units through the initialized STONX/USDG Ve33 pool. The legacy router returnsSwapMustHappenThroughForward()for the same tokens, amount and configuration. A freshTokenFlowExecutorV2reaches the same error throughexecutePath(). The transaction rolls back, so the user loses transaction cost and cannot use Ve33 liquidity through the Ekubo operation.Recommendation
Deploy Ekubo's
v3.2.0configurableRouterfor Robinhood with the existing Core, MEV Capture and Ve33 addresses then pin the next executor deployment to that router. The configurable router preserves the seven argumentswap()ABI used byEkuboV3Operation, routes ordinary pools through Core and forwards pools whose extension matches itsMEV_CAPTUREorVE33immutable. Check the deployedMEV_CAPTURE()andVE33()values during deployment. Preserve the current allowance, native receive, input budget, output minimum and residue controls when changing the pinned address. -
L-06 Low Pinned Pons factory rejects current curves Compatibility Resolved
Description
Pons operation
230executes an exact input trade against a bonding curve after proving that its token and quote asset match a Pons launch record. This proof is meant to prevent a caller from granting approval to an unrelated curve.PONS_V2_LAUNCH_FACTORYfixes the proof to0x7E1EAbd52Ae29598e6483F72dCf1a70b14284dB8._validatePonsV2Route()always queries that address even though each Pons curve exposes the immutable factory that created it. The curve interface omitsfactory(), so validation has no way to select among trusted Pons deployments.Pons replaced the V2 stack before the reviewed executor was introduced. The current integration documentation identifies
0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7eas the live factory. It also states that a replacement creates a whole new stack while tokens from earlier stacks continue to trade, so an integration must resolve the stack that each token belongs to. The official repository changed the factory listing on August 11, 2026. FlowRouter's initial audit package added operation230on August 12 with the predecessor address.At Robinhood block
55,030,674, native curve0xE9103855888D6d574E69274694FA20e4562dFEf9reports the current factory and token0x1F3699741c9C9EE9069ccd97f754FF6A2e94f348. The current factory returns the matching existing launch record while the pinned predecessor returns an empty record. A direct0.001 ETHbuy succeeds and returns509,833,553,483,831,890,982,152token units. The identical executor operation reverts withInvalidPonsV2Curvebefore it calls the curve.Every curve launched by the current Pons factory is therefore unavailable through operation
230. A user whose route selects current Pons liquidity loses the transaction cost and cannot complete the quoted swap through FlowRouter.Recommendation
Add
factory()toIPonsV2BondingCurveand deploy the next executor with an explicit immutable allowlist of the predecessor and current Pons factories. During route validation, read the curve's factory, require that it belongs to this allowlist and querygetLaunchedToken()on that factory. Continue matching the record's token, curve and pair token before granting approval or admitting the route. Do not accept an unrestricted factory address from route bytes. This preserves operation230and its existing encoding while supporting both Pons generations. -
L-07 Low V4 partial fills trigger delta mismatches Compatibility Resolved
Description
Uniswap V4 exact-input swaps can stop at
sqrtPriceLimitX96before consuming the complete requested amount. The pinned v4-core swap loop exits when either the amount is exhausted or the price limit is reached, then returns onlyamountSpecified - amountSpecifiedRemainingas the caller's input delta. This can happen when a request exceeds the input that available pool liquidity can consume before the executor's hardcoded extreme price limit.Both V4 execution branches reject that valid result.
_uniswapV4RunSingle()requiresinDeltato equal the full negativejob.amountIn, while_uniswapV4RunBatch()requires each input delta to equal its full negative hop allocation and later requires any negative declared-input net to equal the complete operation allocation. The executor therefore revertsUniswapV4DeltaMismatchbefore settling a smaller nonzero debt, even when the partial output satisfies the applicable minima. A failure at any batch hop reverts the complete route.A local test against v4-core commit
46c6834698c48bc4a463a86d8420f4eb1d7f3b75, matched to the pinned PoolManager, requests1e18input units against ordinary narrow-range liquidity. PoolManager consumes6,035,841,794,200,769input units, produces5,981,737,760,509,662output units and completes unlock after the actual debt is settled. Separate executor tests model a 60-input/50-output fill against a 100-input allocation and a 49-output minimum. Both current branches revert, while a strict accounting control settles 60, takes 50 and retains 40 unused input.Users cannot execute these otherwise valid V4 partial fills through FlowRouter and incur failed-transaction costs. This contradicts README's operation-accounting rule that a venue may consume less than its allocation.
Recommendation
Treat each requested V4 exact-input amount as an upper bound. Require each returned input delta to represent nonzero consumption no greater than its resolved allocation, retain the existing positive-output and minimum checks, and settle the actual negative debt rather than the requested amount.
For batches, retain currency netting, reject negative net debt in any currency other than the declared settlement currency, and require any negative declared-input net to be no greater than
job.amountIn. Settle that actual net debt. Preserve the documented treatment of zero-net or cyclic batches; imposing a new requirement that the final declared-input net must always be negative is a separate policy change, not necessary to fix rejection of nonzero partial fills.Keep existing balance floors and residue handling. Unused referenced non-final input goes back to FlowRouter under that handling; this change does not introduce a wallet refund guarantee. Add regressions for single swaps and batches that consume less than their allocation while meeting output minima, plus overconsumption, insufficient output, unexpected-currency debt and existing cyclic-batch behavior.
-
L-08 Low Terminal routes strand split final output Logical Error Acknowledged
Description
The finalizer for a terminal operation that can deliver output directly to
FlowRouteris intended to measure the final token at both contracts._buildFinalOutputContext()instead setsskipExecutorAccountingfor every single operation route withexternalTake._completeFinalOutput()then leavesproducedOnExecutorat zero. It checksminFinalOutputagainst only the router balance increase and never transfers newly received final tokens from the executor.A Uniswap V4 hook receives the executor as the swap
senderand can transfer ordinary tokens independently of the credit returned throughPoolManager. If a hook supplies 99 units throughPoolManagerand sends one additional unit of the same final token to the executor, a 99 unit program minimum succeeds while the additional unit remains in the executor. A 100 unit program minimum reverts even though the operation produced 100 final token units across the two documented destinations.This contradicts the documented terminal output behavior. It causes coherent routes whose quote includes both destinations to revert or converts part of their final output into a standing executor balance with no direct recovery function. The issue affects a single terminal operation listed by
_operationCanTakeFinalOutputExternally()when its venue or hook pays part of the declared final token to the executor. OuterFlowRouter.outputMinremains enforced against the amount that reached the router, so this is a limited accounting and integration failure rather than a protected funds loss.Recommendation
Always snapshot the executor's final token balance in
_buildFinalOutputContext(), including for single operation routes that use an external take. Sum the positive executor and router balance increases in_completeFinalOutput()and transfer the executor increase toFlowRouter. RemoveskipExecutorAccountingso callbacks and venues cannot create untracked final output at the executor. -
L-09 Low Native RFQ operations cannot consume variable AMM output Compatibility Resolved
Description
Native supports adjusting a signed quote’s input within bounded limits to consume the actual output of a preceding AMM swap. Its pool calculates the corresponding output proportionally.
The executor requires exact-input mode, checks that the resolved amount equals
quote.sellerTokenAmount, and always passes zero as the actual-input override. Consequently, a quote signed for 100 tokens cannot consume 98 tokens returned by a preceding AMM. Allocating 100 fails the available-balance check, allocating 98 fails the quote-amount check, and full-balance mode is prohibited.The route therefore reverts even when Native could execute the adjusted amount and satisfy all output minima. An included transaction costs the submitter gas and leaves the swap unexecuted. This concerns ordinary Native quotes with
multiHop = falsefollowing separate executor operations.Recommendation
Add a versioned Native operation that resolves the available input and forwards it as
actualSellerAmount, preserving Native’s deviation limits and quote validation, the executor’s input accounting, and all output minima. -
L-10 Low Self-settled V4 hook output bypasses fixed referral and protocol fees accounting / hook integration Acknowledged
Description
The V4 runners accept caller-selected hooks with return-delta permissions and use only PoolManager.swap's net caller BalanceDelta for terminal output. In afterSwap, a hook can call PoolManager.take(outputToken, recipient, X), creating a -X transient hook delta, then return +X as its unspecified hook delta. PoolManager credits that return to the hook, exactly cancelling the debt while subtracting X from the executor-visible delta. Unlock therefore finishes cleanly, but FlowRouter snapshots and charges referral/surplus economics only on the reduced output. The hook can activate conditionally when sender is the production executor, while a trusted gateway's precommitted route and terms remain unchanged.
Impact: An untrusted hook operator can direct nearly all V4 output to a chosen recipient and leave only the configured minimum visible to FlowRouter. This bypasses the fixed referral fee and protocol-retained fee share (and outputQuote surplus capture) on otherwise unchanged trusted-gateway swaps. The live PoC shows honest execution generating >1.5 tokens for the beneficiary and >0.3 protocol fee, while attacked execution of the same committed route yields only 0.016 and 0.004 respectively, with no unsettled PoolManager delta or calldata modification.
Recommendation
Reject V4 PoolKeys whose hook address advertises BEFORE_SWAP_RETURNS_DELTA or AFTER_SWAP_RETURNS_DELTA in both single and batch paths, or admit such hooks only through a governance-reviewed allowlist with direct-payment behavior incorporated into referral/quote accounting. Ensure terminal output cannot bypass FlowRouter measurement.
-
I-01 Informational Opaque Bebop calls expose executor authority Validation Resolved
Description
Bebop operation
250is intended to perform an exact input swap through the pinned Bebop router._swapBebopRfqExactInputdoes not requireoperation.datato selectIBebopRouter.swapand does not decode the payload. Instead, it measures only the balances of the declaredtokenInandtokenOutbefore and after forwarding the payload throughBEBOP_ROUTER.call(operation.data).Any caller can submit a route through FlowRouter that reaches operation
250. Because the executor uses a low level call, the Bebop router treats the executor as the caller even when the payload selects another function. A payload can callinvalidateNoncewhile the operation declares zero input. The declared input token remains unchanged, so the executor records zero spent and accepts the result. The caller can add a second operation that wraps one wei of native currency into WETH to satisfy FlowRouter's required nonzero final output. The transaction then completes and Bebop records the selected nonce as invalid for the executor, preventing a later RFQ order with the same nonce from executing.The missing binding also affects token accounting. A normal Bebop operation gives the router a persistent maximum allowance. A later operation can declare zero units of an unrelated token while its Bebop payload pulls a previously approved token from the executor. Since the executor measures the unrelated declared token, it sees no balance change and accepts the expected zero spend. If the payload returns the declared output token, FlowRouter settles it to the caller. With a compatible signed order, the executor's standing balance can therefore be consumed without naming that token as the operation input.
These effects do not meet the README's protected funds eligibility rules. The README protects user wallet funds beyond authorized input, successful settlement of at least
outputMinand FlowRouter balances. It excludes executor balance loss, preexisting executor inventory, persistent maximum approvals and denial of service without protected fund loss. This behavior affects executor inventory and executor owned Bebop state. The README also documents opaque Bebop forwarding as intended and assigns quote shape and token binding to the provider, compiler and gateway. The issue was therefore considered informational.Recommendation
No code change is necessary if opaque Bebop forwarding and its effects on executor inventory and executor owned state match the intended trust model. If operation
250should only perform an exact input swap, requireIBebopRouter.swap.selector, decode the payload and bind the signed order's input token, output token, token owner, receiver and exact amount to the resolved operation. Reject a zero resolved amount. Give any other supported Bebop method its own opcode and method specific validation. -
I-02 Informational Renunciation erases explicit sweeper authority Access Control Acknowledged
Description
FlowRoutergives the owner implicit sweep authority while explicit sweeper grants are meant to remain active across ownership changes and renunciation until the owner removes them. Both sources of authority use the same boolean value inOnlyApproved.approved, so the contract cannot preserve them independently.If an address already has an explicit sweeper grant when it becomes the owner,
_addApprovedAddress()only writestrueover the existing value. When that address later renounces ownership,_removeApprovedAddress()changes the shared value tofalse. This removes both the owner's implicit authority and the earlier explicit grant even though no one calledremoveApprovedAddress()for that grant.A normal role transition reaches the failure: the current owner explicitly approves an independent sweeper, transfers ownership to that sweeper and the new owner later renounces. The contract clears the sweeper's approval and leaves
owner()equal to the zero address. If no other sweeper remains, no account can withdraw existing or future ERC-20 and native balances from the router. No owner remains to restore the intended explicit grant, so those funds are permanently inaccessible.Recommendation
Store explicit sweeper grants separately from the owner's implicit authority. Make the sweep authorization check accept either the current owner or an explicitly approved sweeper and stop adding or removing owners in the explicit approval mapping during ownership transitions. This preserves a prior explicit grant when its holder becomes owner and later renounces while still revoking only the departing owner's implicit authority.
-
I-03 Informational Revocation permits referral code reassignment Business logic errors Acknowledged
Description
The referral registry is intended to prevent a code from being assigned more than once.
registerReferralCode()rejects a code whoseregisteredfield is true and its adjacent comment states that referral codes must not be overwritten. However,revokeReferralCode()deletes the completereferralLookupentry. This clears the sameregisteredfield that guards registration, so the revoked identifier immediately passes the registration check again with an unrelated beneficiary and fee.This makes revocation change the identity of a code instead of only disabling its benefits. For example, the owner can revoke a one percent upper half code and later assign the same identifier to a new beneficiary with the maximum two percent fixed fee. Integrations and pending swaps that retain the old identifier then use the new terms. A swap whose
outputMinstill passes completes after the unexpected additional fee deduction and sends the beneficiary share to the replacement account.Recommendation
Track whether each referral code has ever been assigned separately from whether its benefits are active. Set the permanent assignment marker during registration and have
registerReferralCode()reject every previously assigned identifier. Revocation can then clear the active fee and surplus configuration without making the identifier available for reuse. -
I-04 Informational Nested swaps can reclaim retained surplus Reentrancy Resolved
Description
FlowRouteris intended to cap receiver settlement atoutputQuoteand retain its share of both positive execution surplus and fixed referral fees. The_swap()function snapshots the router output balance before it calls the executor then treats the entire balance increase after that call as output of the current swap. The executor can reenter either public swap entry point before the outer balance delta is calculated.An unprivileged caller can start an outer swap whose executor uses the received input to perform a nested swap through the same router. If the nested execution returns 100 output tokens with an inner quote of 90, the inner frame pays 90 to its receiver and records the remaining 10 as the protocol share. When the nested call returns, the outer frame sees those 10 tokens as an increase over its earlier snapshot. It therefore treats the retained inner surplus as outer output and transfers it to the receiver controlled by the caller.
The same defect reaches fixed fee revenue even when positive slippage is zero. With 100 gross output, a one percent referral fee and an inner quote of 99, the inner frame pays 0.8 to the beneficiary and 99 to the receiver, leaving the router's 0.2 protocol fee share. The outer frame then transfers that 0.2 to the caller. A caller can therefore wrap a swap and remove either form of router revenue created during the transaction without sweeper authority.
Recommendation
Apply one transient reentrancy lock to both public swap paths and keep it active through fee, surplus and receiver settlement. This ensures that each measured output balance delta belongs to only one active swap.
-
I-05 Informational Native referral beneficiaries can veto swaps DoS Acknowledged
Description
FlowRoutersynchronously pushes referral proceeds to the configured beneficiary before it pays the swap receiver. For native output,TokenHelper.universalTransfer()performs an unrestricted call to the beneficiary and reverts the entire transaction if that call fails. This gives a beneficiary control over whether every native output swap using its referral code can settle, even though the beneficiary is otherwise only a payment recipient.The dependency affects both referral payment mechanisms. A fixed fee code calls its beneficiary during
_swap(), while a zero fee code with a positive surplus configuration calls its beneficiary during_settlePositiveSurplus(). A contract beneficiary that does not accept native value therefore disables the affected native routes. A beneficiary with toggleable receive logic can selectively veto them. Because the revert unwinds the complete transaction, the user's input transfer, venue execution and all settlement are rolled back.The fixed fee path also calls the beneficiary when the computed beneficiary payment rounds to zero. With two wei of gross output and a one percent fee, the beneficiary share is zero while the user's output after fees is one wei and satisfies a minimum of one wei. A rejecting callback with zero value still reverts that otherwise valid swap.
Recommendation
Use a force safe native transfer for referral beneficiary payouts. A helper such as
SafeTransferLib.forceSafeTransferETH()first attempts delivery with bounded gas and falls back to forced native value delivery when the beneficiary rejects the call. Continue usingSafeERC20for token payouts. Apply the force safe path only to referral beneficiary payments unless the receiver settlement semantics are also intended to guarantee forced delivery.Skip the fixed fee beneficiary call when its computed amount is zero. This avoids invoking beneficiary code when no payment is due.
-
I-06 Informational Transient output custody retains token rewards Rewards Acknowledged
Description
FlowRoutersnapshots only the finaloutputTokenbalance before calling the executor. It later measures that token's increase and transfers the settled amount tooutputReceiver. For supported terminal operations, the executor can direct output toFlowRouterbefore the outer settlement completes.Some output tokens execute external reward accounting while transferring. A deployed dividend token observes
FlowRouteras the temporary output holder and pays a reward in a different token beforeFlowRouterforwards the complete final output. Because_swap()accounts only foroutputToken, this reward remains in the router and becomes withdrawable by an approved sweeper.Source pool ID
0xbbbcfb87db0b05ba937c2e386a56824c7f6ff8c14974d5bc14906777d9c408e2converts native currency into bridge and reward token0xfB5b5778d45AE47F15323fb59B666c655174A79C. Target pool ID0x6d01881cc8c66cf6c301c5b74f4ced290e79d0c06cab026481e4163a455d6777converts that token into output token0x7B237dc3e88DDc4B7BF71c40c5d767708EE24Ef0through hook0xeE3de28732F24d15fB188139AE1471B2B6A940cC.The route consumes
53,081,966,819wei and delivers1,152,255,084,428,067,091,171output units to the designated receiver. During settlement, the dividend mechanism pays3,386,327,335bridge-token units toFlowRouter. The input bound, output minimum, and received output remain correct.This treatment is consistent with the documented policy that incidental residue is protocol inventory. The Informational impact is that integrators may otherwise assume rewards generated by a user's route follow its output, while these side rewards instead accrue to protocol custody.
Recommendation
Document that rewards in assets other than
outputTokenwhich are paid toFlowRouterduring execution belong to the protocol. Route pricing and user interfaces should exclude these side rewards from user proceeds, while protocol accounting should identify them separately from ordinary positive surplus. -
I-07 Informational Metric OMM swaps cannot supply extension data Compatibility Acknowledged
Description
Metric OMM's six argument
swap()separates callback data from extension data. The pool forwards the sixth argument to its configuredbeforeSwapandafterSwapextensions and Metric's reference router defines it as opaque data forwarded to the pool. Its multiple hop form carries one extension payload per pool. Metric pools may be created with up to seven arbitrary nonzero extension addresses. The factory validates their uniqueness and call order but does not restrict them to implementations that accept empty data.The executor cannot represent this protocol input.
MetricOmmPoolDataincludes only the pool, direction and price limit._swapMetricOmmPoolExactInput()reconstructs the pool call with literal empty byte strings for both callback data and extension data. Appending bytes toOperation.datadoes not help because the executor decodes only the fixed tuple and still supplies""as the sixth call argument.An otherwise valid Metric pool whose swap extension authenticates an opaque payload therefore cannot execute through operation
32, even when the route compiler possesses the correct payload. A complete stack proof appends a valid authorization payload to the operation data. A direct Metric compatible call forwards that payload and swaps successfully while the sameFlowRouterroute reaches the pool with the empty data hash and reverts withRequiredExtensionData. The complete transaction rolls back, so the user loses transaction cost and cannot use the affected pool through this executor.We did not identify a currently deployed Robinhood pool that requires nonempty extension data. The four published Metric extensions reviewed during validation do not read this argument. The proof therefore establishes that operation
32is incompatible with valid Metric pools that use a custom or future extension which consumes this data. It does not establish that any currently deployed pool is unusable.Recommendation
Add a new Metric OMM opcode or route version whose pool data includes
bytes extensionDataand pass that value unchanged as the sixthswap()argument. Keep operation32as an explicitly empty extension data variant so existing version 2 encoders retain their tuple meaning.Callback data can remain empty because the executor authenticates the active pool and stores its payment context transiently. Preserve the existing pool sender check, input cap, delta sign validation, direct final recipient accounting and residue handling in the new path.
-
I-08 Informational Generated executor address is stale Configuration Acknowledged
Description
The generated Robinhood deployment projection is intended to identify the executor for the reviewed release. Its
ACTIVE_EXECUTORconstant still points to0x23dF315E5bEFE989Bd9259BE66557E580eaB14a7.The current Kipseli adapter at
0xc0010CFEce8bbD3D7c964099a12f1C4159816161authorizes only0x9a4212cf08B59a2C4B3d383Df67Be6bA43126E0f. The latter runtime exactly matches the executor compiled from commit3b6ce6b. The generated address has a different runtime, is no longer authorized by the adapter and does not expose the release'sBEBOP_ROUTER()integration. Both executors accept TokenFlow version 2, while the version 1 behavior belongs only to the retired executor at0x8e9A5e1195095F2baDDD4911993e1B0a787f5312.A route service, deployment process or monitor that consumes
ACTIVE_EXECUTORcan therefore select an obsolete feature set. Kipseli calls through that address revert because the adapter rejects it and release checks can incorrectly assess Bebop support. No onchain asset exposure was observed because the current authorized executor has no standing venue allowances or LiquidCore admission at the pinned block.Recommendation
Regenerate
RobinhoodDeployment.solsoACTIVE_EXECUTORidentifies the intended release executor. Make the release pipeline reject a projection unless its address, runtime code hash, accepted TokenFlow version, immutable integration addresses and adapter authorization all match the intended deployment. -
I-09 Informational Rialto requires optional token getters Compatibility Acknowledged
Description
Rialto's Prop AMM guide at commit
b5c3347314a3b124c0336af405d21ac42777fac7definesgetAmountOutandswapExactInas the required pool surface. It expressly makestoken0()andtoken1()optional because Rialto obtains the pair and its direction from configuration._ensureRialtoPropAmmAllowance()first confirms that the active Rialto router allows the pool'sswapExactInselector. It then unconditionally calls both optional getters and compares their results with the route tokens. A pool that omits either getter therefore reverts during admission before it receives an allowance or a settlement call. This happens even when the pool implements both required selectors and the active Rialto router permits it.A route compiler cannot avoid the failure by changing operation data because operation
170has no alternate source for the configured token pair. Every FlowRouter swap that selects such a pool rolls back. The current deployed Fermi pool census contains getters, so the demonstrated impact applies when Rialto admits a pool that uses the smaller supported interface.The local compatibility proof passed both tests against the unmodified executor. A direct quote and standard-token swap succeed through the required pool methods, while the executor route reverts at the missing token0 selector after a mocked successful allowlist response. No currently affected deployed pool was identified. This is submitted as an informational integration limitation, with no protected-fund loss or current production outage claimed.
Reference: Rialto Prop AMM Integration Guide.
Recommendation
Treat the token getters as an optional validation aid. Probe each getter with
staticcalland compare any well formed address it returns. When both getters are available and match, retain the existing cached admission and persistent allowance path. When either getter is unavailable, do not cache the caller supplied pair. Confirm the active Rialto router's allowlist on every execution, approve only the resolved input amount, executeswapExactInand reset the allowance to zero before residue accounting. -
I-10 Informational Native output to an executor reverts Compatibility Acknowledged
Description
FlowRouteraccepts an address asoutputReceiverand transfers the measured output to that address after the executor returns. An ERC-20 output can therefore settle toTokenFlowExecutorV2through an ordinary token transfer.Native settlement follows a different path.
FlowRoutercalls the executor afterexecutePath()has cleared its route lock. The executor'sreceive()function accepts native currency only from the wrapped native token or from selected venues while that lock is active. It does not acceptFlowRouter, so the final native transfer fails.TokenHelper.universalTransfer()converts the failed call intoTokenHelper.InvalidNativeTransfer()and the complete swap rolls back.This does not cause asset loss or a persistent denial of service because the caller chooses the recipient and the revert is atomic. However, route construction cannot treat the executor as an interchangeable output destination across ERC-20 and native assets. A route that works with an ERC-20 output deterministically fails when its output is native currency.
Recommendation
Mark executor addresses as unsupported native output recipients in route construction policy and document this restriction next to the
outputReceiverparameter. -
I-11 Informational Fixed referral fees overflow on large outputs Math Acknowledged
Description
FlowRouterapplies a registered fixed referral fee before it compares the measured output withoutputQuote. The beneficiary share and the remaining user output both use multiplication before division. In particular, line 198 evaluatesamountOut * (FEE_DENOM - referralFee)with checkeduint256arithmetic even though the following division would produce a validuint256result.The interface does not limit
amountOutto keep that intermediate product in range. With a one percent fee, a gross output of116961706300319389316738368695644351366939378450141983878239settles. Increasing the gross output by one raw unit overflows the net output multiplication. This value is belowtype(int256).max, so the overflow occurs before the separate signed slippage conversion.Fixed fee swaps therefore support a smaller amount range than swaps without a fixed referral fee. An affected call reverts after executor output is delivered and the EVM rolls back every transfer. Users with an extreme raw token amount cannot settle the route and still pay its transaction cost.
Recommendation
Calculate both fixed fee amounts with full precision multiplication and division. Use
Math.mulDiv(amountOut, uint256(referralFee) * 8, FEE_DENOM * 10)for the beneficiary share andMath.mulDiv(amountOut, FEE_DENOM - referralFee, FEE_DENOM)for the remaining user output. This preserves the current floor rounding and removes the overflowing intermediate products. -
I-12 Informational Signed slippage casts reject large amounts Math Acknowledged
Description
swapTokenInforepresentsoutputQuoteandoutputMinasuint256._swapalso measuresamountOutas auint256. The settlement logic nevertheless converts bothamountOutandoutputQuotewithSafeCast.toInt256()to calculate the signedslippagevalue. Either conversion reverts when its operand is at least2^255.This signed representation places an undocumented bound on otherwise valid unsigned swap amounts. A no fee swap that receives and quotes exactly
type(int256).maxsettles normally. Increasing both values by one makes the same path revert after the executor has delivered the output token. The quote conversion can also trigger the failure independently. For example, an output of one unit withoutputMinset to one still reverts whenoutputQuoteis2^255, even though the mathematical slippage of1 - 2^255is representable byint256.All state changes roll back, so the failure does not lose the transferred tokens. It prevents
swapandswapPermit2from settling ordinary ERC20 routes that use the upper half of the advertised amount range. The practical impact is limited to tokens or quotes with extreme raw values, but affected users pay gas for a route that completes execution before the router rejects its telemetry calculation.Recommendation
Keep settlement accounting unsigned. Compare
amountOutwithoutputQuotedirectly and calculate the surplus or shortfall magnitude only in the selected branch. Replace the signed slippage field with a versioned event that records a sign flag and auint256magnitude so event encoding does not restrict the amounts that the swap functions can settle. -
I-13 Informational Pons refunds are retained by FlowRouter Business logic errors Acknowledged
Description
FlowRouterdelivers the caller's fullinputAmountto the executor before executing a route. When the supplied input would buy more launch tokens than remain available for sale, the Pons curve limits the purchase to the remaining amount. It recalculates the input required for those tokens and returns the excess to its caller, which isTokenFlowExecutorV2.For ERC-20 input,
_finishPulledInput()measures the lower actual spend and_returnInputResidue()transfers the balance above the route floor to the immutablerouter. For native input, the curve returns excess wei to the executor during the call and the program finalizer transfers the balance abovenativeBalanceFloorto the router.FlowRouter._swap()only measures and transfers the output token. It does not account for input assets returned by the executor or forward them to the input payer.Consequently, a successful partial Pons buy charges the caller the full supplied input while the unused portion becomes FlowRouter inventory that only an approved sweeper can withdraw through the intended interface. The caller receives the clamped launch token output, so the output minimum can still pass. The caller therefore loses the difference between the supplied input and the amount Pons actually charged. This loss is limited only by the route's supplied input and can exceed negligible rounding or dust amounts.
Recommendation
Track the input asset balance around the executor call in
FlowRouterand return any positive route scoped increase tomsg.senderafter successful execution. For native input, calculate the opening router balance without the currentmsg.valueso that only wei returned by the executor is refunded. Preserve the executor's existing token and native floors so that balances from earlier routes cannot be included in the refund. -
I-14 Informational V4 native settlement inherits stale sync DoS Acknowledged
Description
The Uniswap V4
PoolManageruses one transient synced currency context for settlement. Callingsync(token)selects an ERC-20 and snapshots its reserves. Callingsync(address(0))restores the native currency context. Ifsettle()receives native value while a nonzero currency remains selected, the manager reverts withNonzeroNativeValue()rather than crediting the native debt.Both native debt paths in
TokenFlowExecutorV2assume that this context is already native._uniswapV4RunSingle()callsmanager.settle{value: job.amountIn}()directly and_uniswapV4RunBatch()does the same when the batch's negative net currency is native. Neither path first callsmanager.sync(address(0)).A hook executes inside the same
PoolManager.unlock()and can call the publicsync()function. If the selected hook leaves an ERC-20 synced after returning its valid swap deltas, the executor's swap and all amount/minimum checks pass, but the subsequent native payment reverts exactly withNonzeroNativeValue(). The same context dependency affects a single native input swap and a native input batch with a negative native net.No affected deployed hook was found despite extensive hook census testing. This is a transaction local availability failure. The revert restores the caller's input, manager deltas, hook state and token movements, but the user pays gas and cannot execute the otherwise valid native route through the executor. Although route construction chooses the pool and hook, the executor exposes hooked native V4 swaps as a supported operation and can establish the required native context independently.
Recommendation
Call
manager.sync(address(0))immediately before every nativesettle{value: amount}()in_uniswapV4RunSingle()and_uniswapV4RunBatch(). Keep the existing ERC-20syncthen transfer thensettlesequence unchanged. -
I-15 Informational Algebra swaps cannot supply plugin data Compatibility Acknowledged
Description
Algebra Integral forwards the swap’s data argument to its configured plugin hooks. Plugins can use this input to verify an authorization or apply caller-specific swap behavior.
The executor’s
V3PoolDatacontains only the pool, direction, and price limit. Algebra operation101uses the common V3 execution path, which always passes an empty byte string to the pool. Appending a payload toOperation.datadoes not help because the executor decodes the fixed tuple and reconstructs the call with empty data.Consequently, plugin features that depend on caller-supplied data are unavailable through operation
101, even when the route compiler possesses the required payload. If a plugin requires that payload for execution, the complete swap reverts and an included transaction costs the submitter gas.Recommendation
Acknowledge that operation
101cannot supply plugin payloads and document this restriction for route construction. No onchain change is necessary if payload-dependent plugin features are outside the intended integration scope. -
I-16 Informational Graduation and escrow accounting are incompatible with balance-changing quote tokens fee-on-transfer accounting Acknowledged
Description
Launch graduation measures the quote balance delta and records that amount as the launch's swept claim, but does not ensure the resulting claim remains seedable after a non-standard transfer. During pool creation, transfers are validated only by the recipient's credited delta and not by the factory's own debit; balance-changing tokens can therefore leave a launch permanently unseedable or consume balances backing other launches that share the asset.
Impact: For approved fee-on-transfer, rebasing, or surcharge tokens, a permissionless graduation/pool-creation call can burn or mis-account reserves, strand that launch, and in shared-balance cases deplete escrow attributable to another launch. This is an incompatibility with non-standard balance-changing ERC-20s; severity is limited to Low under the protocol's unsupported-token assumptions.
Recommendation
Reject or explicitly quarantine balance-changing assets, or perform end-to-end debit/credit measurements and rerun seedability checks before committing graduation state. Isolate per-launch escrow so one launch's transfer cannot debit another launch's recorded claim.
-
I-17 Informational Callback ERC20 can capture in-flight V4 payments or poison settlement via unrestricted sync reentrancy Acknowledged
Description
During ERC20 V4 settlement the executor calls sync, transfers the token externally, and then settles. A callback-capable token can overwrite the synced state and poison settlement, or—if an externally sourced payment arrives—call settleFor to claim the combined credit, take it, resync, and return only the executor's nominal amount. The fork PoC shows the latter clears NonzeroDeltaCount so the outer FlowRouter swap succeeds while the intervening donation is diverted.
Impact: This is a materially distinct accounting outcome from the prior revert-only variant, but callback-capable token support and a production reward source are not established. Therefore severity remains Low: affected callback-token routes may revert, and any triggerable third-party payment during the window could be captured; ordinary ERC20 routes are unaffected and no live production theft is demonstrated.
Recommendation
Treat callback-capable ERC20 inputs as unsupported or use pre-funded claims. Alternatively bind PoolManager sync snapshots to the authenticated locker/payer and reject settle/settleFor or resync by a different callback caller while the snapshot is active.
-
I-18 Informational Positive quote-token balance growth enables zero-cost bonding-curve buys cross-flow accounting / rebasing token Acknowledged
Description
Recipient-side balance deltas from quote transferFrom are treated as delivered payment without an upper bound. Positive reflection/rebase growth, including growth triggered by transferFrom(0), is credited as quoteIn; bonding-curve buy pricing then dispenses launch tokens against phantom payment, while FlowRouter can make excess reflected growth spendable as route input.
Impact: With a nonstandard balance-changing quote asset, a caller can obtain launch-token supply for zero or tiny quote debit, repeatedly consuming sellable reserves and distorting graduation accounting; analogous FlowRouter paths over-credit route budgets beyond Permit2 authorization. Impact is limited to unsupported balance-changing tokens and is Low severity.
Recommendation
Reject or quarantine positive-rebasing/reflection assets, or cap observed recipient deltas at requested/signed amounts and separately account excess. Enforce deliveredInputAmount <= signed Permit2 amount before applying route budgets.
No findings match.
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.
