LeveragePtSusdeModule

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 transfer(address to, uint256 amount) external returns (bool);
    function totalSupply() external view returns (uint256);
}

interface IERC4626 {
    function convertToAssets(uint256 shares) external view returns (uint256 assets);
    function convertToShares(uint256 assets) external view returns (uint256 shares);
    function deposit(uint256 assets, address receiver) external returns (uint256 shares);
    function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets);
}

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 IPYieldToken {
    function redeemDueInterestAndRewards(
        address user,
        bool redeemInterest,
        bool redeemRewards
    ) external returns (uint256 interestOut, uint256[] memory rewardsOut);

    function SY() external view returns (address);
}

interface IStandardizedYield {
    function redeem(
        address receiver,
        uint256 amountSharesToRedeem,
        address tokenOut,
        uint256 minTokenOut,
        bool burnFromInternalBalance
    ) external returns (uint256 amountTokenOut);
}

interface IPMarket {
    // minimal interface needed
}
interface IPSwapAggregator {
    // minimal interface needed
}

enum SwapType {
    NONE,
    KYBERSWAP,
    ODOS,
    // ETH_WETH not used in Aggregator
    ETH_WETH,
    OKX,
    ONE_INCH,
    PARASWAP,
    RESERVE_2,
    RESERVE_3,
    RESERVE_4,
    RESERVE_5
}

struct SwapData {
    SwapType swapType;
    address extRouter;
    bytes extCalldata;
    bool needScale;
}

struct SwapDataExtra {
    address tokenIn;
    address tokenOut;
    uint256 minOut;
    SwapData swapData;
}

struct RedeemYtIncomeToTokenStruct {
    IPYieldToken yt;
    bool doRedeemInterest;
    bool doRedeemRewards;
    address tokenRedeemSy;
    uint256 minTokenRedeemOut;
}

interface IPendleRouterV4 {
    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);

    function mintPyFromSy(
        address receiver,
        address YT,
        uint256 netSyIn,
        uint256 minPyOut
    ) external returns (uint256 netPyOut);
}

contract LeveragePtSusdeModule {
    uint256 constant MAX_LOOP = 32;
    uint256 constant MIN_BORROW = 1e18; // min borrow amount (1 USDe)
    uint256 constant SLIPPAGE = 1e13; // 0.001% = 0.00001 * 1e18

    address public immutable safe;
    address public immutable operator;

    address public immutable aaveAddressesProvider;
    address public immutable aavePool;
    address public immutable aaveDataProvider;
    address public immutable pendleRouter;
    address public immutable pendleMarketSusde;

    address public immutable usde;
    address public immutable susde;
    address public immutable ptSusde;
    address public immutable ytSusde;
    address public immutable sySusde;
    address public immutable aptSusde;
    address public immutable variableDebtUsde;

    uint256 public immutable minHealthRate;

    event Loop(
        uint256 loop,
        uint256 oldApt,
        uint256 borrowAmount,
        uint256 susdeReceived,
        uint256 ptMinted,
        uint256 ptSupplied
    );

    constructor(
        address safe_,
        address operator_,
        address aaveAddressesProvider_,
        address pendleRouter_,
        address pendleMarketSusde_,
        address usde_,
        address susde_,
        uint256 minHealthRate_
    ) {
        require(safe_ != address(0), "Invalid Safe");
        require(operator_ != address(0), "Invalid operator");
        require(aaveAddressesProvider_ != address(0), "Invalid Aave addresses provider");
        require(pendleRouter_ != address(0), "Invalid Pendle router");
        require(pendleMarketSusde_ != address(0), "Invalid Pendle market");
        require(usde_ != address(0), "Invalid USDe");
        require(susde_ != address(0), "Invalid sUSDE");
        require(minHealthRate_ >= 1e18, "Health rate must be >= 100%");

        safe = safe_;
        operator = operator_;

        aaveAddressesProvider = aaveAddressesProvider_;
        aavePool = IAaveAddressesProvider(aaveAddressesProvider_).getPool();
        aaveDataProvider = IAaveAddressesProvider(aaveAddressesProvider_).getPoolDataProvider();

        pendleRouter = pendleRouter_;
        pendleMarketSusde = pendleMarketSusde_;
        usde = usde_;
        susde = susde_;

        (sySusde, ptSusde, ytSusde) = IPendleMarketV3(pendleMarketSusde_).readTokens();
        (aptSusde, , ) = IAaveProtocolDataProviderV3(aaveDataProvider).getReserveTokensAddresses(ptSusde);
        (, , variableDebtUsde) = IAaveProtocolDataProviderV3(aaveDataProvider).getReserveTokensAddresses(usde);

        minHealthRate = minHealthRate_;
    }

    modifier onlyOperator() {
        require(msg.sender == operator, "Not operator");
        _;
    }

    function leverage(uint256 targetApt, uint256 safeLtv) external onlyOperator {
        _checkSupplyCap(targetApt);
        uint256 sUSDeNav = IERC4626(susde).convertToAssets(1e18);
        uint256 rounds = 0;
        while (rounds < MAX_LOOP) {
            rounds++;
            // USDe to borrow
            uint256 currentApt = IERC20(aptSusde).balanceOf(safe);
            uint256 aPtToGet = targetApt > currentApt ? targetApt - currentApt : 0;
            if (aPtToGet < MIN_BORROW) {
                break;
            }
            uint256 borrowAmount = _calculateBorrowAmount(currentApt, aPtToGet, safeLtv);
            if (borrowAmount < MIN_BORROW) {
                break;
            }
            _borrow(borrowAmount);
            // borrowed USDe + USDe balance => sUSDe
            uint256 usdeBalance = IERC20(usde).balanceOf(safe);
            uint256 susdeReceived = _stakeUsdeToSusde(usdeBalance, sUSDeNav);
            // staked sUSDe + sUSDe balance => PT + YT
            uint256 sUSDeBalance = IERC20(susde).balanceOf(safe);
            uint256 ptMinted = _mintPtYt(sUSDeBalance, sUSDeNav);
            // minted PT + PT balance => aPT
            uint256 ptBalance = IERC20(ptSusde).balanceOf(safe);
            _supplyPT(ptBalance);
            emit Loop(rounds, currentApt, borrowAmount, susdeReceived, ptMinted, ptBalance);
        }
        _assertFinalHealthRate();
    }

    function claimYieldRewards() external onlyOperator {
        _redeemYtInterest();
        uint256 syBalance = IERC20(sySusde).balanceOf(safe);
        if (syBalance > 0) {
            uint256 ptMinted = _mintPtYtFromSy(syBalance);
            if (ptMinted > 0) {
                _supplyPT(ptMinted);
            }
        }
    }

    function _checkSupplyCap(uint256 targetApt) internal view {
        (, uint256 supplyCap) = IAaveProtocolDataProviderV3(aaveDataProvider).getReserveCaps(ptSusde);
        if (supplyCap == 0) {
            // 0 = no cap
            return;
        }
        uint256 aTokenSupply = IERC20(aptSusde).totalSupply();
        uint256 currentApt = IERC20(aptSusde).balanceOf(safe);
        uint256 delta = targetApt > currentApt ? targetApt - currentApt : 0;
        require(aTokenSupply + delta <= supplyCap * 1e18, "Supply cap exceeded");
    }

    function _calculateBorrowAmount(
        uint256 currentApt,
        uint256 aPtToGet,
        uint256 safeLtv
    ) internal view returns (uint256) {
        uint256 currentDebt = IERC20(variableDebtUsde).balanceOf(safe);
        // assume aPT-sUSDe is in unit of USDe
        uint256 maxSafeBorrow = (currentApt * safeLtv) / 1e18;
        // min(aPtToGet, max(0, maxSafeBorrow - currentDebt))
        uint256 safeBorrowAmount = maxSafeBorrow > currentDebt ? maxSafeBorrow - currentDebt : 0;
        return safeBorrowAmount < aPtToGet ? safeBorrowAmount : aPtToGet;
    }

    function _supplyPT(uint256 amount) internal {
        require(IERC20(ptSusde).balanceOf(safe) >= amount, "Insufficient PT");
        _approveFromSafe(ptSusde, aavePool, amount);
        bytes memory data = abi.encodeWithSelector(
            IAavePoolV3.supply.selector,
            ptSusde, // asset
            amount, // amount
            safe, // onBehalfOf
            0 // referralCode
        );
        bool success = IGnosisSafe(safe).execTransactionFromModule(aavePool, 0, data, IGnosisSafe.Operation.Call);
        require(success, "Supply failed");
        _approveFromSafe(ptSusde, aavePool, 0);
    }

    function _borrow(uint256 amount) internal {
        bytes memory data = abi.encodeWithSelector(
            IAavePoolV3.borrow.selector,
            usde, // asset
            amount, // amount
            2, // interestRateMode (variable rate)
            0, // referralCode
            safe // onBehalfOf
        );
        bool success = IGnosisSafe(safe).execTransactionFromModule(aavePool, 0, 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, // spender
            amount // amount
        );
        bool success = IGnosisSafe(safe).execTransactionFromModule(token, 0, data, IGnosisSafe.Operation.Call);
        require(success, "Approve failed");
    }

    function _stakeUsdeToSusde(uint256 usdeAmount, uint256 sUSDeNav) internal returns (uint256 susdeReceived) {
        _approveFromSafe(usde, susde, usdeAmount);
        // expectedSusde = usdeAmount / sUSDeNav
        uint256 expectedSusde = (usdeAmount * 1e18) / sUSDeNav;
        uint256 minSusde = (expectedSusde * (1e18 - SLIPPAGE)) / 1e18;
        uint256 susdeBefore = IERC20(susde).balanceOf(safe);
        bytes memory data = abi.encodeWithSelector(
            IERC4626.deposit.selector,
            usdeAmount, // assets
            safe // receiver
        );
        bool success = IGnosisSafe(safe).execTransactionFromModule(susde, 0, data, IGnosisSafe.Operation.Call);
        require(success, "sUSDE stake failed");
        uint256 susdeAfter = IERC20(susde).balanceOf(safe);
        susdeReceived = susdeAfter - susdeBefore;
        require(susdeReceived >= minSusde, "Slippage too high on sUSDE stake");
        _approveFromSafe(usde, susde, 0);
    }

    function _mintPtYt(uint256 susdeAmount, uint256 sUSDeNav) internal returns (uint256 ptMinted) {
        _approveFromSafe(susde, pendleRouter, susdeAmount);
        // expectedPt = susdeAmount * sUSDeNav
        uint256 expectedPt = (susdeAmount * sUSDeNav) / 1e18;
        uint256 minPtOut = (expectedPt * (1e18 - SLIPPAGE)) / 1e18;
        IPendleRouterV4.TokenInput memory input = IPendleRouterV4.TokenInput({
            tokenIn: susde,
            netTokenIn: susdeAmount,
            tokenMintSy: susde,
            pendleSwap: address(0),
            swapData: SwapData({ swapType: SwapType.NONE, extRouter: address(0), extCalldata: "", needScale: false })
        });
        uint256 ptBefore = IERC20(ptSusde).balanceOf(safe);
        bytes memory data = abi.encodeWithSelector(
            IPendleRouterV4.mintPyFromToken.selector,
            safe, // receiver
            ytSusde, // YT
            minPtOut, // minPyOut
            input // TokenInput
        );
        bool success = IGnosisSafe(safe).execTransactionFromModule(pendleRouter, 0, data, IGnosisSafe.Operation.Call);
        require(success, "PT mint failed");
        uint256 ptAfter = IERC20(ptSusde).balanceOf(safe);
        ptMinted = ptAfter - ptBefore;
        require(ptMinted >= minPtOut, "PT minted less than expected");
        _approveFromSafe(susde, pendleRouter, 0);
    }

    function _assertFinalHealthRate() internal view {
        (, , , , , uint256 healthFactor) = IAavePoolV3(aavePool).getUserAccountData(safe);
        require(healthFactor >= minHealthRate, "Health factor too low");
    }

    function _redeemYtInterest() internal {
        bytes memory data = abi.encodeWithSelector(
            IPYieldToken.redeemDueInterestAndRewards.selector,
            safe, // user
            true, // doRedeemInterest
            false // doRedeemRewards
        );
        bool success = IGnosisSafe(safe).execTransactionFromModule(ytSusde, 0, data, IGnosisSafe.Operation.Call);
        require(success, "YT redeem interest failed");
    }

    function _mintPtYtFromSy(uint256 syAmount) internal returns (uint256 ptMinted) {
        _approveFromSafe(sySusde, pendleRouter, syAmount);
        // when minting from SY, we expect 1:1 conversion to PT
        uint256 minPtOut = (syAmount * (1e18 - SLIPPAGE)) / 1e18;
        uint256 ptBefore = IERC20(ptSusde).balanceOf(safe);
        bytes memory data = abi.encodeWithSelector(
            IPendleRouterV4.mintPyFromSy.selector,
            safe, // receiver
            ytSusde, // YT
            syAmount, // netSyIn
            minPtOut // minPyOut
        );
        bool success = IGnosisSafe(safe).execTransactionFromModule(pendleRouter, 0, data, IGnosisSafe.Operation.Call);
        require(success, "PT mint from SY failed");
        uint256 ptAfter = IERC20(ptSusde).balanceOf(safe);
        ptMinted = ptAfter - ptBefore;
        require(ptMinted >= minPtOut, "PT minted from SY less than expected");
        _approveFromSafe(sySusde, pendleRouter, 0);
    }
}

Tags:
Proxy, Swap, Upgradeable|addr:0x6485d122cf8659ef8d3d117b9bd1911f4798c58e|verified:true|block:23431224|tx:0xc04b02df3bbf94ce4cf4d022de367f40a9bdd85be6b72b0ed7173a907c27ff0e|first_check:1758733705

Submitted on: 2025-09-24 19:08:25

Comments

Log in to comment.

No comments yet.