Findings
High Risk
1 findings
1 fixed
0 acknowledged
Medium Risk
4 findings
1 fixed
3 acknowledged
Low Risk
6 findings
2 fixed
4 acknowledged
Informational
8 findings
2 fixed
6 acknowledged
High Risk1 finding
Loss of funds due to broken redeem functionality for siUSD
State
Severity
- Severity: High
Submitted by
slowfi
Description
In the
PendleInfiniFisiUSDcontract, the_redeemfunction handles the redemption of SY shares back to underlying tokens, supporting USDC, iUSD and siUSD as output tokens based on theisValidTokenOutandgetTokensOutfunctions, which explicitly include SIUSD (the staked token address) as a valid option. However, whentokenOutis set to SIUSD, the function does not properly process the redemption and instead falls through to an uninitialized return statement:return amountTokenOut;, whereamountTokenOutdefaults to 0 due to lack of assignment in this path. This occurs because theifconditions only handle iUSD and USDC explicitly, leaving siUSD unhandled despite being advertised as supported.The base contractPendleERC4626UpgSYV2correctly handles yieldToken (siUSD) redemptions by assigningamountTokenOut = amountSharesToRedeemand transferring the siUSD tokens, but the override inPendleInfiniFisiUSDneglects this case. As a result, during redemption viaSYBaseUpg.redeem, the user's SY shares are burned successfully, but no tokens are transferred to the receiver, leading to permanent loss of the redeemed value. Here is the relevant code snippet fromPendleInfiniFisiUSD._redeem:if (tokenOut == IUSD) { return IInfiniFiGateway(GATEWAY).unstake(receiver, amountSharesToRedeem);}if (tokenOut == USDC) { uint256 receiptOut = IInfiniFiGateway(GATEWAY).unstake(address(this), amountSharesToRedeem); address redeemController = IInfiniFiGateway(GATEWAY).getAddress("redeemController"); uint256 assetsOut = IRedeemController(redeemController).receiptToAsset(receiptOut); return IInfiniFiGateway(GATEWAY).redeem(receiver, receiptOut, assetsOut);}return amountTokenOut;In contrast, the
_previewRedeemfunction correctly previews the siUSD redemption by returningamountSharesToRedeem, creating a discrepancy where users or integrators expect to receive siUSD based on previews but receive nothing in reality. The overall impact is severe, as this bug enables direct and irrecoverable loss of user funds during redemptions targeting siUSD.Recommendation
To correct this issue, add explicit handling for the SIUSD case in the
_redeemfunction to mirror the base contract's behavior, ensuring theamountSharesToRedeemis assigned toamountTokenOutand the tokens are transferred to the receiver. Alternatively, if siUSD redemption is not intended, remove SIUSD fromgetTokensOutandisValidTokenOutto prevent misleading users.Here is a suggested code fix for adding the handling:if (tokenOut == SIUSD) { amountTokenOut = amountSharesToRedeem; _transferOut(SIUSD, receiver, amountTokenOut); // Assuming _transferOut from TokenHelper is available return amountTokenOut;}InfiniFi: Fixed on commit ID 1cfc3b96f22b85ba6652145cdd8528b46b36e6fe.
Cantina: Fix verified. The fix now transfers
SUSDto users if it is set astokenOut.
Medium Risk4 findings
Inaccurate wrapped yield token tracking
State
- Acknowledged
Severity
- Severity: Medium
Submitted by
r0bert
Description
In
wrapYieldTokenToPt, thePendleV2FarmV2contract updatestotalWrappedYieldTokensby adding the input amount_yieldTokenIn. However, the actual amount received after the swap, accounting for slippage, is calculated asactualOut = ptToYieldToken(ptReceived), which is lower than_yieldTokenIndue t solippage. This discrepancy causestotalWrappedYieldTokensto overstate the true wrapped value by up to themaxSlippagethreshold (0.3%).For example, providing 1000(1e21)
_yieldTokenIn,actualOutin the integration test is 9.997e20, as shown in the logs below:│ ├─ emit DebugUint(a: "_yieldTokenIn", b: 1000000000000000000000 [1e21])│ ├─ emit DebugUint(a: "actualOut", b: 999747383618229566197 [9.997e20])This inflated state variable is fed into the interpolatingYield calculation:
uint256 totalWrappedAssets = yieldTokensToAssets(totalWrappedYieldTokens);int256 totalYieldRemainingToInterpolate = int256(maturityAssetAmount) - int256(totalWrappedAssets) - int256(alreadyInterpolatedYield);leading to overstated remaining yield. Pre-maturity,
assets()includes this viayieldTokensToAssets(totalWrappedYieldTokens) + interpolatingYield(), overvaluing the farm.The same pattern affects the
PendleV2Farmcontract in itswrapAssetToPtfunction, wheretotalWrappedAssets += _assetsIndespite slippage reducing effective value.Recommendation
Track the effective post-slippage amount by updating
totalWrappedYieldTokens += actualOutinstead of_yieldTokenIn. Apply the same adjustment inPendleV2FarmfortotalWrappedAssets.InfiniFi: Adding a comment for it on commit ID 1506903063c5230f97563217f8cbaf2a058c8436. The farm does not report losses when investing in the PTs, as the yield generated towards maturity should make up for this, that's why before maturity it is only reported an increasing amount of assets.
Cantina: Acknowledged and documented by InfiniFi team.
Prematurity asset overvaluation from undiscounted slippages
State
- Acknowledged
Severity
- Severity: Medium
Submitted by
r0bert
Description
The
assetsfunction in thePendleV2FarmV2contract overstates value before maturity by not accounting for slippages in the full deployment path. Whenblock.timestamp < maturity, it returns:return supportedAssetBalance + yieldTokensToAssets(totalWrappedYieldTokens) + interpolatingYield();Here,
supportedAssetBalanceincludes USDC (assetToken) that must first be swapped toyieldTokens(e.g., sUSDe) viasignSwapOrder, incurring up to 0.3% slippage (maxSlippage= 0.997e18). Then, thoseyieldTokensare wrapped to PTs viawrapYieldTokenToPt, adding another 0.3% slippage. The calculation treats these as fully deployable without loss, inflating the valuation.On the other hand,
interpolatingYielddoes applymaturityPTDiscount(0.998e18, 0.2%) for unwrap slippage:maturityAssetAmount = maturityAssetAmount.mulWadDown(maturityPTDiscount);but this only covers post-maturity unwrap, not the entry slippages. Unwrapping is 1:1 after maturity in Pendle, but prematurity valuation ignores the ~0.6% cumulative entry cost. After maturity, the only slippage we should account for is the conversion from
yieldTokento USDC (if executed).Recommendation
In the pre-maturity branch of the
PendleV2FarmV2.assetsfunction, discountsupportedAssetBalanceandyieldTokensToAssets(totalWrappedYieldTokens)by a cumulative entry slippage factor.InfiniFi: Code commented on commit ID 1506903063c5230f97563217f8cbaf2a058c8436 to highlight the uncounted fees.
Cantina: Acknowledged and documented by InfiniFi team.
Undiscounted swap fees in asset valuation in EthenaFarm
State
- Acknowledged
Severity
- Severity: Medium
Submitted by
r0bert
Description
The
assetsfunction values all holdings (USDC, USDe, sUSDe) in USDC-equivalent terms using oracle prices, but doesn't apply any discount for swap fees or slippage when converting USDC->USDe->sUSDe and sUSDe->USDe->USDC:uint256 usdcBalance = IERC20(_USDC).balanceOf(address(this));uint256 usdcPrice = Accounting(accounting).price(_USDC); uint256 usdeBalance = IERC20(_USDE).balanceOf(address(this));uint256 usdePrice = Accounting(accounting).price(_USDE); uint256 susdeBalance = IERC20(_SUSDE).balanceOf(address(this));uint256 susdePrice = Accounting(accounting).price(_SUSDE); // add USDe in the process of unstakingusdeBalance += ISUSDe(_SUSDE).cooldowns(address(this)).underlyingAmount; usdcBalance += usdeBalance.mulDivDown(usdePrice, usdcPrice);usdcBalance += susdeBalance.mulDivDown(susdePrice, usdcPrice); return usdcBalance;Swaps between USDC ↔ USDe (or indirectly for sUSDe) via
signSwapOrderincur up to 0.2% loss (maxSlippage = 0.998e18). Unstaking sUSDe to USDe is fee-free, but the subsequent USDe → USDC swap still applies. This overstates the farm's true liquid value, especially when holding large USDe/sUSDe balances.For example, 1000 USDe valued at ~1000 USDC (assuming 1:1 peg) reports as 1000, but actual redemption after swap might yield only 998 USDC.
Recommendation
Discount non-USDC balances in
assets()bymaxSlippagewhen converting to USDC-equivalent (e.g.,usdcBalance += usdeBalance.mulDivDown(usdePrice, usdcPrice).mulWadDown(maxSlippage)).InfiniFi: Code commented on commit ID 1506903063c5230f97563217f8cbaf2a058c8436 to highlight the uncounted fees.
Cantina: Acknowledged and documented by InfiniFi team.
previewDeposit and previewRedeem reports inaccurate rates
State
Severity
- Severity: Medium
Submitted by
r0bert
Description
ERC‑4626 expressly requires the preview helpers to provide a quote that is “as close to and no more than the exact result of the real deposit or redeem executed in the same transaction.” The Pendle InfiniFi SY breaks this guarantee whenever the underlying gateway triggers its internal reward‑settlement routine.
Both
stakeandmintAndStakeinsideInfiniFiGatewayV2begin by callingyieldSharing.distributeInterpolationRewards(). That function realizes pending yield for every SIUSD holder and, crucially, mutates the vault’s accounting variables before the SY’s own balance‑changing code executes. In contrast,previewDepositandpreviewRedeemare pure view calls: they bypass the gateway and read the pre‑distribution state. The up‑to‑date exchange‑rate that will exist after the first real deposit or redeem in a block is therefore invisible to the previews.The observable consequence is that an integrator calling
previewDeposit(token, amount)receives a quote that can be off by the full size of the reward distribution. Every time rewards accrue between two user interactions, the first state‑changing call will settle them and invalidate all cached previews observed since the previous settlement, violating ERC‑4626 sectionpreviewDeposit/previewRedeem.Recommendation
Consider updating the
_previewDepositand_previewRedeemfunctions inPendleInfiniFisiUSDto consider the effects of thedistributeInterpolationRewardscall.InfiniFi: Fixed in commit ID 1cfc3b96f22b85ba6652145cdd8528b46b36e6fe.
Cantina: Fix verified. The fix updated the functions
_previewDepositand_previewRedeemto provide an exact quote, adjusting toERC4626specification.
Low Risk6 findings
Unaccounted direct PT token transfers
State
- Acknowledged
Severity
- Severity: Low
Submitted by
r0bert
Description
The
assetTokensfunction defines supported assets as onlyassetToken(e.g., USDC) andyieldToken(e.g., sUSDe):address[] memory tokens = new address[](2);tokens[0] = assetToken;tokens[1] = yieldToken;return tokens;PT tokens (e.g., PT-sUSDe) are not included and
isAssetSupportedwould returnfalsefor them. However, if PTs are transferred directly to the farm,IERC20(ptToken).balanceOf(address(this))increases without updatingtotalWrappedYieldTokensortotalReceivedPTs.Prematurity,
assets()ignores this extra balance, as it relies ontotalWrappedYieldTokensandinterpolatingYield, which don't factor in unsolicited PTs. Post-maturity,assets()includes the fullbalanceOfPTs:uint256 balanceOfPTs = IERC20(ptToken).balanceOf(address(this));uint256 ptAssetsValue = ptToAssets(balanceOfPTs).mulWadDown(maturityPTDiscount);This creates a discontinuity: understated value pre-maturity (yield interpolation misses extra PTs) and sudden inclusion post-maturity.
Recommendation
In
assets()andinterpolatingYield, calculate effective PT value frombalanceOfPTsalways, treating excess overtotalReceivedPTsas a bonus (add tomaturityAssetAmount).InfiniFi: Acknowledged.
Cantina: Acknowledged by InfiniFi team.
Abrupt asset valuation changes from discount updates
State
- Acknowledged
Severity
- Severity: Low
Submitted by
r0bert
Description
The
setMaturityPTDiscountfunction allowsPROTOCOL_PARAMETERSrole to adjustmaturityPTDiscountat any time, including before maturity:maturityPTDiscount = _maturityPTDiscount;This discount (default 0.998e18, ~0.2%) factors into
interpolatingYieldfor pre-maturity yield projection:maturityAssetAmount = maturityAssetAmount.mulWadDown(maturityPTDiscount);which feeds into
totalYieldRemainingToInterpolateand the interpolated amount.assets()pre-maturity includes this via+ interpolatingYield(), so altering the discount causes an immediate revaluation of the farm's reported value.Integration tests demonstrate the issue: After wrapping 1000e18
yieldTokensand warping 45 days (halfway to maturity),assets()returned value is 1188.032912. SettingmaturityPTDiscountto 0.9e18 (10% discount) drops it to 1179.379584, a ~0.73% step decrease:farm.assets() before -> 1188032912>Call to setMaturityPTDiscount(0.9e18)<farm.assets() after -> 1179379584Such jumps disrupt protocol yield accounting in the
YieldSharingV2contract and should minimized as much as possible.Recommendation
Restrict
setMaturityPTDiscountto only callable after maturity or when no PTs are held (balanceOfPTs == 0).InfiniFi: Added a comment to it in commit ID 1506903063c5230f97563217f8cbaf2a058c8436the function to highlight this behavior.
Cantina: Acknowledged and documented by InfiniFi team.
Hardcoded ethena cooldown duration
Severity
- Severity: Low
Submitted by
r0bert
Description
The maturity function in
EthenaFarmhardcodes a 7-day cooldown:return block.timestamp + 7 days;This assumes a fixed unstaking period for sUSDe, but Ethena admins can update it via
setCooldownDuration, restricted toDEFAULT_ADMIN_ROLEand capped atMAX_COOLDOWN_DURATION(90 days):function setCooldownDuration(uint24 duration) external onlyRole(DEFAULT_ADMIN_ROLE) { if (duration > MAX_COOLDOWN_DURATION) { revert InvalidCooldown(); } uint24 previousDuration = cooldownDuration; cooldownDuration = duration; emit CooldownDurationUpdated(previousDuration, cooldownDuration);}If Ethena increases the cooldown (e.g., to 14 days for risk management), the farm underreports maturity, treating positions as "matured" prematurely. This misleads allocation voting in the protocol, directing funds to the farm expecting quicker liquidity.
Recommendation
Replace the hardcoded value with a dynamic read:
return block.timestamp + ISUSDe(_SUSDE).cooldownDuration(). Considering that you want to keep this farm illiquid with a minimum maturity of 7 days, the final implementation could look like this:function maturity() public view virtual override returns (uint256) { uint256 ethenaCooldown = ISUSDe(_SUSDE).cooldownDuration(); uint256 minMaturityDuration = 7 days; return block.timestamp + (ethenaCooldown > minMaturityDuration ? ethenaCooldown : minMaturityDuration);}Infinifi: Fixed in commit ID 12ef280bb8eb27edd97cf43a4bd8b97f6e11ccfa.
Cantina: Fix verified.
Missing require check in withdrawSecondaryAsset function
Severity
- Severity: Low
Submitted by
r0bert
Description
The
withdrawSecondaryAssetfunction validates the asset withisAssetSupported(_asset), which returnstruefor the primaryassetTokensinceassetTokens()includes it. This allows callingwithdrawSecondaryAssetwith_asset == assetToken, withdrawing the primary asset via a directsafeTransfer:require(isAssetSupported(_asset), InvalidAsset(_asset)); uint256 assetsBefore = assets();IERC20(_asset).safeTransfer(_to, _amount);uint256 assetsAfter = assets(); emit AssetsUpdated(block.timestamp, assetsBefore, assetsAfter);The primary withdraw path uses
_withdraw(also asafeTransferinMultiAssetFarm) and emits with assumedassetsBefore - amount. While currently equivalent, this overlap could bypass subclass-specific logic if_withdrawis overridden in the future (e.g., for slippage checks or custom unwrapping in farms likePendleV2Farm). It also muddles intent, as secondary withdrawals are for non-primary assets.Recommendation
Add a check in the
withdrawSecondaryAssetfunction after theisAssetSupportedcall to ensure_asset != assetToken, reverting withInvalidAssetor a new error likePrimaryAssetNotSecondary. This enforces separation and future-proofs against overrides in_withdraw.Infinifi: Fixed in commit ID 1506903063c5230f97563217f8cbaf2a058c8436.
Cantina: Fix verified. The
withdrawSecondaryAssetfunction now checks that_asset != assetTokenpreventing from withdrawing the primary asset.Dangling dust approvals in PendleV2FarmV2 contract
State
- Acknowledged
Severity
- Severity: Low
Submitted by
r0bert
Description
In
unwrapPtToYieldTokenandwrapYieldTokenToPt, the contract sets an approval forpendleRouterusingforceApprove, but doesn't reset it to zero afterward:IERC20(ptToken).forceApprove(pendleRouter, _ptTokensIn);followed by
pendleRouter.call(_calldata). This can leave a non-zero approval indefinitely if the call doesn't consume the full amount. The same occurs inwrapYieldTokenToPtwithIERC20(yieldToken).forceApprove(pendleRouter, _yieldTokenIn).If
pendleRouteris compromised or behaves maliciously, it could drain the farm's PT oryieldTokenbalances via atransferFromcall. WhilependleRouteris trusted, this violates least-privilege by maintaining open-ended access.Recommendation
After the
pendleRouter.call, explicitly reset approval to zero withIERC20(token).forceApprove(pendleRouter, 0)in both functions to limit exposure to the current transaction.InfiniFi: Acknowledged.
Cantina: Acknowledged by InfiniFi team.
Potential Denial of Service on redeems due to pending losses
State
- Acknowledged
Severity
- Severity: Low
Submitted by
r0bert
Description
In the InfiniFi protocol, redemption operations, such as those handled via
InfiniFiGatewayV2.redeemandLockingController.withdraw(invoked indirectly throughPendleInfiniFisiUSDredemptions to USDC or iUSD), include a check in_revertIfThereAreUnaccruedLossesthat reverts ifYieldSharingV2.unaccruedYield()returns a negative value, indicating pending losses from farms. Similarly,StakedToken.maxRedeemandmaxWithdrawrevert under the same condition to prevent users from exiting positions before losses are propagated, ensuring fair loss distribution across locking users, staked holders and general iUSD holders.The
unaccruedYieldfunction calculates the difference between the protocol's total asset value (converted to receipt tokens like iUSD) and the total supply of receipt tokens, where a negative result signifies unrealized losses. To resolve this and enable redemptions, theYieldSharingV2.accruefunction must be called, which applies the yield (positive or negative) by minting/burning tokens or slashing positions as needed. In Pendle integrations, redeeming SY shares to USDC involves unstaking siUSD to iUSD via the gateway and then redeeming iUSD to USDC, inheriting these revert conditions and requiring accrual if losses are pending.Relevant code from
InfiniFiGatewayV2._revertIfThereAreUnaccruedLosses:require(yieldSharing.unaccruedYield() >= 0, PendingLossesUnapplied());And from
YieldSharingV2.accrue:int256 yield = unaccruedYield();if (yield > 0) _handlePositiveYield(uint256(yield));else if (yield < 0) _handleNegativeYield(uint256(-yield)); emit YieldAccrued(block.timestamp, yield);The
accruefunction is permissionless, allowing any user to call it and apply pending yields. For negative yields, it sequentially slashes locking positions (viaLockingController.applyLosses), staked tokens (viaStakedToken.applyLosses) and finally adjusts the receipt token oracle price if necessary. This ensures losses are applied fairly but requires an extra transaction during loss events. While this mechanism prevents users from redeeming at inflated values before losses are realized, it introduces a temporary barrier to redemptions until accrue is invoked. In the context ofPendleInfiniFisiUSD, where redemptions to USDC or iUSD rely on InfiniFi's gateway and staked token logic, this could manifest as a short-term denial-of-service (DoS), forcing users to wait for accrual, performed by themselves or another party, before completing the transaction.Recommendation
Given that calling
YieldSharingV2.accruepresents high gas costs, cosinder adding an informative revert error so users are aware that the revert is temporary until the losses are realized within Infinifi.InfiniFi team: Will not fix. Redeem will revert if losses were not applied but do not want to increase gas cost on the redemption.
Cantina: Acknowledged by InfiniFi team.
Informational8 findings
Asset valuation jump on early unwrap
Severity
- Severity: Informational
Submitted by
r0bert
Description
In
unwrapPtToYieldToken, theMANUAL_REBALANCERrole can exit PT positions early (before maturity), but must unwrap the fulltotalReceivedPTsamount. This resets tracking variables liketotalWrappedYieldTokens = 0andalreadyInterpolatedYield = 0. Pre-unwrap,assets()(whenblock.timestamp < maturity) uses a conservative valuation:supportedAssetBalance + yieldTokensToAssets(totalWrappedYieldTokens) + interpolatingYield(), whereinterpolatingYielddiscounts the projected maturity value bymaturityPTDiscount(0.998e18) for expected unwrap slippage.Post-unwrap, the farm holds
yieldTokens(e.g., sUSDe), valued directly via oracle prices without that discount. Integration tests showed this caused a small step increase in the reportedassets()value:farm.assets() before -> 1188.032912>Call to farm.unwrapPtToYieldToken(1198970021823818348799, _PENDLE_ROUTER_CALLDATA_9)<farm.assets() after -> 1188.905564The ~0.07% jump (from 1188.03 to 1188.91) reflects recovering more value than the discounted interpolation assumed, as actual early unwrap slippage may be less than anticipated. Impact is minor: sudden "yield spike" in the total protocol yield accounting.
Recommendation
Document this behavior in comments or protocol docs as expected due to conservative pre-maturity discounting.
Infinifi: Documented in commit ID 1506903063c5230f97563217f8cbaf2a058c8436.
Cantina: Fix verified. Code commented as suggested.
Unchecked deposit cap in secondary movements
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
The
_singleMovementSecondaryAssetfunction withdraws secondary assets from the source farm and transfers them directly to the destination farm address, then callsIFarm(_to).deposit()without first checking the destination'smaxDeposit():if (_asset == _fromAssetToken) { IFarm(_from).withdraw(_amount, _to);} else { MultiAssetFarm(_from).withdrawSecondaryAsset(_asset, _amount, _to);} // trigger deposit in destination farmIFarm(_to).deposit();In contrast, the primary asset path in
_singleMovementcaps_amounttomaxDepositbefore withdrawing:uint256 maxDeposit = IFarm(_to).maxDeposit();_amount = _amount > maxDeposit ? maxDeposit : _amount;Without this check for secondary assets, if the transfer would exceed the cap, the subsequent
deposit()reverts (asFarm.deposit()enforces cap), causing the entire transaction to revert. This includesbatchMovement, where one over-cap secondary move reverts the whole batch.Recommendation
Mirror the primary path by querying
maxDeposit()before the transfer and capping_amountaccordingly, allowing partial movements and avoiding unnecessary reverts.InfiniFi: Acknowledged.
Cantina: Acknowledged by InfiniFi team.
Unused custom error definition
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
The
ManualRebalancercontract defines a custom errorInactiveRebalancer:error InactiveRebalancer();but it is never thrown or referenced anywhere in the codebase.
Recommendation
Remove the unused error definition to streamline the contract and reduce deployment costs.
InfiniFi: Acknowledged.
Cantina: Acknowledged by InfiniFi team.
Potential redemption failures if low liquid USDC in Infinifi
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
In the protocol's redemption flow, the
InfiniFiGatewayV2.redeemfunction includes a strict minimum output check:require(assetsOut >= _minAssetsOut, MinAssetsOutError(_minAssetsOut, assetsOut));, where_minAssetsOutis set to the full expected USDC amount based on the iUSD input (computed viaRedeemController.receiptToAsset). The underlyingRedeemController.redeemsupports partial fulfillment by sending immediate liquidity and queueing the remainder inuserPendingClaimsfor later manual claim.If liquidity is insufficient (or the redemption queue is not empty),
redeemController.redeemreturns only the immediateassetsOut(potentially 0 or partial).In the Pendle integration (
PendleInfiniFisiUSD._redeemfor USDC out), the call togateway.redeem(receiver, receiptOut, assetsOut)(withassetsOutas full expected) reverts if only a partial amount of USDC is redeemed, reverting always in the following check:// InfiniFiGatewayV2.redeemuint256 assetsOut = redeemController.redeem(_to, _amount);require(assetsOut >= _minAssetsOut, MinAssetsOutError(_minAssetsOut, assetsOut)); // Reverts on partialThis design prioritizes atomicity (full or nothing) but at the cost of availability. Therefore, if there is not too much USDC on liquid farms that can be pulled and the amount of USDC redeemed is high, a partial redemption with its respective revert might be triggered.
Example scenario:
- Assume low liquidity in RedeemController:
liquidity() = 500 USDC, queue empty. - User/Pendle calls
gateway.redeem(receiver, 1000 iUSD, 1000 USDC min-out)(full expected from preview). redeemController.redeem: ComputesassetAmountOut=1000, but available=500 → sends 500 USDC, enqueues 500 equivalent iUSD.- Returns
assetsOut=500→ gateway checks500 >= 1000→ reverts with MinAssetsOutError. - Entire tx (including Pendle unstake/redeem) reverts → no redemption, no queue entry.
Recommendation
Merely informative. If this happens user can always perform a smaller
redeemcall pulling USDC and then pull the rest as iUSDC.InfiniFi: Acknowledged.
Cantina: Acknowledged by InfiniFi team.
Exchange rate is given directly in iUSD
Severity
- Severity: Informational
Submitted by
r0bert
Description
The
exchangeRatefunction inPendleInfiniFisiUSDreturnsIERC4626(SIUSD).convertToAssets(PMath.ONE), which computes the iUSD value per siUSD share. However, while this rate is correct, if iUSD is ever slashed it will not be reflected for any integrators. Integrators might mistakenly interpret the exchange rate as reflecting USDC-equivalent value, especially since deposits and redeems often involve USDC paths.Without adjustment, integrators querying
exchangeRatemay overvalue positions, leading to incorrect collateralization, liquidation thresholds, or trading decisions in protocols built on Pendle.Recommendation
Update the
exchangeRatefunction to compute the USDC-equivalent value by chaining the iUSD-to-siUSD rate with the iUSD-to-USDC redemption rate fromRedeemController. This provides a more accurate and integrator-friendly rate. Suggested code fix:address redeemController = IInfiniFiGateway(GATEWAY).getAddress("redeemController");uint256 iusdPerSiUsd = IERC4626(yieldToken).convertToAssets(PMath.ONE);return IRedeemController(redeemController).receiptToAsset(iusdPerSiUsd);Additionally, enhance documentation in
assetInfoandexchangeRateto clarify that the rate is slashing-sensitive and recommend using the full USDC conversion for valuations.InfiniFi: Fixed in commit ID 1cfc3b96f22b85ba6652145cdd8528b46b36e6fe as suggested.
Cantina: Fix verified.
Inefficient capital deployment due to cooldown between CowSwap orders
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
In the current design of CowSwap-integrated farms—such as
EthenaFarm,PendleV2FarmV2, and any others inheriting fromCoWSwapFarmBase—USDC is converted into sUSDe via CowSwap limit orders. This process is regulated by a cooldown constraint enforced through the_SIGN_COOLDOWNconstant, which defines a global minimum interval between consecutive order signings.The core logic resides in the
_checkSwapApproveAndSignOrderfunction withinCoWSwapFarmBase, and its behavior affects all derived contracts. When an order is signed, thelastSignTimeis updated and subsequent attempts to sign a new order must wait until the cooldown period elapses.This design introduces inefficiencies in capital utilization. For example, when a first deposit is received and swapped, any additional deposits arriving during the cooldown window must wait idle, without generating yield. In periods of frequent deposits or volatile user behavior, this can lead to multiple periods of unproductive capital across all CowSwap-based farms.
Recommendation
Consider to make the cooldown duration dynamic based on current conditions or remove the constraint altogether to avoid delays in capital deployment.
InfiniFi: Acknowledged.
Cantina: Acknowledged by InfiniFi team.
CowSwap approval logic may overwrite allowances in future extensions
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The CowSwap-integrated farms rely on
forceApproveto authorize token transfers by the CowSwap relayer. This approach unconditionally sets the approval amount, overwriting any existing allowance. While this is not currently an issue in the code, as approvals are not interleaved or shared across multiple flows, it could become problematic if the cooldown restriction is removed or relaxed and multiple approvals are performed more frequently or concurrently.Specifically, in
CoWSwapFarmBase, the_checkSwapApproveAndSignOrderfunction callsforceApprovefor USDC before initiating a swap. If deposits arrive at high frequency and swaps are triggered more dynamically, repeated calls toforceApprovecould lead to race conditions or overwrite approval amounts unexpectedly.Although not currently exploitable, using
safeIncreaseAllowancewould follow a more defensive pattern, especially when integrating with third-party protocols. It avoids unintentional overwrites and provides more granular control over allowance changes.The use of
forceApproveis also present inPendleInfinifiSIUSDduring interactions with Pendle contracts. While appropriate in those cases due to the deterministic nature of approvals before a one-time interaction, it's worth reviewing all occurrences for consistency and future extensibility.Recommendation
Consider to replace
forceApprovewithsafeIncreaseAllowancewhere possible to follow best practices and avoid allowance overwriting in future changes that introduce higher-frequency or multi-party interactions.InfiniFi: Acknowledged.
Cantina: Acknowledged by InfiniFi team.
Consider swapping USDC to USDe before staking to optimize for solver fees
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
In the current implementation of
EthenaFarm, thesignSwapOrderfunction consistently performs swaps from USDC to sUSDe via CowSwap, as seen in the integration tests. While this flow is functional and aligns with the intended behavior, it may not always be optimal in a production environment.Specifically, CowSwap solvers may charge higher fees when sourcing sUSDe directly from USDC, particularly in situations where sUSDe has lower available liquidity or fragmented pools. This could reduce the total net asset value (NAV) of the farm due to increased slippage or fee spread.
An alternative and potentially more cost-effective route would be to first swap USDC to USDe (which typically has deeper liquidity), and then stake the USDe into sUSDe. This would avoid excessive solver premiums and preserve more value for users.
The
EthenaFarmarchitecture appears to support this adjusted flow without requiring structural changes, making it a viable improvement path.Recommendation
Consider to perform swaps from USDC to USDe before staking into sUSDe in production deployments. This may reduce solver fees and improve the effective NAV of the farm, especially under volatile or high-volume conditions.
InfiniFi: Acknowledged.
Cantina: Acknowledged by InfiniFi team.