@@ -63,6 +63,9 @@ contract PendlePtStrategyFacet {
6363 /// zero TWAP duration.
6464 error PendleInvalidOracle ();
6565
66+ /// @notice Thrown when a configured slippage tolerance exceeds 100%.
67+ error PendleInvalidSlippage (uint16 bps );
68+
6669 // -----------------------------------------------------------------------
6770 // Events
6871 // -----------------------------------------------------------------------
@@ -73,13 +76,22 @@ contract PendlePtStrategyFacet {
7376 /// @notice Emitted when the mark-to-market oracle is set (or cleared).
7477 event PendleOracleSet (address indexed oracle , uint32 twapDuration );
7578
79+ /// @notice Emitted when the max AMM slippage tolerance is set.
80+ event PendleSlippageSet (uint16 maxSlippageBps );
81+
7682 // -----------------------------------------------------------------------
7783 // Storage
7884 // -----------------------------------------------------------------------
7985
8086 /// @dev erc7201:vaultrouter.strategy.pendle
8187 bytes32 internal constant PENDLE_STORAGE_SLOT = 0xb0e016db49ce2cfbe35770c2200cbf5f1a9b502bca57dbaaddf328cb9e0cef00 ;
8288
89+ /// @dev Basis-points denominator.
90+ uint16 internal constant PENDLE_BPS = 10_000 ;
91+
92+ /// @dev Slippage tolerance (bps) used when none is explicitly configured: 1%.
93+ uint16 internal constant DEFAULT_MAX_SLIPPAGE_BPS = 100 ;
94+
8395 struct PendleStorage {
8496 /// @notice PendleRouterV4 — handles all swap and redemption paths.
8597 IPendleRouter router;
@@ -92,6 +104,9 @@ contract PendlePtStrategyFacet {
92104 IPYLpOracle oracle;
93105 /// @notice TWAP window (seconds) passed to the oracle.
94106 uint32 twapDuration;
107+ /// @notice Max AMM slippage tolerance (bps) applied against the oracle
108+ /// mark when deriving minOut for swaps. Zero => 1% default.
109+ uint16 maxSlippageBps;
95110 }
96111
97112 function _ps () internal pure returns (PendleStorage storage s ) {
@@ -146,6 +161,24 @@ contract PendlePtStrategyFacet {
146161 emit PendleOracleSet (address (oracle), twapDuration);
147162 }
148163
164+ /// @notice Set the maximum AMM slippage tolerance (bps) for pre-maturity
165+ /// swaps, applied against the oracle mark when deriving minOut.
166+ /// @dev Owner-gated. Only enforceable when an oracle is configured — without
167+ /// an on-chain mark there is no reference to bound the swap against.
168+ /// @param bps Tolerance in basis points (<= 10_000). Zero selects the 1% default.
169+ function pendleSetSlippage (uint16 bps ) external {
170+ LibDiamond.enforceIsContractOwner ();
171+ if (bps > PENDLE_BPS) revert PendleInvalidSlippage (bps);
172+ _ps ().maxSlippageBps = bps;
173+ emit PendleSlippageSet (bps);
174+ }
175+
176+ /// @dev Effective slippage tolerance: the configured value, or the 1% default.
177+ function _maxSlippageBps (PendleStorage storage s ) private view returns (uint16 ) {
178+ uint16 bps = s.maxSlippageBps;
179+ return bps == 0 ? DEFAULT_MAX_SLIPPAGE_BPS : bps;
180+ }
181+
149182 // -----------------------------------------------------------------------
150183 // Strategy surface (pendle* prefix)
151184 // -----------------------------------------------------------------------
@@ -209,20 +242,32 @@ contract PendlePtStrategyFacet {
209242 // Empty limit order, strategy does not participate in the limit book.
210243 IPendleRouter.LimitOrderData memory limit;
211244
212- uint256 ptBefore = s.pt.balanceOf (address (this ));
245+ // Derive an on-chain minimum from the oracle mark when available: invert
246+ // the PT->asset rate to value `amount` in PT, then haircut by the
247+ // slippage tolerance. The router reverts if it cannot meet minPtOut.
248+ // Without an oracle there is no mark to bound against, so we fall back to
249+ // 0 (unchanged behaviour for unpriced markets) and rely on the post-call
250+ // zero check.
251+ uint256 minPtOut;
252+ if (address (s.oracle) != address (0 )) {
253+ uint256 rate = s.oracle.getPtToAssetRate (s.market, s.twapDuration);
254+ if (rate > 0 ) {
255+ uint256 expectedPt = amount * 1e18 / rate;
256+ minPtOut = expectedPt * (PENDLE_BPS - _maxSlippageBps (s)) / PENDLE_BPS;
257+ }
258+ }
213259
214- s.router
260+ ( uint256 netPtOut ,,) = s.router
215261 .swapExactTokenForPt (
216262 address (this ), // PT receiver is the vault itself
217263 s.market,
218- 0 , // minPtOut: checked post-call below
264+ minPtOut,
219265 approx,
220266 input,
221267 limit
222268 );
223269
224- uint256 ptReceived = s.pt.balanceOf (address (this )) - ptBefore;
225- if (ptReceived == 0 ) revert PendleDepositFailed (ptReceived);
270+ if (netPtOut == 0 ) revert PendleDepositFailed (netPtOut);
226271 }
227272
228273 /// @notice Return `amount` of underlying from the Pendle position to the vault.
@@ -242,10 +287,11 @@ contract PendlePtStrategyFacet {
242287 if (amount > ptBalance) revert PendleInsufficientPt (amount, ptBalance);
243288
244289 IERC20 underlying = IERC20 (IERC4626 (address (this )).asset ());
245- uint256 underlyingBefore = underlying.balanceOf (address (this ));
246290
247291 IERC20 (address (s.pt)).forceApprove (address (s.router), amount);
248292
293+ uint256 received;
294+
249295 if (s.pt.isExpired ()) {
250296 // Post-maturity: PT redeems 1:1. minTokenOut = 99% (dust tolerance).
251297 IPendleRouter.TokenOutput memory output = IPendleRouter.TokenOutput ({
@@ -259,13 +305,24 @@ contract PendlePtStrategyFacet {
259305 });
260306
261307 // redeemPyToToken burns PT (YT is implicitly 0 post-maturity).
262- s.router.redeemPyToToken (address (this ), s.pt.YT (), amount, output);
308+ (received,) = s.router.redeemPyToToken (address (this ), s.pt.YT (), amount, output);
263309 } else {
264- // Pre-maturity: sell PT on the Pendle AMM.
265- // minTokenOut = 0 here; slippage is validated post-call.
310+ // Pre-maturity: sell PT on the Pendle AMM. Derive minTokenOut from the
311+ // oracle mark when available (PT->asset rate, haircut by slippage) so
312+ // the router itself enforces the bound; fall back to 0 only when no
313+ // oracle is configured (unchanged behaviour for unpriced markets).
314+ uint256 minTokenOut;
315+ if (address (s.oracle) != address (0 )) {
316+ uint256 rate = s.oracle.getPtToAssetRate (s.market, s.twapDuration);
317+ if (rate > 0 ) {
318+ uint256 expected = amount * rate / 1e18 ;
319+ minTokenOut = expected * (PENDLE_BPS - _maxSlippageBps (s)) / PENDLE_BPS;
320+ }
321+ }
322+
266323 IPendleRouter.TokenOutput memory output = IPendleRouter.TokenOutput ({
267324 tokenOut: address (underlying),
268- minTokenOut: 0 ,
325+ minTokenOut: minTokenOut ,
269326 tokenRedeemSy: address (underlying),
270327 pendleSwap: address (0 ),
271328 swapData: IPendleRouter.SwapData ({
@@ -275,10 +332,9 @@ contract PendlePtStrategyFacet {
275332
276333 IPendleRouter.LimitOrderData memory limit;
277334
278- s.router.swapExactPtForToken (address (this ), s.market, amount, output, limit);
335+ (received,,) = s.router.swapExactPtForToken (address (this ), s.market, amount, output, limit);
279336 }
280337
281- uint256 received = underlying.balanceOf (address (this )) - underlyingBefore;
282338 if (received == 0 ) revert PendleWithdrawFailed (amount, received);
283339 }
284340
0 commit comments