Organization
- @infinifi
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
Medium Risk
3 findings
3 fixed
0 acknowledged
Low Risk
3 findings
3 fixed
0 acknowledged
Informational
18 findings
8 fixed
10 acknowledged
Medium Risk3 findings
Merkl claim sweeps entire farm balance
State
Severity
- Severity: Medium
Submitted by
r0bert
Description
MerklRewardsClaimer.claimRewardscallsCoreControlled(_farm).emergencyActionto pull rewards into the farm and then immediately transfersIERC20(_rewardToken).balanceOf(_farm)to the recipient. The code never nets out the farm’s pre-existing balance in that token, so any funds already parked on the farm, such as principal or idle liquidity, are swept along with the newly claimed rewards. Because_rewardTokenis only checked againstenabledRewards, a misconfiguration or malicious reward distribution that happens to use the farm’s asset token will cause the next claim to drain the entire Farm's assets to therecipient, collapsing the reportedtotalAssets.Recommendation
Record the farm’s reward token balance before calling
emergencyAction, subtract it from the post-claim balance and only transfer the incremental amount. Additionally forbid_rewardTokenfrom equalling the farm’s primary asset unless that behaviour is explicitly intended.Withdraw slippage guard misapplied
State
Severity
- Severity: Medium
Submitted by
r0bert
Description
Farm.withdrawrecords how much value vanished from the farm (assetsSpent = assetsBefore - assetsAfter) but the slippage guard compares the caller suppliedamountto the tolerance. Imagine the manager asks to withdraw 100 units, yet the downstream venue only returns 50 due to fees.assetsSpentstill reads 100 because the farm’s assets dropped by that figure.minAssetsOuttherefore equals ~100, and the guard seesamount >= minAssetsOut(100 >= 100) and happily approves the withdrawal, even though the recipient only got 50. The check never inspects what was actually delivered, so any routing error or fee spike can burn value far beyondmaxSlippagewhile the contract reports success.Recommendation
Track the recipient’s balance change (or the farm’s
assets()delta) and compare that to the tolerance, e.g.require(assetsSpent.mulWadDown(maxSlippage) <= actualAmountDelivered)so the guard enforces what was actually withdrawn, not just the requested amount.assets() undercounts NAV when ERC-7540 requests transition to claimable state
State
Severity
- Severity: Medium
Submitted by
slowfi
Description
The function
assetsfrom contractERC7540Farmcomputes the farm’s NAV without accounting for ERC-7540 request states that have become claimable. In ERC-7540 vaults, a depositor’s balance can sit in several states: pending deposit request, pending redeem request, and (after settlement) claimable deposit/redeem. WhenrequestId <= lastDepositEpochIdSettled(or the equivalent for redeems), the view helperspendingDepositRequest/pendingRedeemRequestreturn0, while the value has moved to claimable. As a result, the current logic may omit amounts that are past settlement but not yet claimed, leading to NAV lower than expected and inconsistent reporting immediately before users can deposit/redeem again.This is particularly relevant for the Plasma
fxSaveintegration the farm targets, but the behavior is inherent to any ERC-7540 vault that follows these state transitions.Recommendation
Consider to include claimable amounts in the NAV computation alongside any still-pending requests:
- Query both
pendingDepositRequest/pendingRedeemRequestandclaimableDepositRequest/claimableRedeemRequestfor the farm’s address and add all non-zero amounts to NAV. - Guard for the epoch transition edge case where
requestId <= last{Deposit/Redeem}EpochIdSettled: if pending returns0, check the correspondingclaimable*view and include it. - Document that NAV includes unclaimed-but-claimable balances so reporting stays consistent across settlement boundaries.
- Add unit tests for the boundaries: (a) right before settlement, (b) immediately after settlement (claimable but not claimed), and (c) after claim.
- If gas cost of extra calls is a concern, consider to cache the latest observed
requestIdand only callclaimable*whenrequestIdhas crossed the last settled epoch.
Low Risk3 findings
Chainlink oracle uses deprecated latestAnswer call
State
Severity
- Severity: Low
Submitted by
r0bert
Description
ChainlinkOracle.price()still invokeslatestAnswer()on the feed contract. Chainlink has deprecated this getter in favour oflatestRoundData()(see https://docs.chain.link/data-feeds/api-reference#latestanswer), so the current implementation skips the completeness and freshness checks that Chainlink now exposes through round metadata. If the feed is answered with an outdated or incomplete round, the oracle will still return the stale value and propagate it to accounting, potentially causing mispriced conversions.Recommendation
Consider switching to the
latestRoundData()function.ERC7540 farm uses raw ERC20.transfer function
State
Severity
- Severity: Low
Submitted by
r0bert
Description
ERC7540Farm._withdrawforwards funds with a plainERC20(assetToken).transfer(_to, _amount). Some ERC20s returnfalseon failure and others (notably legacy tokens such as USDT in Ethereum mainnet) return no boolean at all. The current code expects a boolean result and reverts whenever the callee omits the return value, despite the transfer having succeeded. UsingSafeERC20.safeTransferhandles both silent success and boolean returns, keeping behaviour aligned with the rest of the codebase.Recommendation
Replace the manual
transfercall withIERC20(assetToken).safeTransfer(_to, _amount)from OpenZeppelin’sSafeERC20, which gracefully handles missing orfalsereturn values.Missing staleness/heartbeat validation for Chainlink prices
State
Severity
- Severity: Low
Submitted by
slowfi
Description
The function
pricefrom contractChainlinkOraclereturns the feed value without verifying data freshness against the feed’s heartbeat. Chainlink feeds define a maximum interval between updates; if no update occurs within that interval, the price should be treated as stale. The current logic does not checkupdatedAt(or any equivalent timestamp) against a configured max age, nor does it provide an alternative failure mode. As a result, downstream consumers may operate using stale prices when the feed stops updating.Recommendation
Consider to retrieve round data (e.g., via
latestRoundData) and enforce freshness before returning:- Compare
updatedAtto a configurablemaxDelayaligned with each feed’s heartbeat; if exceeded, revert or emit aStalePrice(feed, updatedAt)event depending on the desired behavior. - Make
maxDelayconfigurable per feed or per deployment so it can match the official heartbeat. - Document the chosen failure mode (revert vs. emit-and-continue) and apply it consistently across oracle reads.
Documentation reference (Feeds time official source): https://data.chain.link/feeds
Informational18 findings
ERC7540 pending requests ignore potential fees
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
ERC7540Farm.assets()books the raw numbers returned byIERC7540(vault).pendingDepositRequestandpendingRedeemRequestalongside the live share balance. Those getters return the face value that was staged before settlement, but the underlyingVaultcharges management/performance fees during each settlement via_updateTotalAssetsAndTakeFees. That helper runs_takeFees, minting manager/protocol shares against the same asset base and lowering the post-settlement price per share before pending deposits are converted or pending redeems are cashed out. The farm therefore continues to report the pre-fee figures until the batch is claimed, overstating NAV relative to the amount the vault will actually credit when the requests are finally executed.Recommendation
Merely informative. We have checked the
_PFXSAVEVault(https://plasmascan.to/address/0x5f264836CE02496CcF55D7F9AA5Cb34e34319DB5/contract/9745/readProxyContract), and the management fee is set to 0 while the performance fee is set to 10%:[feeRates method Response]managementRate tuple : 0performanceRate : 1000ERC7540 farm requires vault whitelist
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
ERC7540Farm.vaultRequestDepositcallsIERC7540(vault).requestDeposit(_assets, address(this), address(this)). The HopperVaultimplementation insists that theownerbe whitelisted beforerequestDepositsucceeds, so the farm controller must be registered on the vault’s whitelist or deposits revert. If governance forgets to whitelist the farm (or the whitelist is toggled mid-flight), the farm cannot queue deposits even though core code appears configured, leaving funds idle.Recommendation
Document the dependency and bake it into your deployment checks.
Pendle swap allows same-token path
State
Severity
- Severity: Informational
Submitted by
r0bert
Description
PendleSYFarm.swapaccepts_tokenInand_tokenOutwithout enforcing that they differ. If the caller supplies the same token, the function still routes throughsy.depositandsy.redeem, triggering superfluous approvals and slippage checks. With the defaultmaxSlippage = 0.999e18, a no-op conversion can still haircut the position by 10 bps becauseminTokenOutis scaled down before the redemption guard. At best this wastes gas; at worst, integration mistakes could repeatedly erode principal while appearing to “swap” nothing.Recommendation
Add
require(_tokenIn != _tokenOut, InvalidToken(_tokenIn))(or a dedicated error) at the top ofswapso identical assets exit early without incurring Pendle conversions or slippage tolerances.Track SY token lists for potential updates
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
r0bert
Description
PendleSYFarmsnapshotssy.getTokensIn()/getTokensOut()in the constructor and mirrors those assets into its own allow-list. The Pendle SY implementation is upgradeable and the adapter can change its supported tokens at runtime, so the farm’s whitelist can silently fall out of sync. When Pendle adds or removes tokens, swaps revert as there is no oracle/asset support in thePendleSYFarm.Recommendation
Run an off-chain monitor that polls
sy.getTokensIn()andgetTokensOut(), compares the results to the farm’s configured assets and triggersenableAssets/disableAssetsplus Accounting oracle updates whenever they diverge.Add input token allow-list check
State
Severity
- Severity: Informational
Submitted by
r0bert
Description
PendleSYFarm.swaponly validates_tokenOutwithisAssetSupported, leaving_tokenInunchecked. Pendle SY will revert on unsupported inputs, so the call is safe, but operators get a Pendle revert instead of a farm-level error. A directisAssetSupported(_tokenIn)would surface misconfigurations sooner.Recommendation
Add the same allow-list guard for
_tokenInso unsupported inputs fail fast with a clear farm error rather than bubbling the SY revert.Allow zero-amount swaps in the PendleSYFarm
State
Severity
- Severity: Informational
Submitted by
r0bert
Description
PendleSYFarm.swaprejects_amountIn == 0viarequire(_amountIn > 0, InvalidAmountIn(_amountIn)). Allowing_amountIn == 0and simply skipping the deposit leg would let callers perform a redemption safely without having to perform any deposit in case that it was needed because, for some reason, the farm had received SY tokens directly.Recommendation
Remove the strict
> 0requirement and wrap the SYpreviewDeposit&depositinteractions in an if code block so zero-amount calls bypass thedepositflow:if(_amountIn > 0){ // swap into SY IERC20(_tokenIn).forceApprove(address(sy), _amountIn); uint256 minSyOut = sy.previewDeposit(_tokenIn, _amountIn).mulWadDown(maxSlippage); sy.deposit{value: 0}(address(this), _tokenIn, _amountIn, minSyOut);}Minor docstring typos
State
Severity
- Severity: Informational
Submitted by
r0bert
Description
There are a couple of spelling slips in the farm comments. In
PendleSYFarmthe header note says “use them to performcovnersions” instead of “conversions”. InAutoFarmthe withdraw docstring says “hassignficantless slippage”.Recommendation
Touch up the comments to read “conversions” and “significant” so the documentation looks polished.
Unprotected large withdrawals can incur high slippage
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
_withdrawfrom contractAutoFarmredeems shares directly against the vault with no slippage guardrails:uint256 share = ERC4626(vault).convertToShares(_amount);ERC4626(vault).redeem(share, _to, address(this));If the requested withdrawal exceeds the vault’s idle liquidity, the vault may unwind positions and route through DEX liquidity, which can introduce significant slippage (as described in the AutoFarm docs). The current implementation provides no mechanism to bound or mitigate this impact for large withdrawals.
Recommendation
Consider to implement a “large-withdrawal” path similar to the approach outlined in the docs and the Tokemak example, enabling safer execution under specific scenarios:
- Use a staged/streamed unwind or TWAP-like execution to reduce market impact.
- Quote expected proceeds (e.g., via
previewRedeem) and enforce user-supplied bounds such asminOut/maxSlippageBps, reverting when unmet. - Split the withdrawal into tranches with per-tranche slippage limits, or route through an external executor that can batch/cross-route with better price discovery.
- Offer an async/queued withdrawal mode that allows strategies to raise liquidity before final settlement.
References: AutoFarm docs, Tokemak example.
Slippage tolerance misconfigured to 1 bp instead of intended 10 bps
State
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
constructorfrom contractAutoFarmsetsmaxSlippage = 0.9999e18. With 1e18 fixed-point scaling, this equals 0.9999, i.e., an allowed slippage of 1 bp (0.01%). It was mentioned the target was 10 bps (0.10%). A 1 bp tolerance is extremely tight and can make routine withdrawals fail due to minor price movements or fees, causing unnecessary reverts under normal market conditions.Recommendation
Consider to:
- Set
maxSlippageto0.999e18to allow 10 bps tolerance if that matches the intended policy. - Replace the floating multiplier with an explicit bps parameter (e.g.,
uint256 maxSlippageBps) and compute the multiplier on use to avoid confusion. - Make
maxSlippageconfigurable (governance/owner with events) to adapt per deployment or vault behavior. - Add unit tests that cover typical withdrawal paths and verify that expected minor deviations do not revert under the configured tolerance.
Zero pendingRequestId calls are ambiguous across ERC-7540 implementations
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
assetsfrom contractERC7540Farmcalls:IERC7540(vault).pendingDepositRequest(pendingDepositRequestId, address(this));IERC7540(vault).pendingRedeemRequest(pendingRedeemRequestId, address(this));
without guarding against the case where
pendingDepositRequestId == 0orpendingRedeemRequestId == 0. Different ERC-7540 integrations interpret a zero request id differently (e.g., some return the last pending request, others the first, and some may treat it as an invalid sentinel). Since this behavior is not standardized, invoking the views with a zero id can yield inconsistent results and misreport the farm’s NAV.Recommendation
Consider to:
- Add an explicit check: if a request id is
0, skip the correspondingpending*Requestcall or resolve the correct id first via a source of truth (e.g., track the latest created ids in the farm or query vault-provided “current/last request id” views when available). - Normalize the behavior in code: define and enforce a local convention (e.g., “id 0 means no request”) and document it.
- Where applicable, use dedicated vault getters (e.g., “lastPendingDepositId”, “lastPendingRedeemId”) before querying
pending*Request, and only call the view if the resolved id is non-zero. - Add tests against multiple ERC-7540 vault mocks that emulate differing zero-id semantics to ensure consistent NAV accounting.
Unnecessary ERC-20 approvals in deposit/redeem flows
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The functions handling deposits and redemptions from contract
ERC7540Farmcall:- At line 107:
IERC20(assetToken).forceApprove(vault, _assets)(deposit path), and - At line 137:
IERC20(share).forceApprove(vault, _shares)(redeem path).
If the ERC-7540 vault functions used by the farm do not actually pull tokens via
transferFrom(e.g., they burn shares frommsg.senderand/or accept assets already transferred in), these approvals are redundant. Keeping allowances to the vault also increases the attack surface (lingering spend rights if the vault is upgraded/compromised) and adds gas overhead due toforceApprove’s write pattern.Recommendation
Consider to remove token approvals from both the deposit and redeem paths when the vault API does not require them. If an allowance is strictly necessary for a specific call, consider to:
- Use a “safe spend” pattern: only top-up allowance when insufficient, prefer
approve(0)thenapprove(amount)for non-standard ERC-20s, and reset back to0after use. - Prefer
permit/Permit2 when supported to avoid persistent allowances. - Where possible, transfer assets in first (farm → vault) and call the vault method that consumes
msg.senderbalance, or use the variant that burns shares withouttransferFrom(owner)so no allowance is needed.
This reduces gas and avoids maintaining unnecessary standing approvals.
vaultDeposit and redeem flows do not account for partial submissions/settlements
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
vaultDepositfrom contractERC7540Farmallows submitting a deposit request amount_assets, but on Plasma’s fxSave (and, in general, ERC-7540 flows) the subsequent submission step can accept less than the originally requested amount. It can also be called again to continue filling the remaining amount, and callingrequestDepositagain will aggregate with previously requested-but-unsubmitted amounts. The same behavior applies to redeem requests.If the farm logic assumes a 1:1 relationship between requested and submitted amounts, or assumes a single-shot settlement, the internal accounting and NAV can become inconsistent around these edges (e.g., part of the request remains pending/claimable while the farm assumes it is fully settled).
Recommendation
Consider to make the deposit and redeem paths explicitly partial-fill aware:
- Track, per request id, the requested, submitted, and remaining amounts; update internal state on each partial submission rather than assuming full fill.
- When (re)submitting, compute the delta to submit from on-chain views (pending and/or claimable), not from the original requested amount.
- Only advance/close the tracked request id after confirming the remaining amount is zero (fully settled), otherwise keep it open for subsequent submissions.
- Emit events on partial fills (e.g.,
DepositSubmitted(requestId, submitted, remaining)/RedeemSubmitted(...)) to aid monitoring and retries. - Add tests covering: (a) partial submission < requested, (b) multiple submissions accumulating to full, (c) re-calling
requestDeposit/requestRedeemthat aggregates with prior amounts, and (d) the analogous redeem scenarios. - Document the behavior so integrators know they can submit in tranches and that the farm will reconcile state across multiple submissions.
Allowance not reset to zero after approve in HopperVault path
State
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
constructorfrom contractHopperVaultFarmperforms a token approval usingforceApprove(see line 28), but unlike other sections of the codebase, it does not reset the allowance back to zero after use. Keeping a standing allowance to the vault/adapter increases the attack surface (unexpected spend if the spender is upgraded/compromised) and is inconsistent with the safer pattern used elsewhere in the project where approvals are cleared after the operation.Recommendation
Consider to:
- Set the allowance back to zero after the operation completes (mirror the pattern used in other files), e.g., call
IERC20(token).forceApprove(spender, 0)once the spend is no longer required. - Only increase allowance immediately before the call that requires it and for the exact amount needed.
- Prefer
permit/Permit2 when supported to avoid persistent approvals altogether. - Add tests that assert post-call allowances are zeroed.
Missing cancel path for same-epoch deposit requests can strand funds when settlement is blocked
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
syncDepositfrom contractHopperVaultFarmintegrates with a vault that supports synchronous and ERC-7540-style asynchronous deposits, but it does not expose a way to cancel a pending deposit request during the same epoch. In the ERC-7540 implementation you provided,cancelRequestDeposit()is explicitly designed for same-epoch rollbacks: it refunds the requested assets from the vault’spendingSiloto the requester if settlement cannot proceed this epoch. Without exposing this path at the farm level, funds can remain stuck pending when:- The vault becomes paused/closing before settlement.
- External router/liquidity steps needed for settlement are unavailable this epoch.
- An operator/input error (wrong amount/receiver/referral) is detected after requesting.
- Caps/policy changes mid-epoch make the route temporarily non-executable.
Recommendation
Consider to expose a guarded cancel function and reconcile accounting:
- Add
cancelVaultDeposit()(e.g.,whenNotPaused,onlyCoreRole(CoreRoles.FARM_SWAP_CALLER)or appropriate role) that calls the vault’scancelRequestDeposit()and emits aDepositCanceled(requestId, refundedAssets)event. - On cancel, recompute any tracked pending amounts/ids and ensure NAV reflects removal of the pending deposit (i.e., exclude amounts that were in
pendingSilo). - Gate the call so it’s only used within the same epoch (mirror the vault’s revert semantics) and provide clear revert reasons in events/logs for operators.
- Add tests for: (a) cancel in same epoch (success), (b) attempt after epoch boundary (revert), (c) cancel when vault paused mid-epoch (success), (d) cancel after operator input mistake then re-request.
Infinifi: The farm is only intended to do synchronous deposits. Although the team is aware that this path may revert under some scenarios it is not considered necessary to implement a cancellable path given the designed uses cases.
Cantina Managed: Acknowledged by the Infinifi team.
Unused imports in AutoFarm and MerklRewardsClaimer
State
Severity
- Severity: Informational
Submitted by
slowfi
Description
The files declare imports that are never referenced in code:
AutoFarm.sol:8—import {CoreRoles} from "@libraries/CoreRoles.sol";(unused)MerklRewardsClaimer.sol:7—import {FarmRegistry} from "@integrations/FarmRegistry.sol";(unused)
Leaving unused imports increases maintenance noise, can mask refactor mistakes, and may marginally affect compile time/bytecode metadata. It also contradicts typical linting rules and can hide accidental dependency creep.
Recommendation
Consider to:
- Remove both unused imports.
- Add a lint/check in CI to fail on unused imports (e.g.,
solhintwithno-unused-imports, or Foundry’sforge build -vvv+ a simple script that greps compiler warnings). - As a practice, prefer importing only where symbols are referenced, and prune imports during refactors.
No administrative resync for pending ERC-7540 request ids can leave the farm stuck
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The state variables
pendingDepositRequestIdandpendingRedeemRequestIdfrom contractERC7540Farmtrack the current async requests, but there is no guarded way to manually resynchronize them if an ERC-7540 vault rejects a deposit/redeem or otherwise skips/invalidates a request id. Some async vaults can reject or drop requests outside of the standard, which may leave the farm pointing at a stale/nonexistent id and, in turn, misaccounting pending/claimable amounts or blocking subsequent submissions.Recommendation
Consider to introduce a narrowly scoped, access-controlled resync path:
- Add an admin/guardian function (e.g.,
resyncPendingRequestIds(uint256 depositId, uint256 redeemId)) to updatependingDepositRequestIdandpendingRedeemRequestIdwhen an async request is rejected or skipped. - Gate with strong controls: only callable by governance/guardian, optionally behind a timelock or when
paused, and emit events (PendingIdsResynced(oldDepositId, newDepositId, oldRedeemId, newRedeemId)). - Add sanity checks against vault state (e.g., verify the provided ids are ≥ last settled ids, or query that they exist/are pending) and require reconciliation of any associated accounting (e.g., pending vs. claimable deltas).
- Document the operational runbook for when to use this function, and add tests covering rejected/invalidated request scenarios and subsequent successful submissions.
Redundant payment on deposit call
State
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
<function_name>from contractPendleSYFarmcalls:sy.deposit{value: 0}(address(this), _tokenIn, _amountIn, minSyOut);Passing an explicit zero
msg.valueis unnecessary and slightly increases bytecode/gas. It can also be misleading by suggesting ETH is conditionally forwarded when this path is strictly ERC-20 based.Recommendation
Consider to remove the value specifier and rely on the default zero:
sy.deposit(address(this), _tokenIn, _amountIn, minSyOut);If a native-ETH deposit path is ever introduced, gate it behind a separate branch that forwards non-zero
msg.valuewith explicit checks and events.Mixing prior SY balance with current redeem can violate caller’s minTokenOut bound
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
slowfi
Description
The function
swapfrom contractPendleSYFarmredeems allSYheld by the contract:uint256 syBalance = sy.balanceOf(address(this));sy.redeem(address(this), syBalance, _tokenOut, minTokenOut, false);The
syBalancemay include leftovers from previous operations. In that case, the caller’sminTokenOut—which is presumably calibrated for the_amountIn → SY → _tokenOutroundtrip of the current call—no longer matches the redeemed amount. This can result in:- Redeeming more SY than intended, making the provided
minTokenOuttoo loose for the larger redemption and potentially less profitable than expected; or - A mismatch where
minTokenOutis too tight relative to the actual per-unit rate, causing avoidable reverts.
Recommendation
Consider to redeem only the
SYminted by the current operation and alignminTokenOutto that amount:- Delta accounting: Snapshot
preSY = sy.balanceOf(address(this))beforedeposit, then computesyMinted = sy.balanceOf(address(this)) - preSYand redeem exactlysyMinted. - Return value usage (if available): If
sy.deposit(...)returns thesyOutamount, use that value directly in the subsequentredeem. - State hygiene: Alternatively, require
sy.balanceOf(address(this)) == 0at function entry (or sweep/settle leftovers) to avoid mixing flows. - Bound correctness: Ensure
minTokenOutreflects only the SY redeemed in this call (e.g., scale the bound bysyMintedif the input is expressed per unit). - Events/telemetry: Emit events with
syMintedandsyRedeemedto help off-chain builders set accurateminTokenOutfor the actual redeemed amount.
These adjustments keep slippage protections meaningful and prevent cross-contamination between historical dust and the current swap.