Description:
Proxy contract enabling upgradeable smart contract patterns. Delegates calls to an implementation contract.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IGnosisSafe {
enum Operation {
Call,
DelegateCall
}
function execTransactionFromModule(
address to,
uint256 value,
bytes calldata data,
Operation operation
) external returns (bool success);
function enableModule(address module) external;
}
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function totalSupply() external view returns (uint256);
}
interface IERC20Metadata is IERC20 {
function decimals() external view returns (uint8);
}
interface IAavePoolV3 {
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
function borrow(
address asset,
uint256 amount,
uint256 interestRateMode, // 1 = stable, 2 = variable
uint16 referralCode,
address onBehalfOf
) external;
function getUserAccountData(
address user
)
external
view
returns (
uint256 totalCollateralBase,
uint256 totalDebtBase,
uint256 availableBorrowsBase,
uint256 currentLiquidationThreshold,
uint256 ltv,
uint256 healthFactor
);
}
interface IAaveProtocolDataProviderV3 {
// supplyCap, borrowCap are integers with 0 decimals (whole tokens)
function getReserveCaps(address asset) external view returns (uint256 borrowCap, uint256 supplyCap);
function getReserveTokensAddresses(
address asset
) external view returns (address aTokenAddress, address stableDebtTokenAddress, address variableDebtTokenAddress);
}
interface IAaveAddressesProvider {
function getPool() external view returns (address);
function getPoolDataProvider() external view returns (address);
}
interface IPendleMarketV3 {
function readTokens() external view returns (address SY, address PT, address YT);
}
interface IPendleMarketSY {
function getTokensIn() external view returns (address[] memory);
}
interface IPendleRouterV4 {
enum SwapType {
NONE,
KYBERSWAP,
ODOS,
ETH_WETH,
OKX,
ONE_INCH,
RESERVE_1,
RESERVE_2,
RESERVE_3,
RESERVE_4,
RESERVE_5
}
struct SwapData {
SwapType swapType;
address extRouter;
bytes extCalldata;
bool needScale;
}
struct TokenInput {
address tokenIn; // in
uint256 netTokenIn; // in amount
address tokenMintSy; // in amount for mint SY
address pendleSwap; // 0x0 when mint
SwapData swapData; // none when mint
}
function mintPyFromToken(
address receiver,
address YT,
uint256 minPyOut,
TokenInput calldata input
) external payable returns (uint256 netPyOut, uint256 netSyInterm);
}
contract LeveragePtModule {
uint256 constant MAX_LOOP = 32;
// only operator
address public immutable safe; // gnosis safe wallet address
mapping(address => bool) public operators; // who can call this module
// supply() uses these
address public immutable aaveAddressesProvider; // IAaveAddressesProvider
address public immutable aavePool; // IAavePoolV3
address public immutable aaveDataProvider; // IAaveProtocolDataProviderV3
address public immutable pendleRouter; // router that exposes mintPyFromToken(..)
address public immutable pendleMarket; // pendle market address
address public immutable ptToken; // market.PT
address public immutable ytToken; // market.YT
address public immutable syToken; // market.SY
address public immutable borrowToken; // SY.tokensIn, aave.borrow
// health rate constraint at the end. 1e18 = 100%
uint256 public immutable minHealthRate;
event Loop(uint256 loop, uint256 supplyThisRound, uint256 supplied, uint256 borrowAmt, uint256 mintedPt);
constructor(
address safe_,
address[] memory operators_,
address aaveAddressesProvider_,
address pendleRouter_,
address pendleMarket_,
address borrowToken_,
uint256 minHealthRate_ // e.g. 1e18 for 100% health rate
) {
require(safe_ != address(0), "Invalid Safe");
require(operators_.length > 0, "At least one operator required");
require(aaveAddressesProvider_ != address(0), "Invalid Aave addresses provider");
require(pendleRouter_ != address(0), "Invalid Pendle router");
require(pendleMarket_ != address(0), "Invalid Pendle market");
require(borrowToken_ != address(0), "Invalid borrow token");
require(minHealthRate_ >= 1e18, "Health rate must be >= 100%");
safe = safe_;
for (uint256 i = 0; i < operators_.length; i++) {
address op = operators_[i];
require(op != address(0), "Invalid operator address");
operators[op] = true;
}
aaveAddressesProvider = aaveAddressesProvider_;
aavePool = IAaveAddressesProvider(aaveAddressesProvider_).getPool();
aaveDataProvider = IAaveAddressesProvider(aaveAddressesProvider_).getPoolDataProvider();
pendleRouter = pendleRouter_;
pendleMarket = pendleMarket_;
borrowToken = borrowToken_;
minHealthRate = minHealthRate_;
require(aavePool != address(0), "Invalid Aave pool from provider");
require(aaveDataProvider != address(0), "Invalid Aave data provider from provider");
// pt, yt, sy
(syToken, ptToken, ytToken) = IPendleMarketV3(pendleMarket_).readTokens();
require(ptToken != address(0), "Invalid PT from market");
require(ytToken != address(0), "Invalid YT from market");
require(syToken != address(0), "Invalid SY from market");
// SY.mint should accept borrowToken, otherwise we need to swap
require(_isSyAcceptsToken(borrowToken), "SY does not accept borrowToken");
// check decimals
require(IERC20Metadata(ptToken).decimals() == 18, "Unsupported ptToken.decimals");
require(IERC20Metadata(ytToken).decimals() == 18, "Unsupported ytToken.decimals");
require(IERC20Metadata(syToken).decimals() == 18, "Unsupported syToken.decimals");
require(IERC20Metadata(borrowToken).decimals() == 18, "Unsupported borrowToken.decimals");
}
modifier onlyOperator() {
require(operators[msg.sender], "Not operator");
_;
}
function supply(
uint256 initialPt,
uint256 targetPt,
uint256 borrowPerPtWad // e.g., 0.5e18 => borrow 0.5 USDe for each 1 PT
) external onlyOperator {
require(initialPt > 0, "initialPt = 0");
require(targetPt > 0, "targetPt = 0");
require(targetPt >= initialPt, "target < initial");
_checkSupplyCap(targetPt);
// loop
uint256 supplied = 0;
uint256 supplyThisRound = initialPt;
uint256 count = 0;
while (supplyThisRound > 0) {
uint256 borrowAmt;
uint256 mintedPt;
// supply
_supplyPT(supplyThisRound);
supplied += supplyThisRound;
if (count < MAX_LOOP && supplied < targetPt) {
count += 1;
// borrow until supplied + borrowAmt -> targetPt
// note: we assume borrowToken:PT = 1:1
borrowAmt = (supplyThisRound * borrowPerPtWad) / 1e18;
if (supplied + borrowAmt > targetPt) {
borrowAmt = targetPt - supplied;
}
if (borrowAmt > 0) {
_borrow(borrowAmt);
// mint PT YT
uint256 ptBeforeMint = IERC20(ptToken).balanceOf(safe);
_mintPtYt(borrowAmt);
uint256 ptAfterMint = IERC20(ptToken).balanceOf(safe);
require(ptAfterMint >= ptBeforeMint, "Unexpected mint");
mintedPt = ptAfterMint - ptBeforeMint;
require(mintedPt >= borrowAmt, "Unexpected mint");
}
}
emit Loop(count, supplyThisRound, supplied, borrowAmt, mintedPt);
// we got pt = mintedPt
supplyThisRound = mintedPt;
}
_assertFinalHealthRate();
}
// SY.mint accepts token, otherwise we need to swap
function _isSyAcceptsToken(address token) internal view returns (bool ok) {
address[] memory ins = IPendleMarketSY(syToken).getTokensIn();
for (uint i; i < ins.length; i++) {
if (ins[i] == token) {
return true;
}
}
return false;
}
// cap: aToken.totalSupply + targetPt <= supplyCap * 1e18
function _checkSupplyCap(uint256 targetPt) internal view {
// supply.decimals = 0
(, uint256 supplyCap) = IAaveProtocolDataProviderV3(aaveDataProvider).getReserveCaps(ptToken);
if (supplyCap == 0) {
// 0 = no cap
return;
}
(address aTokenAddr, , ) = IAaveProtocolDataProviderV3(aaveDataProvider).getReserveTokensAddresses(ptToken);
uint256 aTokenSupply = IERC20(aTokenAddr).totalSupply();
require(aTokenSupply + targetPt <= supplyCap * 1e18, "Cap");
}
function _supplyPT(uint256 amountPT) internal {
require(IERC20(ptToken).balanceOf(safe) >= amountPT, "Insufficient PT");
// approve
_approveFromSafe(ptToken, aavePool, amountPT);
// supply
bytes memory data = abi.encodeWithSelector(
IAavePoolV3.supply.selector,
ptToken, // asset
amountPT,
safe, // onBehalfOf
uint16(0) // referralCode
);
bool success = IGnosisSafe(safe).execTransactionFromModule(
aavePool, // to
0, // value
data,
IGnosisSafe.Operation.Call
);
require(success, "Supply failed");
// revoke
_approveFromSafe(ptToken, aavePool, 0);
}
function _borrow(uint256 amountBorrow) internal {
bytes memory data = abi.encodeWithSelector(
IAavePoolV3.borrow.selector,
borrowToken, // asset
amountBorrow,
uint256(2), // 1 = stable, 2 = variable
uint16(0), // referralCode
safe // onBehalfOf
);
bool success = IGnosisSafe(safe).execTransactionFromModule(
aavePool, // to
0, // value
data,
IGnosisSafe.Operation.Call
);
require(success, "Borrow failed");
}
function _approveFromSafe(address token, address spender, uint256 amount) internal {
bytes memory data = abi.encodeWithSelector(IERC20.approve.selector, spender, amount);
bool success = IGnosisSafe(safe).execTransactionFromModule(
token, // to
0, // value
data,
IGnosisSafe.Operation.Call
);
require(success, "Approval failed");
}
function _mintPtYt(uint256 borrowAmt) internal {
// assume 1:1
uint256 minPyOut = borrowAmt;
// approve
_approveFromSafe(borrowToken, pendleRouter, borrowAmt);
// mint
IPendleRouterV4.TokenInput memory tokenInput = IPendleRouterV4.TokenInput({
tokenIn: borrowToken,
netTokenIn: borrowAmt,
tokenMintSy: borrowToken, // equal to tokenIn for mint
pendleSwap: address(0), // 0 for mint
swapData: IPendleRouterV4.SwapData({
swapType: IPendleRouterV4.SwapType.NONE, // 0 for mint
extRouter: address(0), // 0 for mint
extCalldata: bytes(""), // 0 for mint
needScale: false // 0 for mint
})
});
bytes memory data = abi.encodeWithSelector(
IPendleRouterV4.mintPyFromToken.selector,
safe, // receiver
ytToken, // YT
minPyOut,
tokenInput
);
bool success = IGnosisSafe(safe).execTransactionFromModule(
pendleRouter, // to
0, // value
data,
IGnosisSafe.Operation.Call
);
require(success, "mintPyFromToken failed");
// revoke
_approveFromSafe(borrowToken, pendleRouter, 0);
}
function _assertFinalHealthRate() internal view {
(, , , , , uint256 hr) = IAavePoolV3(aavePool).getUserAccountData(safe);
require(hr >= minHealthRate, "Final unhealthy");
}
}
Submitted on: 2025-09-17 12:52:27
Comments
Log in to comment.
No comments yet.