Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
b8e6526
add CurveDolaLPHelperDynamic
0xtj24 Nov 22, 2024
4964e65
add DolasUSDe tests
0xtj24 Nov 22, 2024
93b65e5
simplify _addLiquidity for dynamic array
0xtj24 Nov 22, 2024
db08147
add DolasUSDs LP tests, add initial ALE test refactor
0xtj24 Nov 27, 2024
f5998d1
add DolascrvUSD LP tests
0xtj24 Nov 28, 2024
c218c88
update BorrowController test setup
0xtj24 Nov 28, 2024
07ad3f5
add sDOLA-scrvUSD markets tests
0xtj24 Dec 12, 2024
9c6ce4f
add ALE sDOLA LP helper
0xtj24 Dec 12, 2024
2f41fa5
add ALE tests for sDOLA-scrvUSD markets
0xtj24 Dec 12, 2024
ef665eb
update config file
0xtj24 Dec 12, 2024
abfaf46
use interface
0xtj24 Dec 12, 2024
16114e5
update natspec
0xtj24 Dec 12, 2024
cc2a55d
add YearnV2 deployed vault sDola-scrvUSD test, add fuzz tests
0xtj24 Dec 13, 2024
d671a3e
add initial PendlePTHelper and fork test
0xtj24 Dec 19, 2024
e6fed9d
Merge branch 'dev' into feat/pendle-pt-helper
0xtj24 Dec 19, 2024
4d6e58c
rm unused import
0xtj24 Dec 19, 2024
12c5327
update natspec
0xtj24 Dec 19, 2024
1646d74
update natspec
0xtj24 Dec 20, 2024
712fa6d
more natspec update
0xtj24 Dec 20, 2024
af6107c
fix typos
0xtj24 Dec 20, 2024
20e7b77
add safety check for stucked Pendle tokens
0xtj24 Dec 20, 2024
2cf4856
add Pendle PT helper fuzz tests
0xtj24 Dec 26, 2024
f646dce
rm unused import in test
0xtj24 Dec 26, 2024
419181f
add initial PendlePTHelper update, add ALEPendle, update tests
0xtj24 Jan 7, 2025
6a9df68
use selector check for PendlePTHelper, add tests
0xtj24 Jan 9, 2025
2f084d6
update natspec
0xtj24 Jan 9, 2025
f140324
store maturity for gas optimization
0xtj24 Jan 9, 2025
d1f67f9
add Events
0xtj24 Jan 10, 2025
b858075
update natspec
0xtj24 Jan 10, 2025
713b3e4
apply review feedback
0xtj24 Jan 15, 2025
e8611f5
add refundExcess internal function
0xtj24 Jan 15, 2025
1d87b03
add minOut check for buy and sell DBR
0xtj24 Jan 20, 2025
3cc6e03
revert if helper or buySellToken not set
0xtj24 Jan 20, 2025
b05b6f4
fix updateMarketHelper logic, add test
0xtj24 Jan 28, 2025
1c04da4
use BytesLib library in PendlePTHelper
0xtj24 Jan 30, 2025
fb836b0
add Dola-DeUSD market and ALE tests
0xtj24 Feb 13, 2025
ce13367
fix old test
0xtj24 Feb 13, 2025
7650d9f
add Feed and Escrow tests, update market test with CL feed
0xtj24 Feb 14, 2025
89bf3fb
fix typo
0xtj24 Feb 14, 2025
5dd5b66
rm spender from ALEPendle, update tests
0xtj24 Feb 28, 2025
3945ec0
introduce ALEV2, update ERC4626Helper and YVYCRVHelper, update tests
0xtj24 Mar 4, 2025
6c05ba2
update CurveDolaLPHelper and tests
0xtj24 Mar 6, 2025
5e7c94c
update CurveDolaLPHelperDynamic and tests
0xtj24 Mar 6, 2025
36180d4
Merge branch 'feat/sDOLA-scrvUSD' into feat/pendle-pt-helper
0xtj24 Mar 6, 2025
37268a7
update CurveSDolaLPHelperDynamic and tests
0xtj24 Mar 7, 2025
c085b9d
add USDe before maturity with NAV, add tests with NAV
0xtj24 Mar 12, 2025
d90491a
add maturity check and test
0xtj24 Mar 12, 2025
8c2c4ae
update natspec
0xtj24 Mar 12, 2025
49f8efb
add multiple proxy support for ALEV2, update tests
0xtj24 Mar 18, 2025
5625279
Merge branch 'dev' into feat/dola-deUsd
08xmt Mar 24, 2025
7a13fb8
Merge branch 'dev' into feat/pt-sude
08xmt Mar 24, 2025
aa97bd9
use PendleNAVFeed returning block.timestamp
0xtj24 Mar 27, 2025
8690cf0
Merge branch 'feat/pt-sude' of github.com-work_user2:InverseFinance/F…
0xtj24 Mar 27, 2025
c80d052
fix sdeUSD test
0xtj24 Mar 27, 2025
84c1f35
add test for staleness in Market tests
0xtj24 Mar 27, 2025
4640e8c
update staleness test for Markets
0xtj24 Mar 28, 2025
3c8163d
use deployed contracts for PendlePTsUSDe29May25
0xtj24 Mar 28, 2025
272e9c6
add initial ALEV2 and helpers addresses in tests
0xtj24 Apr 1, 2025
8a97b69
Merge branch 'feat/pt-sude' into feat/pendle-pt-helper
0xtj24 Apr 1, 2025
7ec0bcc
use latest block for SDola-scrvUSD tests, update ALEPendlePT tests
0xtj24 Apr 1, 2025
5c7d7ce
uncomment assertions
0xtj24 Apr 1, 2025
4110616
add ALE PT USDe 29May25 test
0xtj24 Apr 3, 2025
5455f32
Feat: Dola-USR markets (#112)
0xtj24 Apr 8, 2025
0e7c2d7
Merge branch 'feat/dola-deUsd' into feat/pendle-pt-helper
0xtj24 Apr 8, 2025
a18c1f6
update Dola-DeUSD and Dola-USR ALE tests
0xtj24 Apr 8, 2025
6b4f16c
update setMarket function
0xtj24 Apr 11, 2025
ebc76db
update tests with latest deployments
0xtj24 Apr 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 281 additions & 0 deletions src/util/PendlePTHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {IMarket} from "src/interfaces/IMarket.sol";
import {Sweepable, SafeERC20, IERC20} from "src/util/Sweepable.sol";
import {IMultiMarketTransformHelper} from "src/interfaces/IMultiMarketTransformHelper.sol";

/**
* @title Pendle PT ALE and market helper
* @notice This contract is a generalized ALE and market helper contract for Pendle PT tokens from and to DOLA.
* @dev Carefully prepare the router calldata from Pendle API when using it from the ALE:
* When converting TO collateral, the receiver in Pendle API has to be set to this contract address
* When converting FROM collateral, the receiver in Pendle API has to be set to the ALE address.
* The Pendle Router can either SWAP DOLA for PT or MINT PT and YT as well SWAP PT for DOLA or REDEEM PT (using YT before maturity) for DOLA.
* Do not use this contract for other routes otherwise won't work properly.
**/

contract PendlePTHelper is Sweepable, IMultiMarketTransformHelper {
using SafeERC20 for IERC20;

error InsufficientDOLA();
error InsufficientPT();
error InsufficientYT();
error MarketNotSet(address market);
error PendleSwapFailed();

struct PT {
address pt;
address yt;
}

event MarketSet(
address indexed market,
address indexed pt,
address indexed yt
);
event MarketRemoved(address indexed market);

IERC20 public immutable DOLA;
address public immutable router;

/// @notice Mapping of market addresses to their associated PT and YT tokens.
mapping(address => PT) public markets;

/** @dev Constructor
@param _gov The address of Inverse Finance governance
@param _guardian The address of the guardian
@param _pendleRouter The address of the Pendle Router
**/
constructor(
address _gov,
address _guardian,
address _dola,
address _pendleRouter
) Sweepable(_gov, _guardian) {
DOLA = IERC20(_dola);
router = _pendleRouter;
}

/**
* @notice Convert DOLA to PT or PT and YT
* @dev Used by the ALE but can be called by anyone. Carefully review input data for Pendle API.
* The receiver in Pendle API has to be set to this contract address. If a MINT is performed, provide a ytRecipient or YT will be kept in this contract.
* @param amount The amount of underlying token to be deposited.
* @param data Encoded address of the market, minimum amount of PT to receive (and possibly YT), ytRecipient if minting, and Pendle callData.
* @return collateralAmount The amount of PT (and possibly YT) token received.
*/
function transformToCollateral(
uint256 amount,
bytes calldata data
) external override returns (uint256 collateralAmount) {
collateralAmount = transformToCollateral(amount, msg.sender, data);
}

/**
* @notice Convert DOLA to PT or PT and YT
* @dev The receiver in Pendle API has to be set to this contract address. If a MINT is performed, provide a ytRecipient or YT will be kept in this contract.
* @param amount The amount of DOLA to be deposited.
* @param recipient The recipient address of PT token.
* @param data Encoded address of the market, minimum amount of PT to receive (and possibly YT), ytRecipient if minting, and Pendle callData.
* @return collateralAmount The amount of PT (and possibly YT) token received.
*/
function transformToCollateral(
uint256 amount,
address recipient,
bytes calldata data
) public override returns (uint256 collateralAmount) {
(
address market,
uint256 minMint, // Minimum amount of PT to receive (and possibly YT)
address ytRecipient,
bytes memory callData
) = abi.decode(data, (address, uint256, address, bytes));

_revertIfMarketNotSet(market);

IERC20 pt = IERC20(markets[market].pt);
IERC20 yt = IERC20(markets[market].yt);
DOLA.safeTransferFrom(msg.sender, address(this), amount);
DOLA.approve(router, amount);
// Avoid accounting for possibly stucked token for previous bad input and allow recovery
uint256 ptBalBefore = pt.balanceOf(address(this));
uint256 ytBalBefore = yt.balanceOf(address(this));
(bool success, ) = router.call(callData);
if (!success) revert PendleSwapFailed();

uint256 ptBal = pt.balanceOf(address(this)) - ptBalBefore;

if (ptBal < minMint) revert InsufficientPT();
if (recipient != address(this)) pt.safeTransfer(recipient, ptBal);
// Send YT to user if specified
if (ytRecipient != address(0)) {
uint256 ytBalMinted = yt.balanceOf(address(this)) - ytBalBefore;
if (ytBalMinted < minMint) revert InsufficientYT();
yt.safeTransfer(ytRecipient, ytBalMinted);
}

return ptBal;
}

/**
* @notice Redeems PT token for DOLA.
* @dev Used by the ALE but can be called by anyone. Carefully review input data for Pendle API.
* The receiver in Pendle API has to be set same as the recipient. If a REDEEM is performed, include a ytProvider with enough allowance.
* @param amount The amount of PT token to be redeemed (and YT if specified).
* @param data Encoded address of the market, minimum amount of DOLA to receive, ytProvider, and Pendle callData.
* @return dolaAmount The amount of DOLA redeemed.
*/
function transformFromCollateral(
uint256 amount,
bytes calldata data
) external override returns (uint256 dolaAmount) {
dolaAmount = transformFromCollateral(amount, msg.sender, data);
}

/**
* @notice Redeems Collateral for DOLA.
* @dev The receiver in Pendle API has to be set same as the recipient. If a REDEEM is performed, include a ytProvider with enough allowance.
* @param amount The amount of PT Token to be redeemed (and YT if specified).
* @param recipient The address to which the underlying token is transferred.
* @param data Encoded address of the market, minimum amount of DOLA to receive for the recipient, ytProvider, and Pendle callData.
* @return dolaAmount The amount of DOLA redeemed.
*/
function transformFromCollateral(
uint256 amount,
address recipient,
bytes calldata data
) public override returns (uint256 dolaAmount) {
(
address market,
uint256 minOut, // Minimum amount of DOLA to receive
address ytProvider,
bytes memory callData
) = abi.decode(data, (address, uint256, address, bytes));
_revertIfMarketNotSet(market);

if (ytProvider != address(0)) {
IERC20 yt = IERC20(markets[market].yt);
yt.safeTransferFrom(ytProvider, address(this), amount);
yt.approve(router, amount);
}

IERC20 pt = IERC20(markets[market].pt);
pt.safeTransferFrom(msg.sender, address(this), amount);
pt.approve(router, amount);

uint256 dolaBal = DOLA.balanceOf(recipient);
(bool success, ) = router.call(callData);
if (!success) revert PendleSwapFailed();
// Ensure recipient received at least minOut DOLA
dolaAmount = DOLA.balanceOf(recipient) - dolaBal;
if (dolaAmount < minOut) revert InsufficientDOLA();
}

/**
* @notice Convert DOLA to PT or PT and YT and deposit PT amount on behalf of recipient, sending YT to ytRecipient
* @param assets The receiver in Pendle API has to be set to this contract address. If a MINT is performed, provide a ytRecipient or YT will be kept in this contract.
* @param recipient The address on behalf of which the PT tokens are deposited.
* @param data The encoded address of the market.
* @return collateralAmount The amount of collateral deposited into the market.
*/
function transformToCollateralAndDeposit(
uint256 assets,
address recipient,
bytes calldata data
) external override returns (uint256) {
(address market, , , ) = abi.decode(
data,
(address, uint256, address, bytes)
);

// Convert DOLA to PT token
uint256 amount = transformToCollateral(assets, address(this), data);

// Deposit PT into Market
IERC20(markets[market].pt).approve(market, amount);
IMarket(market).deposit(recipient, amount);

return amount;
}

/**
* @notice Withdraw the collateral from the market then convert to DOLA.
* @dev The receiver in Pendle API has to be set same as the recipient. If a REDEEM is performed, include a ytProvider with enough allowance.
* @param amount The amount of PT token to be withdrawn from the market.
* @param recipient The address to which DOLA is transferred.
* @param permit The permit data for the Market.
* @param data The encoded address of the market.
* @return dolaAmount The amount of DOLA redeemed.
*/
function withdrawAndTransformFromCollateral(
uint256 amount,
address recipient,
Permit calldata permit,
bytes calldata data
) external override returns (uint256 dolaAmount) {
(
address market,
uint256 minOut,
address ytProvider,
bytes memory callData
) = abi.decode(data, (address, uint256, address, bytes));
_revertIfMarketNotSet(market);

IMarket(market).withdrawOnBehalf(
msg.sender,
amount,
permit.deadline,
permit.v,
permit.r,
permit.s
);

if (ytProvider != address(0)) {
IERC20 yt = IERC20(markets[market].yt);
yt.safeTransferFrom(ytProvider, address(this), amount);
yt.approve(router, amount);
}

IERC20 pt = IERC20(markets[market].pt);
pt.approve(router, amount);

uint256 dolaBal = DOLA.balanceOf(recipient);
(bool success, ) = router.call(callData);
if (!success) revert PendleSwapFailed();

dolaAmount = DOLA.balanceOf(recipient) - dolaBal;
if (dolaAmount < minOut) revert InsufficientDOLA();
}

function _revertIfMarketNotSet(address market) internal view {
if (address(markets[market].pt) == address(0))
revert MarketNotSet(market);
}

/**
* @notice Set the market address and its associated Pendle PT and YT addresses.
* @dev Only callable by the governance.
* @param marketAddress The address of the market.
* @param ptAddress Pendle PT address
* @param ytAddress Pendle YT address
*/
function setMarket(
address marketAddress,
address ptAddress,
address ytAddress
) external onlyGov {
markets[marketAddress] = PT({pt: ptAddress, yt: ytAddress});
emit MarketSet(marketAddress, ptAddress, ytAddress);
}

/**
* @notice Remove the market.
* @dev Only callable by the governance or the guardian.
* @param market The address of the market to be removed.
*/
function removeMarket(address market) external onlyGuardianOrGov {
delete markets[market];
emit MarketRemoved(market);
}
}
4 changes: 2 additions & 2 deletions test/marketForkTests/PendlePTUSDeMarketForkTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ contract PendlePTUSDeMarketForkTest is MarketBaseForkTest {
address marketAddr = address(0x0DFE3D04536a74Dd532dd0cEf5005bA14c5f4112);
address feedAddr = address(0xddB5653FaC7a215139141863B2FAd021D44d7Ee4);

function setUp() public {
function setUp() public virtual {
//This will fail if there's no mainnet variable in foundry.toml
string memory url = vm.rpcUrl("mainnet");
vm.createSelectFork(url, 21077341);
vm.createSelectFork(url, 21437191);

_advancedInit(address(marketAddr), feedAddr, false);
}
Expand Down
Loading
Loading