Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · May 2026

Protocol Review

for Avon

Guardian's review of Protocol Review for Avon, published May 2026. The report records 45 findings across 6 review rounds, including 1 high and 18 medium.

Published
Review window
November 24, 2025 to April 30, 2026
Rounds
Main Review, Remediation Review, Remediation Review 2, Remediation Review 3, Remediation Review 4, Remediation Review 5
Language
Solidity
Sector
Lending
  • 0 Critical
  • 1 High
  • 18 Medium
  • 19 Low
  • 7 Informational

31 resolved · 1 partially resolved · 13 acknowledged

Scope

32 files in scope · 3,952 nSLOC
FilenSLOCLines
src/VaultV2.sol7771306
src/adapters/AvonPoolAdapter.sol114246
src/adapters/AvonPoolAdapterFactory.sol1641
src/Orderbook.sol363596
src/OrderbookFactory.sol82139
src/OrderbookFactoryStorage.sol2328
src/libraries/AugmentedRedBlackTreeLib.sol635978
src/libraries/ErrorsLib.sol2891
src/libraries/EventsLib.sol22113
src/libraries/MathLib.sol2544
src/libraries/OrderbookLib.sol265429
src/pool/AvonPool.sol655903
src/pool/PoolStorage.sol93129
src/libraries/LiquidityAllocator.sol2744
src/libraries/SharesLib.sol1944
src/factory/AvonPoolFactory.sol5198
src/factory/VaultFactory.sol4181
src/oracle/Oracle.sol4773
src/irm/LinearKinkIRM.sol3253
src/pool/utils/PoolConstants.sol2846
src/pool/utils/PoolErrors.sol2162
src/pool/utils/PoolEvents.sol83292
src/pool/utils/PoolGetter.sol94125
src/pool/extensions/AccrueInterest.sol5686
src/pool/extensions/BorrowRepay.sol89152
src/pool/extensions/CollateralManagement.sol2647
src/pool/extensions/DepositWithdraw.sol3777
src/pool/extensions/FlashLoan.sol3354
src/pool/extensions/Liquidation.sol99154
src/pool/extensions/PositionGuard.sol3053
src/pool/extensions/UpdateOrders.sol3550
src/pool/extensions/Utils.sol610

Findings 45

Main Review

22 findings · November 24 to December 11, 2025
  1. M-01 Medium Referrer Claim DoS From Rounding Mismatch Rounding Resolved
    Location
    VaultV2.sol
    Round
    Main Review

    Description

    The referrer rewards system relies on an index-based accounting model to distribute escrowed referrer fee shares. The mechanism allocates newly escrowed shares by increasing a global index (referrerIndex) and tracking how many shares have been globally allocated but not yet claimed in totalUnclaimedReferrerShares. Both values are updated during index distribution which performs two separate floor-rounded conversions

            uint256 indexIncrease = pending.mulDivDown(WAD, stake);
            referrerIndex += indexIncrease;
            totalUnclaimedReferrerShares += indexIncrease.mulDivDown(stake, WAD);
    

    A referrer’s personal accrual is computed differently. It accrues new rewards based on the full index delta since their last interaction and apply floor rounding only once.

                uint256 delta = idx - lastIdx;
                uint256 sharesAccrued = stake.mulDivDown(delta, WAD);
                if (sharesAccrued != 0) {
                    referrerAccrued[referrer] += sharesAccrued;
                }
    

    The issue arises because totalUnclaimedReferrerShares is updated by rounding down twice on every index update. A referrer, however, accrues rewards by rounding down only once on the total index change since their last interaction. Because there’s a mismatch between the sum of round-downs and rounding down the sum, the referrer’s computed claimable amount can end up slightly higher than totalUnclaimedReferrerShares, even though the rewards were escrowed.

    When a referrer calls claimReferrerFees, the contract subtracts the referrer’s claimable shares from totalUnclaimedReferrerShares. If the referrer’s claimable amount is greater than totalUnclaimedReferrerShares, the subtraction underflows and the transaction reverts. As a result, affected referrers will be unable to claim their rewards.

    Recommendation

    Carry forward the discarded fractional remainder in each index update and transfers only the safely claimable amount instead of reverting when the computed claim exceeds totalUnclaimedReferrerShares. Optionally, consider allowing referrer to provide the intended amount of referrer fee.

  2. M-02 Medium Inverted Slippage Guard In _borrow Function Logical Error Resolved
    Location
    src/pool/extensions/BorrowRepay.sol:51
    Round
    Main Review

    Description

    In the borrow path, when the exact amount of asset is provided, the internal _borrow path computes the borrower’s debt shares from the requested assets and then enforces that the amount of shares must be greater or equal to minExpected, treating this as a minimum tolerable number of debt shares. Since debt shares represent liability, this check is inverted - it causes transactions to revert when the user would receive fewer shares than expected (better terms) while allowing execution when they receive more shares than expected (worse terms), meaning minExpected cannot be used to cap the borrower’s maximum debt and instead only blocks favorable outcomes.

    Recommendation

    Replace the minimum shares check with a maximum debt-share cap so the call reverts whenever calculated debt shares exceed that bound.

  3. M-03 Medium EIP-4626 Previews Skew With Interest Accrual Unexpected Behavior Resolved
    Location
    AvonPool.sol
    Round
    Main Review

    Description

    In AvonPool, the state-changing deposit and mint functions call accrueInterest before delegating to the counterpart functions defined in ERC4626, while previewDeposit and previewMint are only inherited and do not account for this interest accrual. This means that within a single transaction an integrator can observe previewDeposit output on pre‑accrual state and then call deposit, which will mint fewer shares than the preview predicted. Similarly, previewMint can return fewer required assets than mint actually pulls after accrual.

    This violates EIP-4626’s MUST‑level requirements that, in the same transaction, deposit must always return the same or more shares than previewDeposit and mint must always consume the same or fewer assets than previewMint, and can mislead or break integrators relying on these bounds for slippage and safety checks.

    Recommendation

    Override previewDeposit and previewMint to use the same preview‑accrued totals that deposit and mint use after accrueInterest, keeping the previews consistent with actual execution.

  4. M-04 Medium Underwater Liquidations Inflate Bad Debt Logical Error Resolved
    Location
    Liquidation.sol
    Round
    Main Review

    Description

    The liquidation mechanism allows partial liquidations of underwater positions to decrease their health score, meaning each liquidation step removes more collateral value than debt value repaid when the liquidation bonus exceeds the current collateral-to-debt ratio.

    Health score is defined as (LLTV * collateralValue) / borrowAssets and a liquidation that repays some amount of debt seizes collateral worth bonusFactor * debt. When the liquidation bonus factor is greater than collateralValue / borrowAssets, the collateral value drops faster than the debt, so the post-liquidation health score will be lower than before.

    As a result, the collateral/debt coverage ratio gets strictly worse after a partial liquidation, so if prices continue to move against the collateral, this will increase the amount of bad debt ultimately socialized to LPs. This behaviour contradicts the invariant that liquidation should improve a position’s health, and creates opportunity to game this mechanism by executing small partial liquidations that push positions deeper underwater, amplifying future bonuses and extracting more value from borrower collateral and worsening the eventual losses absorbed by LPs.

    Recommendation

    Clamp the effective liquidation bonus to at most the current collateral‑to‑debt ratio, so a partial liquidation can only keep the health score the same or improve it.

  5. M-05 Medium Repay Cleanup Breaks Borrow Share Accounting DoS Resolved
    Location
    src/pool/extensions/BorrowRepay.sol:130
    Round
    Main Review

    Description

    The repay function includes a cleanup condition that sets s.totalBorrowShares to zero whenever s.totalBorrowShares is greater than zero while s.totalBorrowAssets is zero. This is intended to prevent leaving residual global borrow shares after all borrow assets have been fully repaid.

    However, due to the rounding direction in repay and liquidation flows, users can slightly overpay assets, causing the system to reach a state where the cleanup branch zeroes out s.totalBorrowShares even though at least one position still holds non-zero borrowShares.

    This creates an accounting desync: the sum of all user borrowShares becomes strictly greater than the global totalBorrowShares. Once the system enters this state, any attempt to fully repay or liquidate the final remaining borrower will revert due to an underflow when subtracting from totalBorrowShares.

    Recommendation

    Update or remove the cleanup logic so that s.totalBorrowShares is only zeroed when both global borrow assets and all per-position borrowShares have reached zero.

  6. M-06 Medium Referrer Can Be Forced Via Zero-Share Transfer Logical Error Resolved
    Location
    src/VaultV2.sol:1133
    Round
    Main Review

    Description

    The transfer function updates referrer associations whenever shares move between accounts. If the recipient has no referrer set and the sender has a whitelisted referrer, the recipient automatically inherits the sender’s referrer.

    This behaviour can be abused to force a referrer onto any user. An attacker can simply send a zero-share transfer to a user who has not yet set a referrer. That user is then assigned the attacker’s referrer. When the user later performs a deposit and specifies their own referrer through the depositWithReferrer function, the protocol ignores the supplied referrer because the user already has one set. Since referrers cannot be changed once assigned, the user is permanently locked with the initial referrer.

    Recommendation

    Restrict referrer assignment so it only occurs on explicit referrer-setting calls. This prevents attackers from forcing referrers via zero-value or dust transfers.

  7. M-07 Medium Over-Seizure When repaidShares Is Clamped Logical Error Resolved
    Location
    src/pool/extensions/Liquidation.sol:23
    Round
    Main Review

    Description

    In the _liquidate function, when liquidating with the seizedAssets parameter, the function first derives repaidShares from the proposed seizedAssets (after applying seizeCap) using the configured liquidation bonus. Later, it clamps repaidShares to position.borrowShares, but does not recompute seizedAssets after this clamp.

    As a result, in edge cases where the initial calculation would repay more than the remaining debt, the liquidator still seizes the full seizedAssets amount while only repaying the reduced, clamped debt. This allows a liquidator to over-seize collateral beyond what corresponds to the final repaid debt and bonus, causing additional value loss for the borrower.

    Recommendation

    Whenever repaidShares is reduced (for example, clamped to position.borrowShares) in the seizedAssets path, recompute seizedAssets from the final repaidShares.

  8. L-01 Low Unaccounted Fees When feeRecipient Is Zero Logical Error Resolved
    Location
    src/pool/extensions/FlashLoan.sol:66
    Round
    Main Review

    Description

    In _flashLoanLoanToken, the total flashloan fee is split into poolFeeAmount and protocolFeeAmount and only poolFeeAmount is added to s.totalSupplyAssets. If protocolFeeAmount is greater than 0 but feeRecipient is address(0), the transfer is skipped and the full feeAmount stays in the vault’s token balance, but only poolFeeAmount is reflected in s.totalSupplyAssets. This leaves orphaned tokens inside the pool that are neither claimable by LPs nor withdrawn as protocol fees, breaking the expected accounting invariant and effectively locking part of the flashloan fees inside the contract.

    Recommendation

    Ensure the protocol fee portion is consistently accounted by either adding protocolFeeAmount to s.totalSupplyAssets whenever it remains in the pool instead of being transferred out or reverting when protocolFlashLoanFeePercentage > 0 && feeRecipient == address(0).

  9. L-02 Low Inconsistent Interest Projections While Paused Logical Error Resolved
    Location
    src/pool/utils/PoolGetter.sol:92
    Round
    Main Review

    Description

    When the pool is paused, interest accrual is applied to the pause timestamp and then blocked, but _previewAccrueInterest and the view functions that rely on it continue to compute interest using elapsed = block.timestamp - s.lastUpdate as if the pool were still accruing normally. On unpause, pausePool function sets s.lastUpdate = block.timestamp without applying that projected interest, so all interest previewed during the paused interval is effectively discarded. This creates a discrepancy where views overestimate debt and yield while paused, and then return to lower, actually-accrued values once the pool is resumed.

    Recommendation

    Make preview logic respect the paused state by treating elapsed as zero while the pool is paused.

  10. L-03 Low Deposit Cap Not Reflected Informational Partially resolved
    Location
    ERC4626.sol
    Round
    Main Review

    Description

    AvonPool enforces deposit cap inside the deposit and mint functions, via an internal check. However, the contract inherits the default ERC4626 implementations of maxDeposit and maxMint, which both return type(uint256).max. As a result, off-chain integrators that follow the standard pattern of querying maxDeposit(receiver)/maxMint(receiver) to determine the maximum safe amount will be told they can deposit/mint arbitrarily large amounts, even though such calls can revert at runtime once the internal cap check is hit.

    Recommendation

    Override maxDeposit and maxMint in AvonPool so they reflect the effective depositCap or clearly document for integrators that these view functions do not enforce the pool’s deposit cap.

  11. L-04 Low No Slippage Bounds On ERC4626 Flows Unexpected Behavior Resolved
    Location
    AvonPool.sol
    Round
    Main Review

    Description

    AvonPool exposes the standard ERC4626 deposit, mint, withdraw, and redeem functions but does not provide wrapper functions that allow callers to specify minimum/maximum bounds on shares or assets, even though EIP4626 recommends such variants when supporting direct EOA usage.

    As a result, users and integrators have no built-in way to protect themselves from slippage and unexpected exchange rate changes on these operations, increasing the risk of unintentionally receiving fewer assets or shares than intended.

    Recommendation

    Consider adding slippage-aware wrapper functions for deposit, mint, withdraw, and redeem that accept min/max asset or share bounds and revert if these bounds are violated.

  12. L-05 Low Pool Lacks Support Configuration Acknowledged
    Location
    AvonPool.sol
    Round
    Main Review

    Description

    The Avon pool contract does not support non-standard ERC20 tokens. For instance tokens that take fees on transfer, since those fees break the expected accounting. In addition, tokens with callbacks can trigger reentrancy during deposit or borrow operations and bypass internal caps, allowing users to manipulate balances or exceed intended limits.

    Recommendation

    Ensure the pool contract only accepts standard ERC20 tokens.

  13. L-06 Low previewBorrow Ignores Borrow Cap Validation Resolved
    Location
    src/pool/utils/PoolGetter.sol:54
    Round
    Main Review

    Description

    The previewBorrow function does not account for the protocol’s borrowCap when estimating how much a user can borrow. As a result, callers relying on previewBorrow may receive a borrow estimate that exceeds the actual allowable amount, causing transactions to revert when the real borrow call enforces the cap.

    Recommendation

    Update previewBorrow to include the borrow-cap constraint so previews match the limits enforced during actual borrowing.

  14. L-07 Low PreviewBorrow Includes Paused Pools Validation Resolved
    Location
    src/Orderbook.sol:412
    Round
    Main Review

    Description

    The previewBorrow function doesn't skip paused pools, even though the matching logic ignores them during actual matching execution. As a result, previewBorrow can return estimates that rely on liquidity from pools that will not be used, causing the previewed results to differ from real outcomes.

    Recommendation

    Update previewBorrow to filter out paused pools in the same way the matching logic does, ensuring previews align with executable behaviour.

  15. L-08 Low Stale Orders After Borrow Cap Update Logical Error Resolved
    Location
    src/pool/AvonPool.sol:736
    Round
    Main Review

    Description

    The _executeUpdateBorrowCap function updates the borrow cap, which is then used by _updateOrders to compute available liquidity for quoting and maintaining the orderbook. Because _executeUpdateBorrowCap does not call _updateOrders after applying the new cap, the existing orders remain based on outdated liquidity assumptions. This can leave quotes in the orderbook that are no longer fillable under the updated cap.

    Recommendation

    Call _updateOrders after applying a borrow cap update so the orderbook reflects the new available liquidity.

  16. L-09 Low maxWithdraw and maxRedeem Ignore Pool Constraints Unexpected Behavior Resolved
    Location
    AvonPool.sol
    Round
    Main Review

    Description

    AvonPool adds extra withdrawal constraints (liquidity checks against totalBorrowAssets and the whenNotPaused modifier) in withdraw and redeem function, but inherits the default ERC-4626 implementations of maxWithdraw and maxRedeem, which only look at the user’s share balance and the conversion rate. As a result, maxWithdraw and maxRedeem can report amounts that exceed what the pool actually can serve (when liquidity is constrained by borrows or the pool is paused), breaking the EIP-4626 requirement that these functions must factor global limits and never return a value higher than the actual maximum and skewing integrators’ assumptions when they treat these as safe upper bounds.

    Recommendation

    Override maxWithdraw and maxRedeem so they incorporate liquidity constraints and pause state.

  17. L-10 Low Paused Pools Block Fills During Matching Logical Error Resolved
    Location
    src/Orderbook.sol:260
    Round
    Main Review

    Description

    matchMarketBorrowOrder and matchLimitBorrowOrder both skip paused pools when calling _aggregatePoolData, ensuring that paused pools do not contribute liquidity to the final borrow result. However, earlier in the flow, both functions call _matchOrder, which iterates through lender orders and attempts to fill the borrower’s request.

    The issue is that _matchOrder does not check whether a pool is paused. If it encounters an order from a paused pool, it will attempt to match it and count that amount toward the borrower’s progress. Later, _aggregatePoolData correctly excludes that pool, causing the matched amount to be discarded.

    As a result, a borrower may receive only a partial fill even when sufficient liquidity exists in active pools. If the borrower uses a loose minAmountExpected, the call may still succeed but unnecessarily charge the flat matching fee despite having no actual fill. In cases where a paused pool has deep liquidity and good rates but its orders remain on the book, it can consistently block full matches.

    In principle, pools should cancel all orders before pausing. However, if cancellation fails or an order remains for any reason, the stale order can still be picked up by _matchOrder, triggering this inconsistency.

    Recommendation

    Ensure _matchOrder excludes paused pools in the same way _aggregatePoolData does, so that paused pools cannot interfere with match progression.

  18. L-11 Low Incorrect loanTokenAmount Logical Error Resolved
    Location
    src/Orderbook.sol:452
    Round
    Main Review

    Description

    In previewBorrow function, when previewBorrowParams.isCollateral == false, the function correctly computes PreviewMatchedOrder and collateralRequired from the matched orders, but never updates loanTokenAmount, leaving it at its default value of 0 even when totalMatched is greater than 0, which makes the returned data inconsistent with the actual matches and can mislead integrators that rely on loanTokenAmount to understand how much loan liquidity is actually available. The function’s NatSpec explicitly states that loanTokenAmount is “The total amount of loan tokens that would be received”, which is also meaningful in the isCollateral == false case, and returning 0 there is inconsistent with that documented behavior.

    Recommendation

    Consider updating previewBorrow so that loanTokenAmount is always set to the total matched loan amount in both modes or ensure the NatSpec explicitly documents this behavior so integrators can safely rely on the return value.

  19. L-12 Low Market Order LTV And Rate Discrepancy Unexpected Behavior Resolved
    Location
    https://github.com/GuardianOrg/avon-core-team1-1763891009755/blob/main/src/Orderbook.sol#L424 https://github.com/GuardianOrg/avon-core-team1-1763891009755/blob/main/src/Orderbook.sol#L257
    Round
    Main Review

    Description

    For market orders, previewBorrow unconditionally overwrites the caller supplied LTV with MIN_LTV (50%), and rate with the highest lender rate, while matchMarketBorrowOrder only defaults to MIN_LTV and the highest lender rate when ltv or rate are set to 0, otherwise honoring custom non‑zero value. As a result, previews can be computed with different rate/ltv constraints than the ones actually used at execution, leading to incorrect collateral estimates and different pools/liquidity being matched than previewed, causing confusing UX and potential failed or unexpected transactions for users and integrators that rely on previewBorrow as an accurate execution simulator.

    Recommendation

    Update previewBorrow so that for market orders, it resolves ltv and rate exactly like matchMarketBorrowOrder (only defaulting ltv/rate when the input value is 0), or clearly document that isMarketOrder==true ignores any caller-supplied rate/ltv.

  20. I-01 Informational Redundant Fee Adjustment In Interest Preview Superfluous Code Resolved
    Location
    src/pool/utils/PoolGetter.sol:133-135
    Round
    Main Review

    Description

    The protocol uses _accrueInterest function to update state and _previewAccrueInterest for view-only projections, both following the same pattern - compute gross accruedInterest, add it to totalBorrowAssets and totalSupplyAssets, derive managerFeeAmount and protocolFeeAmount and mint fee shares. However, _previewAccrueInterest contains an extra block.

            if (managerFeeAmount > 0 || protocolFeeAmount > 0) {
                accruedInterest -= (managerFeeAmount + protocolFeeAmount);
            }
    

    This modifies only a local accruedInterest that is never used afterward, so it has no effect on the current previewed totals and is misleading.

    Recommendation

    Remove the unused fee subtraction in _previewAccrueInterest function.

  21. I-02 Informational Incorrect Maker Field In OrderCanceled Event Events Resolved
    Location
    src/libraries/OrderbookLib.sol:167
    Round
    Main Review

    Description

    In _cancelOrder, the OrderCanceled event is emitted as emit EventsLib.OrderCanceled(isLender, msg.sender, rate, ltv, amount), even though the actual order owner is stored in entry.account. This misattributes cancellations triggered by owners/keepers/pool cleanup to the caller rather than the original maker, reducing the reliability of the event log for auditing.

    Recommendation

    Emit OrderCanceled with entry.account as the maker argument.

  22. I-03 Informational IPoolImplementation Interface Mismatch Informational Resolved
    Location
    IPoolImplementation.sol
    Round
    Main Review

    Description

    The IPoolImplementation interface used in the Orderbook repo does not match the IPoolImplementation actually implemented by AvonPool, which may lead to broken integration for several core methods in the future.

    • liquidate is declared in the Orderbook interface as liquidate(address borrower, uint256 assets, uint256 shares) but AvonPool implements liquidate(address borrower, uint256 assets, uint256 shares, uint256 minSeizedAmount, uint256 maxRepaidAsset, bytes calldata data),
    • updateOrderbook is declared as updateOrderbook(address newOrderbook, address newOrderbookFactory) in the Orderbook interface, while AvonPool expects an additional bytes32 salt argument,
    • The Orderbook interface exposes increaseLLTV(uint64 newLTV) whereas AvonPool uses updateLLTVUpward(uint64 newLTV, bytes32 salt)

    Recommendation

    Update the Orderbook’s IPoolImplementation to match AvonPool’s signatures.

Remediation Review

8 findings · December 23 to 25, 2025
  1. M-01 Medium maxMint Exceeds Cap After Interest Accrual Unexpected Behavior Resolved
    Location
    src/pool/AvonPool.sol:229
    Round
    Remediation Review

    Description

    The maxDeposit uses _previewAccrueInterest(false) to compute remaining deposit capacity from post‑accrual totalSupplyAssets, but maxMint converts that capacity into shares with convertToShares, which uses pre‑accrual pool totals. This mismatch can cause maxMint function to return a share amount that is not actually mintable once mint flow calls accrueInterest and recomputes required assets, leading to unexpected reverts and breaking ERC‑4626 integrator assumptions that maxMint is a reliable upper bound.

    Recommendation

    Update maxMint to derive shares using the same preview‑accrued totals as maxDeposit.

  2. M-02 Medium Transfers Can Force Referrer Assignment Logical Error Resolved
    Location
    src/VaultV2.sol:1135
    Round
    Remediation Review

    Description

    The _updateReferrerOnTransfer function was updated to avoid assigning a referrer on zero-amount transfers. However, the issue still exists. If a recipient does not yet have a referrer set, and the sender has a whitelisted referrer, transferring any non-zero amount will cause the recipient to inherit the sender’s referrer.

    This makes it possible for an attacker to force a referrer onto any address at minimal cost by sending a dust transfer. Once this happens, even if the recipient later attempts to set their own referrer explicitly through functions like depositWithReferrer, the forced referrer will take precedence and cannot be overridden.

    Recommendation

    Consider Removing referrer assignment from _updateReferrerOnTransfer; only allow referrer assignment through explicit actions (setUserReferrer or depositWithReferrer).

  3. M-03 Medium De-whitelisted Referrers Can Still Claim Fees Logical Error Resolved
    Location
    https://github.com/GuardianOrg/vault-contract-team1-1763752225 https://github.com/GuardianOrg/vault-contract-team1-1763752225872/blob/mihir/add-avon-adapter/src/VaultV2.sol#L1277 https://github.com/GuardianOrg/vault-contract-team1-1763752225872/blob/mihir/add-avon-adapter/src/VaultV2.sol#L1233872/blob/mihir/add-avon-adapter/src/VaultV2.sol#L386
    Round
    Remediation Review

    Description

    The VaultV2 comment states that “Only whitelisted referrers can receive fee distributions”, but the implementation does not enforce this guarantee.

    Whitelisting is controlled by setIsWhitelistedReferrer, which only flips a boolean and removes the address from the _activeReferrers set. It does not clear the referrer’s stake (referrerTotalShares), does not remove their stake from the global stake counter (totalReferrerShares), and does not prevent claiming.

    Reward claiming is performed via claimReferrerFees, which accrues rewards for sender and transfers shares held by the vault to an arbitrary to address. However, there is no check that msg.sender is currently whitelisted. Therefore, a referrer that was whitelisted in the past, accrued rewards, and was later removed from the whitelist can still call claimReferrerFees and receive fee shares.

    Also, the _accrueReferrer uses referrerTotalShares[referrer] as stake and does not verify isWhitelistedReferrer[referrer]. If a referrer is de-whitelisted after having stake, that stake remains in the system and continues to earn index-based rewards unless the owner separately and correctly calls clearReferrerData.

    Recommendation

    Require isWhitelistedReferrer[msg.sender] in claimReferrerFees and, upon de-whitelisting, automatically settle the referrer’s accrual and remove their stake from totalReferrerShares so they can no longer accrue or claim fee distributions.

  4. L-01 Low _updateOrders Can Execute During Pause Unexpected Behavior Resolved
    Location
    AvonPool.sol
    Round
    Remediation Review

    Description

    The _updateOrders function is invoked by several setter functions that are protected by a timelock. If the pool is paused at that time and a timelocked setter that calls _updateOrders is executed, _updateOrders will still run even though the pool is paused. As a result, orders are pushed back into the orderbook while the pool is in a paused state.

    Recommendation

    Ensure that _updateOrders does not execute while the pool is paused, or add explicit pause checks to all setter functions that invoke it. This prevents paused pools from republishing orders, especially if the timelock execution role is ever set to be permissionless.

  5. L-02 Low Share Inflation Can Cause Temporary DOS DoS Acknowledged
    Location
    src/pool/extensions/BorrowRepay.sol:25
    Round
    Remediation Review

    Description

    The cleanup logic that previously zeroed totalBorrowShares in _repay was removed to prevent an accounting desynchronization between the global totalBorrowShares and the sum of all user borrowShares. However, this reintroduces the initial borrow-share inflation edge case, which can be abused to temporarily block borrows.

    Recommendation

    Consider enforcing a minimum borrow amount, or a minimum totalBorrowAssets whenever there are open borrows, to prevent manipulation of borrow share math using small values.

  6. I-01 Informational Misnamed Slippage Parameter In _borrow Resolved
    Location
    src/pool/extensions/BorrowRepay.sol:25
    Round
    Remediation Review

    Description

    The _borrow function applies the slippage guard correctly by ensuring that the computed borrow shares do not exceed the user-provided bound.

    However, the parameter is currently named minExpected, which is misleading. The check enforces an upper bound on shares, meaning it is actually acting as a maximum acceptable amount of borrow shares rather than a minimum.

    Recommendation

    Rename the parameter to reflect its current role as a maximum borrow share amount

  7. I-02 Informational Paused Pool Quotes Can Desync State Informational Acknowledged
    Location
    Orderbook.sol
    Round
    Remediation Review

    Description

    The orderbook keeps two related states - the lender quote liquidity stored in lenderTree and a per-pool list of rates in poolOrders[pool] that is primarily maintained when pools call batchInsertOrder. These can become inconsistent in pause/refresh edge cases.

    When a pool pauses, it attempts to clear its quotes by calling the orderbook with empty arrays inside a try/catch block.

            try IOrderbook(s.orderBook).batchInsertOrder(rates, liquidity) {}
            catch {
                emit PoolEvents.CancelOrdersFailed(address(this));
            }
    

    However, batchInsertOrder is gated by whenNotPaused. If the orderbook is also paused at that time, the cancellation reverts and the failure is swallowed, leaving the pool’s old quotes still present in lenderTree and its rates still listed in poolOrders.

    Once the orderbook is later unpaused (while the pool can remain paused), borrowing matches are allowed again. The matching flow first consumes/removes entries from lenderTree via _matchOrder, and only afterwards drops paused pools during aggregation (in _aggregatePoolData).

    This ordering allows a paused pool’s quotes to be removed from lenderTree without any interaction with the pool, while poolOrders[pool] is not pruned to reflect the removals. The inconsistency persists until the pool successfully re-quotes (or an admin removes the pool), and during this window off-chain consumers of getPoolOrders may observe stale and unserviceable rates.

    Recommendation

    Be aware of this edge case inconsistency and consider making quote cancellation/refresh resilient to orderbook pauses (e.g. allow empty batchInsertOrder calls to cancel existing orders even while paused).

  8. I-03 Informational Shares Sent Without Receive Gate Checks Logical Error Acknowledged
    Location
    https://github.com/GuardianOrg/vault-contract-team1-1763752225872/blob/mihir/add-avon-adapter/src/VaultV2.sol#L1300 https://github.com/GuardianOrg/vault-contract-team1-1763752225872/blob/mihir/add-avon-adapter/src/VaultV2.sol#L1277
    Round
    Remediation Review

    Description

    The vault uses gates (receiveSharesGate, sendSharesGate) to control which accounts can receive or send shares via canReceiveShares/canSendShares, and these checks are enforced in enter, transfer, and transferFrom. However, claimReferrerFees and sweepOrphanedShares transfer shares without calling canReceiveShares.

    As a result, the gating policy is not consistently enforced across all share transfer paths, which can undermine assumptions and lead to shares being held by addresses that should have been blocked.

    Recommendation

    Verify if receiver address can receive shares in both claimReferrerFees and sweepOrphanedShares functions. If the current behavior is intentional, explicitly document that these two functions bypass receiveShareGate/canReceiveShares, and note that if a blacklist is introduced in the future, these paths should be updated to add the gate check.

Remediation Review 2

4 findings · January 9, 2026
  1. M-01 Medium Re-whitelist Restores Stake With Stale Index Logical Error Resolved
    Location
    VaultV2.sol
    Round
    Remediation Review 2

    Description

    The contract intends to prevent unwhitelisted referrers from later receiving rewards for the period they were not whitelisted by updating their referrerIndexOf without accruing rewards. However, when the user is re-whitelisted, setIsWhitelistedReferrer adds the preserved stake back to totalReferrerShares but does not reset referrerIndexOf[referrer] to the current referrerIndex and does not call _accrueReferrer(referrer) while the referrer is still non-whitelisted.

    If referrerIndex increases while the referrer is de-whitelisted and their checkpoint remains stale, the referrer can later be re-whitelisted and call claimReferrerFees, causing _accrueReferrer to credit them for index growth that occurred while they were not eligible, draining referrer escrow and reducing/delaying other referrers’ claims.

    Recommendation

    In the re-whitelisting path, update the global index and set referrerIndexOf[referrer] to the current referrerIndex before re-adding their stake, so they start accruing only from the re-whitelisting moment.

  2. M-02 Medium Withdraw DoS After Referrer De-Whitelist Logical Error Resolved
    Location
    VaultV2.sol
    Round
    Remediation Review 2

    Description

    When the owner de-whitelists a referrer, setIsWhitelistedReferrer subtracts the referrer’s entire stake from totalReferrerShares but leaves referrerTotalShares[referrer]. Later, both exit (used for withdraw and redeem) and _updateReferrerOnTransfer (used by transfer and transferFrom) can still compute a non-zero stake reduction from the reserved referrerTotalShares and decrement totalReferrerShares without checking whether the referrer is currently whitelisted. For users tied to that de-whitelisted referrer, this decrement can underflow (since totalReferrerShares was already reduced), reverting and preventing withdrawals/redeems and transfers until the referrer is re-whitelisted or the owner clears data.

    Recommendation

    Only decrement totalReferrerShares when the referrer is currently whitelisted.

  3. M-03 Medium totalReferrerShares Drift On Transfers Logical Error Resolved
    Location
    src/VaultV2.sol:1153
    Round
    Remediation Review 2

    Description

    totalReferrerShares is intended to track the amount of shares currently counted toward referrer fee distribution. When a referrer is de-whitelisted, their stake is removed from totalReferrerShares, but users can still hold shares pointing to that removed referrer. In _updateReferrerOnTransfer, when shares move from a user tied to a removed referrer to a user tied to a whitelisted referrer, those shares should transition from “not counted” to “counted” and increase totalReferrerShares accordingly. However, the function only increases totalReferrerShares via the missingStake adjustment, which is typically zero in the de-whitelist case (since referrerTotalShares[fromReferrer] still exists and the subtraction does not cap). As a result, totalReferrerShares can remain too low after these transitions, drifting from the true sum of whitelisted referrer stakes. This drift can skew referrer fee allocation and referrer index accounting by using an incorrect global denominator.

    Recommendation

    In _updateReferrerOnTransfer, update totalReferrerShares based on whether the transferred shares were previously counted and whether they should be counted after transfer.

  4. M-04 Medium clearReferrerData Skews totalReferrerShares DoS Resolved
    Location
    src/VaultV2.sol:416
    Round
    Remediation Review 2

    Description

    When a referrer is de-whitelisted, setIsWhitelistedReferrer(referrer, false) already removes the referrer’s entire stake by subtracting referrerTotalShares[referrer] from totalReferrerShares. Later, if the owner calls clearReferrerData(referrer), the function reads stake = referrerTotalShares[referrer] and conditionally subtracts that same stake from totalReferrerShares again whenever totalReferrerShares >= stake.

    As a result, clearReferrerData can double-subtract an already-excluded stake, causing totalReferrerShares to drift downward and no longer match the sum of active referrer stake. This can lead to a underflow/DoS in referrer-related transfer and withdrawal logic.

    Recommendation

    Do not decrement totalReferrerShares in clearReferrerData for referrers whose stake has already been removed on de-whitelisting.

Remediation Review 3

2 findings · January 16 to 18, 2026
  1. M-01 Medium Legacy userAttributedShares Wipes New Stake Logical Error Acknowledged
    Location
    src/VaultV2.sol:387
    Round
    Remediation Review 3

    Description

    When a referrer is removed from the whitelist, the vault deletes referrerTotalShares[referrer], but it does not clear users’ userAttributedShares tied to that referrer. If the referrer is later re-whitelisted, only new deposits are added back into referrerTotalShares[referrer], while the user’s userAttributedShares still includes legacy attributed shares from before the removal.

    This creates a mismatch after re-whitelisting: the referrer does not earn fees (after being re-whitelisted) from legacy userAttributedShares accumulated before/during de-whitelisting because those shares are not included in referrerTotalShares[referrer]. However, in exit(), the withdrawal reduction amount is still computed from userAttributedShares, meaning those legacy attributed shares can still drive reductions that wipe out the referrer’s newly re-accrued stake from fresh deposits.

    Example:

    • User A deposits and has userAttributedShares = 200 linked to Referrer A.
    • Referrer A is de-whitelisted → referrerTotalShares[A] is cleared to 0, but user A still has userAttributedShares = 200.
    • Referrer A is re-whitelisted and user B deposits, adding 50 new attributed shares → referrerTotalShares[A] = 50.
    • If user A now withdraws, exit() will reduce referrerTotalShares[A] by up to 50 (capped), wiping out all of Referrer A’s new stake even though the withdrawal was primarily consuming legacy attributed shares that do not earn fees.

    This can cause referrers to lose future earnings capacity on legitimate new deposits due to withdrawals of older, non-counted stake.

    Recommendation

    Consider avoiding re-whitelisting the same referrer address after removal. If a referrer must be reinstated, require a new referrer address to prevent legacy userAttributedShares from wiping post-reinstatement stake.

  2. I-01 Informational Liquidation Can Panic With Zero bonusFactor Logical Error Acknowledged
    Location
    https://github.com/avon-xyz/avon-periphery/blob/main/src/pool/extensions/Liquidation.sol#L43 https://github.com/avon-xyz/avon-periphery/blob/main/src/pool/extensions/Liquidation.sol#L83
    Round
    Remediation Review 3

    Description

    The liquidation pricing logic can hit an unhandled division by zero edge case where the computed collateral value rounds down to zero while the borrower still has non zero debt, which can occur when

    position.collateral * collateralPrice < ORACLE_PRICE_SCALE (1e36).
    

    In liquidation, the code computes a collateral value in loan-token units using integer division, then derives a coverageRatio and clamps the liquidation bonusFactor to that ratio to avoid worsening the borrower’s health. When coverageRatio becomes 0, bonusFactor becomes 0 as well, and subsequent calculations divide by bonusFactor, triggering the panic revert instead of a controlled protocol error.

    Recommendation

    Be aware of this rare dust/low-price scenario and add a check to revert when bonusFactor is zero.

Remediation Review 4

8 findings · April 14 to 20, 2026
  1. H-01 High Residual Limit Orders Brick on Partial Fill Logical Error Resolved
    Location
    src/Orderbook.sol:364
    Round
    Remediation Review 4

    Description

    When a keeper partially fills a borrower limit order, the protocol attempts to preserve the unfilled remainder, but reinserts the residual borrower-tree entry under the wrong account. In matchLimitBorrowOrder(), the keeper executes the fill and then calls _cancelBorrowerOrder() with only the matched amount. If the order is only partially filled, _cancelBorrowerOrder() removes the old tree entry and reinserts the remainder through borrowerTree._insertOrder(false, rate, ltv, orderAmount - amount). However, _insertOrder() records entry.account = msg.sender, and in this path msg.sender is the keeper, not the borrower.

    This leaves the residual order in a split-brain state. The stored order metadata remains under borrowersOrders[borrower] with reduced amount, minAmountExpected, and collateralAmount, but the live borrower-tree entry now belongs to the keeper. All later management paths still assume both pieces belong to the borrower. The borrower still has the stored order, but _cancelBorrowerOrder() scans the tree for entry.account == borrower, which now fails because the residual entry was reassigned to the keeper. As a result, the borrower cannot cancel the remainder, later keeper execution of the remainder fails through the same broken ownership path, and the borrower cannot replace the order at the same (rate, ltv) bucket because that update path also first tries to cancel the old order. In practice, once a keeper partially fills a borrower limit order, the remaining order becomes bricked.

    Beyond bricking the residual order, each such partial fill also leaves behind an orphaned borrower-tree entry that normal flows cannot clean up. Since _cancelBorrowerOrder() performs a linear scan over all entries in the borrower bucket, these dead entries increase future scan costs for later cancellations, same-bucket replacements, and keeper fills. Over time, repeated partial fills at the same (rate, ltv) can therefore accumulate broken entries, increase gas costs, and worsen the borrower-bucket DOS surface.

    Recommendation

    If the intended behavior is to preserve residual orders after partial keeper fills, the post-fill path must reinsert the remainder under the borrower address rather than under msg.sender. More generally, borrower-tree ownership should always be sourced from the logical order owner, not from the immediate caller context. If that behavior is not intended, the protocol should instead fully remove partially filled limit orders and avoid leaving any residual order state behind.

  2. M-01 Medium Partial Fills Leave Residual Orders Unbacked Logical Error Resolved
    Location
    src/Orderbook.sol:327
    Round
    Remediation Review 4

    Description

    When a keeper partially fills a borrower limit order, the protocol preserves a residual order in storage, but refunds collateral using the stale pre-update collateralAmount rather than the residual order’s updated backing. In matchLimitBorrowOrder(), the function snapshots collateralAmount = order.collateralAmount before calling _cancelBorrowerOrder(). During a partial fill, _cancelBorrowerOrder() reduces the stored order in place and leaves a residual order.collateralAmount proportional to the unfilled amount. However, after returning, matchLimitBorrowOrder() still computes excessCollateral = collateralAmount - totalCollateral using the original pre-update collateral value, refunds that excess back to the borrower, and then forwards totalCollateral to the pool.

    This exhausts the full original escrow during the partial fill. The matched portion of collateral is pulled into the pool, and the remainder is refunded back to the borrower. Despite that, the residual order that remains in borrowersOrders[borrower] still reports a nonzero collateralAmount as if collateral were still backing the remaining order. The protocol therefore reaches a state where stored borrower-order accounting diverges from actual collateral balances held by the orderbook.

    In practice, after a partial keeper fill, the orderbook can preserve a residual borrower order whose recorded collateral backing no longer matches actual escrowed funds. Any later logic that treats order.collateralAmount as live reserved collateral will be operating on stale accounting rather than real backing.

    Recommendation

    If the intended behavior is to reinsert residual orders after partial keeper fills, collateral refunds must be calculated from the updated residual order state rather than from the stale pre-fill collateral snapshot. The residual order should only remain in storage if the corresponding residual collateral also remains escrowed in the orderbook. If that behavior is not intended, the protocol should instead fully remove partially filled limit orders and avoid leaving any residual collateral state behind.

  3. M-02 Medium Partial Fills Enforce Full-Order Min Output Logical Error Acknowledged
    Location
    src/Orderbook.sol:327
    Round
    Remediation Review 4

    Description

    When a keeper partially fills a borrower limit order, the protocol validates the matched slice against the full pre-fill minAmountExpected instead of against a proportional minimum for the portion actually executed. In matchLimitBorrowOrder(), the function snapshots minAmountExpected = order.minAmountExpected before matching. If the order is only partially filled, the protocol still executes the borrow for only matchedOrder.totalMatched, but later checks if (netAmount < minAmountExpected) revert ErrorsLib.InsufficientAmountReceived();. This compares the proceeds of a partial execution against the minimum output floor for the entire original order.

    As a result, legitimate partial fills can revert even when the matched slice executed correctly. For example, if a borrower posts an order for 2000 units with minAmountExpected = 1800, and only 1000 units are currently fillable, the partial execution can return at most roughly 1000 units before fees. The protocol nevertheless compares that partial result against 1800 and reverts. In practice, this means partial fills only succeed when the borrower set minAmountExpected low enough to tolerate much smaller executions than the original order size.

    This is also inconsistent with the protocol’s own partial-order accounting model. _cancelBorrowerOrder() already prorates minAmountExpected during partial reductions by subtracting only the matched portion’s proportional minimum from the stored residual order. The matching path, however, still enforces the original full-order minimum against the partial fill itself. The protocol therefore treats minAmountExpected as proportional in one partial-order path and absolute in another, causing otherwise valid partial executions to fail.

    Recommendation

    If partial limit-order fills are intended to be supported, matchLimitBorrowOrder() should validate netAmount against a proportional minimum corresponding only to matchedOrder.totalMatched, using the same scaling logic already applied in _cancelBorrowerOrder().

  4. L-01 Low No Admin Cancel for Stuck Borrow Orders Warning Acknowledged
    Location
    Orderbook.sol
    Round
    Remediation Review 4

    Description

    The orderbook provides no privileged mechanism to cancel or clear borrower limit orders once normal user flows become impractical or unusable. Borrower-side cleanup is only available through cancelBorrowOrder(), keeper execution in matchLimitBorrowOrder(), or same-bucket replacement through insertLimitBorrowOrder(). If a borrower bucket becomes excessively bloated, stale entries accumulate, or a specific order becomes operationally stuck, there is no owner or emergency-admin function that can force-cancel the order and return the associated collateral.

    This reduces the protocol’s ability to respond to griefing or stale-order conditions. Affected orders remain dependent on the same broken or impractical path, with no administrative fallback to unwind them and return collateral.

    Recommendation

    Consider allowing a trusted admin or emergency role to call borrower-order cancellation on a user’s behalf and refund the related collateral

  5. L-02 Low RedStone maxAge allows stale data Configuration Acknowledged
    Location
    src/oracle/RedstoneOracle.sol:50
    Round
    Remediation Review 4

    Description

    The RedStone oracle adapters set maxAge to 600000000 and compare it against block.timestamp, what gives a freshness window of roughly 19 years rather than a normal oracle heartbeat window.

    As a result, getCollateralToLoanPrice can accept stale RedStone prices for borrow previews, collateral safety checks and liquidation logic.

    Recommendation

    Replace the default maxAge value with a realistic freshness window.

  6. L-03 Low Loan/USD freshness is not validated Configuration Acknowledged
    Location
    Oracle
    Round
    Remediation Review 4

    Description

    getLoanToUsdPrice function is part of the shared oracle interface and is implemented by every oracle adapter. However, it returns the loan/USD feed answer without performing any freshness check, and _getTicks uses that value to convert pool liquidity into USD before choosing the quote granularity.

    As a result, stale loan/USD prices can silently drive _getTicks() into the wrong bucket and cause pools to publish an incorrect number and distribution of quotes.

    Recommendation

    Make every oracle adapter’s getLoanToUsdPrice validate updatedAt against maxAge and revert on stale data.

  7. L-04 Low Underwater Liquidations Lack Incentive Warning Acknowledged
    Location
    Liquidation.sol
    Round
    Remediation Review 4

    Description

    The liquidation logic can make deeply underwater liquidations loss-making for liquidators. The computed liquidation bonus is clamped to the position’s collateral coverage ratio via bonusFactor = Math.min(bonusFactor, coverageRatio). When collateralValue < borrowedAssets, this ratio is below 1e18, so the effective bonus falls below par.

    In this state, a liquidator can be required to repay more loan-token value than the seized collateral is worth. This clears the borrower and avoids pool bad-debt realization, but shifts the shortfall to the liquidator. Rational users are unlikely to execute these liquidations, leaving deeply underwater positions dependent on backstop intervention.

    Recommendation

    Consider explicitly monitoring deeply underwater positions and liquidating them through a protocol-controlled backstop or keeper process, since permissionless users are unlikely to perform liquidations that require them to absorb the shortfall. Alternatively, adjust the liquidation flow so that insufficient collateral is realized as protocol bad debt rather than being pushed onto the liquidator.

  8. L-05 Low Zero repay allows users to discard dust interest Logical Error Acknowledged
    Location
    https://github.com/GuardianOrg/avon-periphery-team1-1763891033217/blob/47105e6e0df9f7dd3fc83609b62cadbc7d8999a2/src/pool/AvonPool.sol#L799 https://github.com/GuardianOrg/avon-periphery-team1-1763891033217/blob/47105e6e0df9f7dd3fc83609b62cadbc7d8999a2/src/pool/extensions/AccrueInterest.sol#L49-L53 https://github.com/GuardianOrg/avon-periphery-team1-1763891033217/blob/47105e6e0df9f7dd3fc83609b62cadbc7d8999a2/src/pool/AvonPool.sol#L289-L299
    Round
    Remediation Review 4

    Description

    The pool accrual logic advances lastUpdate even when computed interest rounds down to zero. The public accrueInterest enforces a 12-hour cooldown to prevent accrual spam, but repay accepts (0, 0) inputs and calls the internal accrual path directly, bypassing that guard. An unprivileged caller can therefore trigger accruals every block, discarding each interval's fractional interest rather than letting it accumulate.

            uint256 accruedInterest = totalBorrowAssets.mulDiv(expFactor - PoolConstants.WAD, PoolConstants.WAD);
            if (accruedInterest == 0) {
                s.lastUpdate = currentTime;
                return (0, 0);
            }
    

    This is reachable through repay(0, 0, onBehalf). The repay entrypoint calls accrueInterest first, then routes zero inputs into the exact-shares repay path, which completes as a no-op repayment. As a result, an unprivileged caller can repeatedly trigger accrual on short intervals and keep resetting lastUpdate whenever the pool accrued interest for that interval is still below 1 smallest unit.

    The effect is limited to small pools, low-decimal assets, low rates and short intervals, where fractional interest can be repeatedly discarded and lender yield understated.

    Recommendation

    Reject repay(0, 0, onBehalf) so users cannot trigger zero-effect accrual updates.

Remediation Review 5

1 finding · April 30, 2026
  1. M-01 Medium Stale quote can block borrow matching Logical Error Acknowledged
    Location
    https://github.com/GuardianOrg/avon-periphery-team1-1763891033217/blob/f335b74aee12c569e94be61c1794181763264ef7/src/pool/utils/PoolGetter.sol#L64 https://github.com/GuardianOrg/avon-core-team1-1763891009755/blob/5f25f7071b2009828f6bc4d6e2334eb8c418e349/src/Orderbook.sol#L280-L302 https://github.com/GuardianOrg/avon-core-team1-1763891009755/blob/5f25f7071b2009828f6bc4d6e2334eb8c418e349/src/Orderbook.sol#L627-L632 https://github.com/GuardianOrg/avon-core-team1-1763891009755/blob/5f25f7071b2009828f6bc4d6e2334eb8c418e349/src/Orderbook.sol#L609 https://github.com/GuardianOrg/avon-periphery-team1-1763891033217/blob/f335b74aee12c569e94be61c1794181763264ef7/src/pool/extensions/CollateralManagement.sol#L19
    Round
    Remediation Review 5

    Description

    During pool matching, the orderbook selects pools from the lender tree and then retrieve collateral value from each selected pool using previewBorrow and finally executes depositCollateral and process borrow for each pool.

    Pool quotes in the lender tree reflect the pool's state at the time they were written. _updateOrders uses the stored s.totalBorrowAssets to compute capacity under the borrow cap, while previewBorrow uses the projected post-accrual value. As interest accrues and the projection crosses the borrow cap, previewBorrow returns 0 for any new borrow while the stale quote remains in the tree.

    if (s.borrowCap > 0 && previewPool.totalBorrowAssets + assets > s.borrowCap) return 0;
    

    When matchMarketBorrowOrder runs, it calls previewBorrow on each matched pool to compute required collateral. For a cap-breached pool this returns 0, which is forwarded directly into depositCollateral, causing an unconditional revert.

        function _depositCollateral(PoolStorage.PoolState storage s, uint256 assets, address onBehalf) internal {
            if (assets == 0) revert PoolErrors.ZeroAssets();
    

    There is no skip or fallback logic, so the transaction reverts even when other whitelisted pools have available liquidity. Because the transaction reverts atomically, the stale quote is not consumed, and subsequent borrowers can repeatedly hit the same failing path even if other pools are healthy.

    Recommendation

    Treat previewBorrow == 0 as non-executable quote and exclude it from execution before calling depositCollateral. Remove or refresh that stale entry in the lender tree so it cannot repeatedly block later market borrows.

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.

Get a quote