LeveragePtModule

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");
    }
}

Tags:
Proxy, Swap, Upgradeable|addr:0x18c18664c24d5d79c74b7cd9d3dba984bf5fcd96|verified:true|block:23380152|tx:0x44a8dfddc7f50aefd2824f5a5f0e0007794106209563d6e61f151f980d12d784|first_check:1758106345

Submitted on: 2025-09-17 12:52:27

Comments

Log in to comment.

No comments yet.