Skip to content

[VPD-613] Venus Pendle PT Fixed Rate Vault#53

Open
Debugger022 wants to merge 57 commits intodevelopfrom
feat/vpd-613
Open

[VPD-613] Venus Pendle PT Fixed Rate Vault#53
Debugger022 wants to merge 57 commits intodevelopfrom
feat/vpd-613

Conversation

@Debugger022
Copy link
Contributor

Summary

  • Introduces PendlePTVaultAdapter, a universal upgradeable adapter that wraps Pendle PT swaps and Venus Core deposit/redeem into single transactions
  • Users deposit tokens (slisBNB, WBNB, or native BNB) and receive Venus vTokens backed by Pendle PT, enabling fixed-rate yield exposure through Venus
  • Comprehensive BSC mainnet fork test suite covering all contract flows (deposit, withdraw, redeem, admin, views)
  • Includes deployment script with proxy + automatic BSCScan verification

Architecture

DEPOSIT: tokenIn → Pendle Router (swapExactTokenForPt) → PT → Venus (mintBehalf) → vTokens to user
WITHDRAW: vTokens → Venus (redeemBehalf) → PT → Pendle Router (swapExactPtForToken) → tokenOut to user
REDEEM: vTokens → Venus (redeemBehalf) → PT → Pendle Router (redeemPyToToken, 1:1) → tokenOut to user

Key Design Decisions

  • Stateless — no user accounting; all positions tracked by Venus vTokens
  • Any-token input — accepts any token Pendle supports via TokenInput.tokenIn
  • On-chain derivationaddMarket() derives PT/SY/YT/maturity from Pendle market and comptroller from vToken to prevent misconfiguration
  • Upgrade-safeAccessControlledV8 + PausableUpgradeable + ReentrancyGuardUpgradeable with uint256[48] __gap

Contracts

File Purpose
PendlePTVaultAdapter.sol Main adapter (deposit, depositNative, withdraw, redeemAtMaturity, admin)
IPendlePTVaultAdapter.sol Interface with structs, events, errors, function signatures
IVenusVToken.sol Minimal VToken interface (mintBehalf, redeemBehalf)
IVenusComptroller.sol Minimal Comptroller interface (approvedDelegates)
AggregatorMock.sol Test-only KyberSwap mock for post-maturity fork tests
IPancakeRouterV2.sol PancakeSwap V2 interface used by AggregatorMock

Deployment

File Purpose
deploy/019-deploy-pendle-pt-vault-adapter.ts Deploys implementation + TransparentUpgradeableProxy, calls initialize(accessControlManager)
  • Constructor args: PENDLE_ROUTER, COMPTROLLER (immutables)
  • Proxy owner: NormalTimelock on mainnet, deployer on hardhat/testnet
  • addMarket() calls done separately via governance

Test Coverage

Fork tests pinned at BSC block 83040087 with modular test structure:

Module Cases Coverage
admin.spec.ts 18 addMarket, pause/unpause, sweepTokens, sweepNative
deposit.spec.ts 24 ERC-20 (slisBNB direct + WBNB aggregator), native BNB, post-maturity revert, error cases
withdraw.spec.ts 11 slisBNB (direct), WBNB (aggregator), native BNB output, error cases
redeemAtMaturity.spec.ts 11 slisBNB (direct), WBNB (aggregator via AggregatorMock), native BNB (aggregator via AggregatorMock), error cases
viewFunctions.spec.ts 11 Config queries, delegation, immutables

AggregatorMock (test infrastructure)

Post-maturity tests require ~1.5yr time travel, which expires KyberSwap aggregator calldata (embedded DEX deadlines + executor signatures). AggregatorMock is deployed via hardhat_setCode at the KyberSwap router address to bypass this — it performs a direct PancakeSwap V2 swap instead.

Dependencies Added

  • @pendle/core-v2 — Pendle Router interfaces and type definitions
  • axios (devDependency) — Pendle API integration for fetching swap parameters in tests

Test plan

  • All admin functions (addMarket, pause, sweep) with success + error cases
  • ERC-20 deposit via direct and aggregator-routed paths
  • Native BNB deposit with refund handling
  • Pre-maturity withdraw to 3 output token types (slisBNB, WBNB, native BNB)
  • Post-maturity 1:1 redemption via direct redeemSy path (slisBNB)
  • Post-maturity redemption via WBNB (aggregator path — via AggregatorMock)
  • Post-maturity redemption via native BNB (aggregator path — via AggregatorMock)
  • View function queries and immutable state verification
  • Adapter statelessness verification (zero balances after every operation)
  • Run full fork test suite: FORKED_NETWORK=bscmainnet npx hardhat test tests/hardhat/Fork/pendlePTVaultAdapter/index.spec.ts

…VaultAdapter contract

- Added IPendlePTVaultAdapter interface to define the contract's structure and functions.
- Refactored PendlePTVaultAdapter to implement the new interface, ensuring compliance with the defined methods.
- Removed redundant comments and organized code for better readability.
- Updated error handling and event emissions to align with the new interface specifications.
- add WBNB aggregator path coverage in PendlePTVaultAdapter fork test
- enhance Pendle API to support aggregator routing for token swaps
- Extract inline PT→token swap logic from withdraw() into a dedicated
  _swapPtToToken() internal function for consistency
- Change all private helper functions to internal for uniform visibility
- Added withdraw functionality via adapter with support for direct and aggregator-routed paths.
- Implemented error handling for withdraw cases including zero amounts and market status checks.
- Introduced redeemAtMaturity functionality with support for direct redemption and aggregator routing.
- Enhanced tests for both withdraw and redeemAtMaturity to cover various scenarios and edge cases.
- add related api calls
- Derive comptroller from vToken in addMarket (removes manual param,
  eliminates misconfiguration risk) and validate vToken underlying
  matches the derived PT address
- Add idempotency guards to activateMarket/deactivateMarket to prevent
  redundant state transitions
- Add sweepNative admin function to recover native BNB sent to the
  contract (complements existing sweepTokens for ERC-20)
- Remove unused IWBNB interface and enterMarketBehalf from
  IVenusComptroller; add comptroller() view to IVenusVToken
- Reorganize contract sections (CORE — DEPOSIT / WITHDRAW & REDEEM)
  and improve NatSpec documentation throughout
Extract the monolithic test file into a modular folder structure under
pendlePTVaultAdapter/ with dedicated spec files for each concern:

- admin.spec.ts: addMarket, de/activate, pause, sweep, ownership
- deposit.spec.ts: ERC-20 deposits (direct mintSy + aggregator) and
  native BNB deposits with refund handling
- withdraw.spec.ts: pre-maturity withdrawals via Pendle AMM sell
- redeemAtMaturity.spec.ts: post-maturity PT redemption via SY.redeem
- viewFunctions.spec.ts: getMarketConfig, getAllMarkets, isMatured, etc.

Shared utilities split into constants, fixtures (baseFixture,
depositedFixture), helpers (oracle tolerance, slisBNB minting, dummy
params), and pendleApi (swap/redeem param fetching from Pendle API).

index.spec.ts acts as a single entry point with one forking() call to
avoid multiple hardhat_reset invocations that would invalidate
loadFixture snapshots across spec files.
…leanup

    - Add complete redeemAtMaturity fork tests (slisBNB, WBNB, native BNB output)
    - Add AggregatorMock to bypass KyberSwap executor signature verification
      in fork tests (deployed via hardhat_setCode at router address)
    - Extract IPancakeRouterV2 interface into separate file (one-contract-per-file)
    - Fix function ordering and gas-strict-inequalities linter warnings in adapter
    - Replace magic numbers with BSC_CHAIN_ID constant across all test files
    - Rename userClisbnbBefore/After to userSlisbnbBefore/After for consistency
    - Fix misleading test comments (tokensOut/tokensIn → accurate Pendle terminology)
    - Remove unused imports and add missing NatSpec documentation
Balance delta pattern for incoming token transfers:
- deposit(): replaced pre-transfer validation (input.netTokenIn == amount)
  with post-transfer balance delta (balanceAfter - balanceBefore). The
  actual received amount is validated against input.netTokenIn, correctly
  handling fee-on-transfer tokens.
- withdraw() / redeemAtMaturity(): use balance delta around _redeemVTokens
  (ptBefore/ptAfter) to capture only the PT received from the current
  redeem, rather than reading the full contract balance which could
  include pre-existing tokens from accidental transfers.
- _swapToPt(): simplified to approve input.netTokenIn directly instead
  of reading balanceOf, since deposit() now validates the delta matches.

PT dust sweep after Venus mint:
- deposit() and depositNative() now sweep PT dust back to the user after
  _mintVTokens, in case Venus mint rounds down. Previously only tokenIn
  dust was swept in deposit(), and no dust sweep existed in depositNative().

Why _sweepDust itself was not changed:
- _sweepDust transfers OUT (empties the adapter's balance to the user).
  Balance delta is needed for INCOMING transfers to know exactly how much
  was received. For outgoing transfers, balanceOf correctly represents
  what remains to send — after safeTransfer the adapter holds zero.
  Accidental direct transfers of significant value are handled separately
  by the admin sweepTokens/sweepNative rescue functions.
…d fix fork tests

Contract changes (PendlePTVaultAdapter.sol):
- _sweepDust and _refundNativeDust now use balance-delta pattern (snapshot
  before vs current) instead of sweeping the entire adapter balance. This
  prevents pre-existing token balances from leaking to the next caller.
- Added PT dust sweep after swap/redeem in withdraw() and redeemAtMaturity()
  for parity with deposit flows — any unconsumed PT is returned to the user.
- Wrapped deposit() body in a scoping block to fix "Stack too deep" caused
  by the additional ptBalanceBefore local variable.

Event redesign (IPendlePTVaultAdapter.sol):
- Indexed addresses users will filter by (pendleMarket, user, tokenIn/tokenOut)
  instead of uint256 ptAmount which is rarely used as a filter key.
- Reordered Withdrawn and RedeemedAtMaturity params to group indexed fields
  first for readability.

Test fixes:
- deposit.spec.ts: added missing approve() before the InputAmountMismatch
  error test so safeTransferFrom succeeds and the mismatch check is reached.
- pendleApi.ts: strip signed limit order fills (normalFills/flashFills) from
  Pendle API responses in getPendlePtToTokenParams. Live market-maker
  signatures are invalid on forked chains ("LOP: bad signature") because
  the order nonces don't exist at the forked block. Pure AMM routing is
  used instead, which works reliably on forks.
@Debugger022 Debugger022 self-assigned this Mar 3, 2026
@Debugger022 Debugger022 marked this pull request as ready for review March 3, 2026 06:41
Debugger022 and others added 15 commits March 3, 2026 20:14
…duce SLOADs

- Cache config.pt, config.vToken, and config.yt into local stack variables
  in deposit, depositNative, withdraw, and redeemAtMaturity to avoid
  redundant warm SLOADs (~97 gas saved per eliminated SLOAD)
- Extract _swapToPtNative internal helper from depositNative to resolve
  stack-too-deep compilation error caused by additional local variables
  sharing the stack frame with the inline swapExactTokenForPt call
- No behavioral changes; all existing tests pass unchanged
…in deposit flows

- A malicious limitRouter can inflate the netPtOut return value from swapExactTokenForPt, allowing an attacker to mint vTokens using pre-existing PT balance in the adapter
- Compute actual PT received via balance delta (balanceAfter - balanceBefore) instead of trusting swapExactTokenForPt return value
- Apply fix to both deposit and depositNative flows
- Remove unused return values from _swapToPt and _swapToPtNative
- Pendle Router still enforces minPtOut internally for slippage protection
- Pendle's epsSkipMarket threshold can send residual SY to the adapter
  instead of routing it through the AMM during limit order fills
- Add _sweepDust for SY token in both deposit and depositNative flows
- Use config.sy directly (no local variable) to avoid stack-too-deep
[VPD-744] Venus Pendle PT Fixed Rate Vault Hashdit Audit
[VPD-738] Venus Pendle PT Fixed Rate Vault Certik Audit
Add proxy and implementation deployment records for the
PendlePTVaultAdapter on BSC Mainnet.
@github-actions
Copy link

Code Coverage

Package Line Rate Branch Rate Health
contracts 100% 100%
contracts.DeviationSentinel 88% 86%
contracts.DeviationSentinel.Oracles 93% 90%
contracts.Interfaces 100% 100%
contracts.LeverageManager 94% 80%
contracts.Libraries 29% 33%
contracts.PositionSwapper 0% 0%
contracts.SwapHelper 100% 100%
contracts.SwapRouter 79% 55%
contracts.pendle-pt-fixed-rate-vault 0% 0%
contracts.pendle-pt-fixed-rate-vault.interfaces 100% 100%
contracts.pendle-pt-fixed-rate-vault.test 0% 0%
Summary 61% (569 / 933) 47% (261 / 558)

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