Skip to content

Feature/osu 1505 yieldfowarder with swaps#388

Open
skimaharvey wants to merge 10 commits intofeature/osu-1490-yield-forwardedfrom
feature/osu-1505-yieldfowarder-with-swaps
Open

Feature/osu 1505 yieldfowarder with swaps#388
skimaharvey wants to merge 10 commits intofeature/osu-1490-yield-forwardedfrom
feature/osu-1505-yieldfowarder-with-swaps

Conversation

@skimaharvey
Copy link
Contributor

@skimaharvey skimaharvey commented Feb 17, 2026

Summary

Adds a pluggable swap layer to the yield forwarding pipeline so that strategy profits can be automatically converted to a different target asset (e.g., USDC -> USDT, USDC -> USDS) before being forwarded to the receiver.

Core contracts

  • SwappingYieldForwarder — Inherits YieldForwarder and adds reportSwapAndForward(), which redeems to self, swaps via a pluggable ISwapper, then forwards the target asset. The inherited reportAndForward() remains available as a no-swap circuit breaker.
  • ISwapper — Minimal interface (swap(tokenIn, tokenOut, amountIn, minAmountOut, receiver)) for stateless swap adapters.

Swap adapters

Adapter Route Notes
PSMSwapper Sky PSM (SELL_GEM, BUY_GEM) and DaiUsds converter (DAI<->USDS) Handles PSM fee calculation (tout), 4 routes in one contract
CurveSwapper Curve StableSwap exchange() Balance-measurement approach for legacy pools that don't return output amount
UniswapV3SwapperAdapter Uniswap V3 single-hop and multi-hop Immutable routing config with optional base token for two-hop paths (e.g., USDC -> WETH -> DAI)
UniswapV4SwapperAdapter Uniswap V4 via PoolManager Supports zeroForOne direction config with BalanceDelta extraction for output measurement

Design decisions

  • Fully immutable: All contracts are immutable with no admin functions, upgrades, or sweep. Assets can only flow to the hardcoded receiver.
  • Dual-mode forwarding: Keeper chooses swap vs. no-swap at call-time. If the swap protocol is down, the keeper falls back to reportAndForward().
  • Push-based ISwapper: Caller transfers tokens to the swapper before calling swap(). This keeps adapters stateless and composable.
  • Strategy as call-time parameter: Avoids circular deployment dependencies between strategy and forwarder.

@linear
Copy link

linear bot commented Feb 17, 2026

@skimaharvey skimaharvey changed the base branch from develop to feature/osu-1490-yield-forwarded February 17, 2026 16:47
@github-actions
Copy link

Code Coverage Report for src/ files

File % Lines % Statements % Branches % Funcs
src/core/BaseStrategy.sol ✅ 100.00% (54/54) ✅ 100.00% (39/39) ✅ 100.00% (2/2) ✅ 100.00% (19/19)
src/core/MultistrategyLockedVault.sol ✅ 100.00% (117/117) ✅ 100.00% (121/121) ✅ 100.00% (22/22) ✅ 100.00% (18/18)
src/core/MultistrategyVault.sol ✅ 100.00% (593/593) ✅ 100.00% (617/617) ✅ 98.49% (196/199) ✅ 100.00% (88/88)
src/core/PaymentSplitter.sol ✅ 100.00% (55/55) ✅ 100.00% (52/52) ✅ 100.00% (18/18) ✅ 100.00% (16/16)
src/core/Privileged.sol ✅ 100.00% (13/13) ✅ 100.00% (12/12) ✅ 100.00% (0/0) ✅ 100.00% (4/4)
src/core/SwappingYieldForwarder.sol ✅ 100.00% (16/16) ✅ 100.00% (20/20) ✅ 100.00% (3/3) ✅ 100.00% (2/2)
src/core/TokenizedStrategy.sol ✅ 99.35% (308/310) ✅ 99.64% (275/276) ✅ 95.60% (87/91) ✅ 98.81% (83/84)
src/core/YieldForwarder.sol ✅ 100.00% (12/12) ✅ 100.00% (14/14) ✅ 100.00% (3/3) ✅ 100.00% (2/2)
src/core/libs/DebtManagementLib.sol ✅ 100.00% (76/76) ✅ 100.00% (80/80) ✅ 100.00% (20/20) ✅ 100.00% (2/2)
src/core/libs/ERC20SafeApproveLib.sol ✅ 100.00% (4/4) ✅ 100.00% (5/5) ✅ 100.00% (1/1) ✅ 100.00% (1/1)
src/factories/AaveV3StrategyFactory.sol ✅ 100.00% (13/13) ✅ 100.00% (19/19) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/factories/AddressSetFactory.sol ✅ 100.00% (15/15) ✅ 100.00% (15/15) ✅ 100.00% (0/0) ✅ 100.00% (4/4)
src/factories/BaseERC4626StrategyFactory.sol ✅ 100.00% (15/15) ✅ 100.00% (19/19) ✅ 100.00% (0/0) ✅ 100.00% (3/3)
src/factories/BaseStrategyFactory.sol ✅ 100.00% (14/14) ✅ 100.00% (14/14) ✅ 100.00% (1/1) ✅ 100.00% (4/4)
src/factories/ERC4626StrategyFactory.sol ✅ 100.00% (2/2) ✅ 100.00% (1/1) ✅ 100.00% (0/0) ✅ 100.00% (1/1)
src/factories/LidoStrategyFactory.sol ✅ 100.00% (12/12) ✅ 100.00% (17/17) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/factories/MorphoCompounderStrategyFactory.sol ✅ 100.00% (13/13) ✅ 100.00% (19/19) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/factories/MultistrategyVaultFactory.sol ✅ 100.00% (74/74) ✅ 100.00% (64/64) ✅ 100.00% (31/31) ✅ 100.00% (18/18)
src/factories/PaymentSplitterFactory.sol ✅ 100.00% (58/58) ✅ 100.00% (69/69) ✅ 100.00% (22/22) ✅ 100.00% (10/10)
src/factories/RegenEarningPowerCalculatorFactory.sol ✅ 100.00% (14/14) ✅ 100.00% (14/14) ✅ 100.00% (0/0) ✅ 100.00% (4/4)
src/factories/RegenStakerFactory.sol ✅ 100.00% (32/32) ✅ 100.00% (29/29) ✅ 100.00% (2/2) ✅ 100.00% (10/10)
src/factories/SkyCompounderStrategyFactory.sol ✅ 100.00% (12/12) ✅ 100.00% (17/17) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/factories/SparkStrategyFactory.sol ✅ 100.00% (2/2) ✅ 100.00% (1/1) ✅ 100.00% (0/0) ✅ 100.00% (1/1)
src/factories/YieldForwarderFactory.sol ✅ 100.00% (19/19) ✅ 100.00% (21/21) ✅ 100.00% (1/1) ✅ 100.00% (5/5)
src/factories/yieldDonating/YearnV3StrategyFactory.sol ✅ 100.00% (12/12) ✅ 100.00% (16/16) ✅ 100.00% (0/0) ✅ 100.00% (2/2)
src/factories/yieldSkimming/RocketPoolStrategyFactory.sol ✅ 100.00% (12/12) ✅ 100.00% (17/17) ✅ 100.00% (2/2) ✅ 100.00% (2/2)
src/guards/KeeperBotGuard.sol ✅ 100.00% (32/32) ✅ 100.00% (30/30) ✅ 100.00% (8/8) ✅ 100.00% (6/6)
src/mechanisms/AllocationMechanismFactory.sol ✅ 100.00% (26/26) ✅ 100.00% (26/26) ✅ 100.00% (3/3) ✅ 100.00% (6/6)
src/mechanisms/BaseAllocationMechanism.sol ✅ 97.01% (65/67) ✅ 97.18% (69/71) ✅ 100.00% (7/7) ✅ 95.83% (23/24)
src/mechanisms/TokenizedAllocationMechanism.sol ✅ 99.76% (422/423) ✅ 99.57% (466/468) ✅ 95.38% (124/130) ✅ 100.00% (81/81)
src/mechanisms/mechanism/OctantQFMechanism.sol ✅ 100.00% (37/37) ✅ 100.00% (37/37) ✅ 100.00% (14/14) ✅ 100.00% (7/7)
src/mechanisms/mechanism/QuadraticVotingMechanism.sol ✅ 100.00% (69/69) ✅ 100.00% (87/87) ✅ 100.00% (17/17) ✅ 100.00% (17/17)
src/mechanisms/voting-strategy/ProperQF.sol ✅ 100.00% (85/85) ✅ 100.00% (104/104) ✅ 100.00% (13/13) ✅ 100.00% (15/15)
src/regen/RegenEarningPowerCalculator.sol ✅ 100.00% (35/35) ✅ 100.00% (31/31) ✅ 100.00% (6/6) ✅ 100.00% (8/8)
src/regen/RegenStaker.sol ✅ 100.00% (16/16) ✅ 100.00% (14/14) ✅ 100.00% (1/1) ✅ 100.00% (5/5)
src/regen/RegenStakerBase.sol ✅ 99.59% (242/243) ✅ 99.59% (243/244) ✅ 97.33% (73/75) ✅ 100.00% (34/34)
src/regen/RegenStakerWithoutDelegateSurrogateVotes.sol ✅ 100.00% (20/20) ✅ 100.00% (17/17) ✅ 100.00% (4/4) ✅ 100.00% (5/5)
src/strategies/periphery/BaseHealthCheck.sol ✅ 100.00% (32/32) ✅ 100.00% (24/24) ✅ 100.00% (14/14) ✅ 100.00% (9/9)
src/strategies/periphery/BaseYieldSkimmingHealthCheck.sol ✅ 100.00% (41/41) ✅ 100.00% (39/39) ✅ 100.00% (18/18) ✅ 100.00% (10/10)
src/strategies/periphery/UniswapV3Swapper.sol ✅ 100.00% (23/23) ✅ 100.00% (30/30) ✅ 100.00% (7/7) ✅ 100.00% (4/4)
src/strategies/yieldDonating/AaveV3Strategy.sol ✅ 100.00% (39/39) ✅ 100.00% (48/48) ✅ 100.00% (9/9) ✅ 100.00% (7/7)
src/strategies/yieldDonating/ERC4626Strategy.sol ✅ 100.00% (26/26) ✅ 100.00% (30/30) ✅ 100.00% (2/2) ✅ 100.00% (7/7)
src/strategies/yieldDonating/MorphoCompounderStrategy.sol ✅ 100.00% (26/26) ✅ 100.00% (30/30) ✅ 100.00% (2/2) ✅ 100.00% (7/7)
src/strategies/yieldDonating/PrivilegedYieldDonatingTokenizedStrategy.sol ✅ 100.00% (16/16) ✅ 100.00% (14/14) ✅ 100.00% (4/4) ✅ 100.00% (6/6)
src/strategies/yieldDonating/SkyCompounderStrategy.sol ✅ 100.00% (88/88) ✅ 100.00% (77/77) ✅ 100.00% (24/24) ✅ 100.00% (19/19)
src/strategies/yieldDonating/SparkStrategy.sol ✅ 100.00% (8/8) ✅ 100.00% (9/9) ✅ 100.00% (6/6) ✅ 100.00% (1/1)
src/strategies/yieldDonating/YearnV3Strategy.sol ✅ 100.00% (26/26) ✅ 100.00% (30/30) ✅ 100.00% (2/2) ✅ 100.00% (7/7)
src/strategies/yieldDonating/YieldDonatingTokenizedStrategy.sol ✅ 100.00% (23/23) ✅ 100.00% (26/26) ✅ 100.00% (5/5) ✅ 100.00% (2/2)
src/strategies/yieldSkimming/BaseYieldSkimmingStrategy.sol ✅ 100.00% (8/8) ✅ 100.00% (5/5) ✅ 100.00% (0/0) ✅ 100.00% (5/5)
src/strategies/yieldSkimming/LidoStrategy.sol ✅ 100.00% (4/4) ✅ 100.00% (3/3) ✅ 100.00% (0/0) ✅ 100.00% (2/2)
src/strategies/yieldSkimming/PrivilegedYieldSkimmingTokenizedStrategy.sol ✅ 100.00% (16/16) ✅ 100.00% (14/14) ✅ 100.00% (4/4) ✅ 100.00% (6/6)
src/strategies/yieldSkimming/RocketPoolStrategy.sol ✅ 100.00% (4/4) ✅ 100.00% (3/3) ✅ 100.00% (0/0) ✅ 100.00% (2/2)
src/strategies/yieldSkimming/YieldSkimmingTokenizedStrategy.sol ✅ 99.61% (254/255) ✅ 99.39% (326/328) ✅ 96.43% (81/84) ✅ 100.00% (28/28)
src/swappers/CurveSwapper.sol ✅ 100.00% (15/15) 🔴 85.71% (18/21) 🔴 0.00% (0/3) ✅ 100.00% (2/2)
src/swappers/PSMSwapper.sol 🔴 46.34% (19/41) 🔴 44.68% (21/47) 🔴 10.00% (1/10) 🔴 50.00% (3/6)
src/swappers/UniswapV3SwapperAdapter.sol ✅ 100.00% (14/14) 🔴 90.91% (20/22) 🔴 50.00% (2/4) ✅ 100.00% (2/2)
src/utils/AddressSet.sol ✅ 100.00% (34/34) ✅ 100.00% (34/34) ✅ 100.00% (12/12) ✅ 100.00% (7/7)
src/zodiac-core/LinearAllowanceExecutor.sol ✅ 100.00% (24/24) ✅ 100.00% (24/24) ✅ 100.00% (7/7) ✅ 100.00% (7/7)
src/zodiac-core/modules/LinearAllowanceSingletonForGnosisSafe.sol ✅ 100.00% (91/91) ✅ 100.00% (100/100) ✅ 95.24% (20/21) ✅ 100.00% (16/16)

@skimaharvey skimaharvey marked this pull request as ready for review February 18, 2026 13:42
@skimaharvey skimaharvey force-pushed the feature/osu-1505-yieldfowarder-with-swaps branch from f23e242 to c2837a3 Compare February 18, 2026 13:43
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f23e2423e9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@0xferit
Copy link
Contributor

0xferit commented Mar 3, 2026

Review finding: silent share burning in SwappingYieldForwarder.reportSwapAndForward()

Severity: Low
File: src/core/SwappingYieldForwarder.sol, lines 138-139

Issue

When redeem() returns 0 assets for non-zero shares, the function exits early without emitting any event:

uint256 assetsIn = IRedeemable(strategy).redeem(shares, address(this), address(this), maxLoss);
if (assetsIn == 0) return 0;

The shares have already been burned inside TokenizedStrategy._withdraw() at this point, so value is irreversibly lost with no on-chain trace in the forwarder's event log.

The parent YieldForwarder.reportAndForward() always emits YieldForwarded after redeem(), even when assets == 0. The child contract breaks this observability guarantee.

When this can happen

The scenario requires all three conditions:

  1. maxLoss == MAX_BPS (10,000 = 100% loss tolerance): this is the standard pattern used in every test and integration
  2. freeFunds() completely fails (e.g., illiquid yield source, Aave pool at 100% utilization, locked Morpho market)
  3. idle == 0 (all strategy assets are deployed)

Under these conditions, TokenizedStrategy._withdraw() burns the shares, transfers 0 tokens, and returns 0 without reverting because the maxLoss < MAX_BPS guard at line 1111 is bypassed.

Impact

Not exploitable (keeper-gated, only donation/profit shares at risk, not user principal). The TokenizedStrategy.Withdraw event still fires with assets=0, so there is some chain trace, just not in the forwarder's event namespace. The concern is operational: off-chain monitoring watching forwarder events would miss this failure mode entirely.

Suggested fix

Emit the event before the early return to maintain parity with the parent contract:

uint256 assetsIn = IRedeemable(strategy).redeem(shares, address(this), address(this), maxLoss);
if (assetsIn == 0) {
    emit YieldSwappedAndForwarded(strategy, receiver, shares, 0, 0);
    return 0;
}

This preserves the early return (avoids calling the swapper with 0 amount) while giving monitoring full visibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants