Skip to content

Commit

Permalink
feat: add PT redemption functions to pendle adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
Van0k committed Aug 17, 2024
1 parent 1fb56f0 commit e620d83
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 5 deletions.
95 changes: 94 additions & 1 deletion contracts/adapters/pendle/PendleRouterAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
IPendleRouter,
IPendleMarket,
IYToken,
SwapData,
SwapType,
TokenInput,
Expand All @@ -39,6 +40,9 @@ contract PendleRouterAdapter is AbstractAdapter, IPendleRouterAdapter {
/// @notice Mapping from (market, tokenIn, pendleToken) to whether swaps are allowed, and which directions
mapping(address => mapping(address => mapping(address => PendleStatus))) public isPairAllowed;

/// @notice Mapping from (tokenOut, pendleToken) to whether redemption after expiry is allowed
mapping(address => mapping(address => bool)) public isRedemptionAllowed;

/// @notice Mapping from PT token to its canonical market
mapping(address => address) public ptToMarket;

Expand Down Expand Up @@ -194,7 +198,7 @@ contract PendleRouterAdapter is AbstractAdapter, IPendleRouterAdapter {
/// @notice Swaps the entire balance of PT into input token, except the specified amount
/// @param market Address of the market to swap in
/// @param leftoverPt Amount of PT to leave on the Credit Account
/// @param diffOutput Input token parameters
/// @param diffOutput Output token parameters:
/// * `tokenOut` - token to swap PT into
/// * `minRateRAY` - minimal exchange rate of PT into the output token
function swapDiffPtForToken(address market, uint256 leftoverPt, TokenDiffOutput calldata diffOutput)
Expand Down Expand Up @@ -232,6 +236,93 @@ contract PendleRouterAdapter is AbstractAdapter, IPendleRouterAdapter {
);
}

/// @notice Redeems a specified amount of PT tokens into underlying after expiry
/// @param yt YT token associated to PT
/// @param netPyIn Amount of PT token to redeem
/// @param output Output token params:
/// * `tokenOut` - token to swap PT into
/// * `minTokenOut` - the minimal amount of token to receive
/// * `tokenRedeemSy` - token received after redeeming SY. Since the adapter does not use PendleRouter's external routing features,
/// this is always enforced to be equal to `tokenOut`
/// * `pendleSwap` - address of the swap aggregator. Since the adapter does not use PendleRouter's external routing features,
/// this is always enforced to be `address(0)`.
/// * `swapData` - off-chain data for external routing. Since the adapter does not use PendleRouter's external routing features,
/// this is always enforced to be an empty struct
/// @notice `receiver` is ignored, since the recipient is always the Credit Account
/// @notice Before expiry PT redemption also spends a corresponding amount of YT. To avoid the CA interacting
/// with potentially non-collateral YT tokens, this function is only executable after expiry
function redeemPyToToken(address, address yt, uint256 netPyIn, TokenOutput calldata output)
external
creditFacadeOnly
returns (uint256 tokensToEnable, uint256 tokensToDisable)
{
address pt = IYToken(yt).PT();

if (!isRedemptionAllowed[output.tokenOut][pt] || IYToken(yt).expiry() > block.timestamp) {
revert RedemptionNotAllowedException();
}

address creditAccount = _creditAccount();

TokenOutput memory output_m;

{
output_m.tokenOut = output.tokenOut;
output_m.tokenRedeemSy = output.tokenOut;
output_m.minTokenOut = output.minTokenOut;
}

(tokensToEnable, tokensToDisable,) = _executeSwapSafeApprove(
pt,
output_m.tokenOut,
abi.encodeCall(IPendleRouter.redeemPyToToken, (creditAccount, yt, netPyIn, output_m)),
false
);
}
/// @notice Redeems the entire balance of PT token into underlying after expiry, except the specified amount
/// @param yt YT token associated to PT
/// @param leftoverPt Amount of PT to keep on the account
/// @param diffOutput Output token parameters:
/// * `tokenOut` - token to swap PT into
/// * `minRateRAY` - minimal exchange rate of PT into the output token
/// @notice Before expiry PT redemption also spends a corresponding amount of YT. To avoid the CA interacting
/// with potentially non-collateral YT tokens, this function is only executable after expiry

function redeemDiffPyToToken(address yt, uint256 leftoverPt, TokenDiffOutput calldata diffOutput)
external
creditFacadeOnly
returns (uint256 tokensToEnable, uint256 tokensToDisable)
{
address pt = IYToken(yt).PT();

if (!isRedemptionAllowed[diffOutput.tokenOut][pt] || IYToken(yt).expiry() > block.timestamp) {
revert RedemptionNotAllowedException();
}

address creditAccount = _creditAccount();

uint256 amount = IERC20(pt).balanceOf(creditAccount);
if (amount <= leftoverPt) return (0, 0);

unchecked {
amount -= leftoverPt;
}

TokenOutput memory output;
output.tokenOut = diffOutput.tokenOut;
output.minTokenOut = amount * diffOutput.minRateRAY / RAY;
output.tokenRedeemSy = diffOutput.tokenOut;

LimitOrderData memory limit;

(tokensToEnable, tokensToDisable,) = _executeSwapSafeApprove(
pt,
diffOutput.tokenOut,
abi.encodeCall(IPendleRouter.redeemPyToToken, (creditAccount, yt, amount, output)),
leftoverPt <= 1
);
}

// ------------- //
// CONFIGURATION //
// ------------- //
Expand Down Expand Up @@ -263,8 +354,10 @@ contract PendleRouterAdapter is AbstractAdapter, IPendleRouterAdapter {
bytes32 pairHash = keccak256(abi.encode(pairs[i].market, pairs[i].inputToken, pairs[i].pendleToken));
if (pairs[i].status != PendleStatus.NOT_ALLOWED) {
_allowedPairHashes.add(pairHash);
isRedemptionAllowed[pairs[i].inputToken][pairs[i].pendleToken] = true;
} else {
_allowedPairHashes.remove(pairHash);
isRedemptionAllowed[pairs[i].inputToken][pairs[i].pendleToken] = false;
}
_hashToPendlePair[pairHash] = pairs[i];

Expand Down
14 changes: 14 additions & 0 deletions contracts/integrations/pendle/IPendleRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,22 @@ interface IPendleRouter {
TokenOutput calldata output,
LimitOrderData calldata limit
) external returns (uint256 netTokenOut, uint256 netSyFee, uint256 netSyInterm);

function redeemPyToToken(address receiver, address YT, uint256 netPyIn, TokenOutput calldata output)
external
returns (uint256 netTokenOut, uint256 netSyInterm);
}

interface IPendleMarket {
function readTokens() external view returns (address sy, address pt, address yt);
}

interface IYToken {
function PT() external view returns (address);

function expiry() external view returns (uint256);
}

interface IPToken {
function YT() external view returns (address);
}
11 changes: 11 additions & 0 deletions contracts/interfaces/pendle/IPendleRouterAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ interface IPendleRouterAdapterEvents {
interface IPendleRouterAdapterExceptions {
/// @notice Thrown when a pair is not allowed for swapping (direction-sensitive)
error PairNotAllowedException();

/// @notice Thrown when a pair is not allowed for PT to token redemption after expiry
error RedemptionNotAllowedException();
}

/// @title PendleRouter adapter interface
Expand Down Expand Up @@ -71,6 +74,14 @@ interface IPendleRouterAdapter is IAdapter, IPendleRouterAdapterEvents, IPendleR
external
returns (uint256 tokensToEnable, uint256 tokensToDisable);

function redeemPyToToken(address receiver, address yt, uint256 netPyIn, TokenOutput calldata output)
external
returns (uint256 tokensToEnable, uint256 tokensToDisable);

function redeemDiffPyToToken(address yt, uint256 leftoverPt, TokenDiffOutput calldata output)
external
returns (uint256 tokensToEnable, uint256 tokensToDisable);

// ------------- //
// CONFIGURATION //
// ------------- //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {AdapterType} from "@gearbox-protocol/sdk-gov/contracts/AdapterType.sol";
import {
IPendleRouter,
IPendleMarket,
IPToken,
IYToken,
SwapData,
SwapType,
TokenInput,
Expand Down Expand Up @@ -45,11 +47,13 @@ contract Live_PendleRouterAdapterTest is LiveTestHelper {

BalanceComparator comparator;

string[4] stages = [
string[6] stages = [
"after_swapExactTokenForPt",
"after_swapDiffTokenForPt",
"after_swapExactPtForToken",
"after_swapDiffPtForToken"
"after_swapDiffPtForToken",
"after_redeemPyToToken",
"after_redeemDiffPyToToken"
];

string[] _stages;
Expand Down Expand Up @@ -151,6 +155,19 @@ contract Live_PendleRouterAdapterTest is LiveTestHelper {
creditAccount, MultiCallBuilder.build(router.swapDiffPtForToken(pair.market, 10 * baseUnit, diffOutput))
);
comparator.takeSnapshot("after_swapDiffPtForToken", creditAccount);

address yt = IPToken(pair.pendleToken).YT();
vm.warp(IYToken(yt).expiry() + 1); // Move time forward to ensure expiry

creditFacade.multicall(
creditAccount, MultiCallBuilder.build(router.redeemPyToToken(creditAccount, yt, 3 * baseUnit, output))
);
comparator.takeSnapshot("after_redeemPyToToken", creditAccount);

creditFacade.multicall(
creditAccount, MultiCallBuilder.build(router.redeemDiffPyToToken(yt, 3 * baseUnit, diffOutput))
);
comparator.takeSnapshot("after_redeemDiffPyToToken", creditAccount);
} else {
IPendleRouter router = IPendleRouter(routerAddress);

Expand Down Expand Up @@ -212,6 +229,17 @@ contract Live_PendleRouterAdapterTest is LiveTestHelper {
uint256 leftoverPt = 10 * baseUnit;
router.swapExactPtForToken(creditAccount, pair.market, ptBalance - leftoverPt, output, lod);
comparator.takeSnapshot("after_swapDiffPtForToken", creditAccount);

address yt = IPToken(pair.pendleToken).YT();
vm.warp(IYToken(yt).expiry() + 1); // Move time forward to ensure expiry

router.redeemPyToToken(creditAccount, yt, 3 * baseUnit, output);
comparator.takeSnapshot("after_redeemPyToToken", creditAccount);

ptBalance = IERC20(pair.pendleToken).balanceOf(creditAccount);
leftoverPt = 3 * baseUnit;
router.redeemPyToToken(creditAccount, yt, ptBalance - leftoverPt, output);
comparator.takeSnapshot("after_redeemDiffPyToToken", creditAccount);
}

vm.stopPrank();
Expand Down
25 changes: 25 additions & 0 deletions contracts/test/multicall/pendle/PendleRouter_Calls.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,29 @@ library PendleRouter_Calls {
callData: abi.encodeCall(IPendleRouterAdapter.swapDiffPtForToken, (market, leftoverPt, diffOutput))
});
}

function redeemPyToToken(
PendleRouter_Multicaller c,
address receiver,
address yt,
uint256 netPyIn,
TokenOutput memory output
) internal pure returns (MultiCall memory) {
return MultiCall({
target: address(c),
callData: abi.encodeCall(IPendleRouterAdapter.redeemPyToToken, (receiver, yt, netPyIn, output))
});
}

function redeemDiffPyToToken(
PendleRouter_Multicaller c,
address yt,
uint256 leftoverPt,
TokenDiffOutput memory diffOutput
) internal pure returns (MultiCall memory) {
return MultiCall({
target: address(c),
callData: abi.encodeCall(IPendleRouterAdapter.redeemDiffPyToToken, (yt, leftoverPt, diffOutput))
});
}
}
Loading

0 comments on commit e620d83

Please sign in to comment.