In-Depth: Closing a Delta Neutral PT Leveraged Long

Yield DevYield Dev
9 min read

Since we have an open position in PT-stS / wS collateralized with USDC, as described in our previous post, we may want to construct some additional scripts in order to manage this position. This will be rather straight forward, since we have previously implemented a scripting framework for this position. With a couple of modifications, we can build out scripts for three scenarios.

First, fully close out the position into USDC by selling our hedged asset PT-stS for wS, paying off the entire outstanding debt and then swapping the remaining wS for USDC.

Second, deleverage the position by selling off a portion of our PT-stS hedged asset to pay off our wS liability thus increasing our collateralization ratio.

Lastly, We will create a script to unhedge our position, swapping our USDC collateral for more of the PT-stS hedged asset thus increasing our net exposure to the wS price.

To start out, we create a new file close.s.sol and create our CloseScript contract. We being by setting up our trade parameters exactly the same as in our open script.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import "./common/EScript.s.sol";
import {SonicLib} from "./common/SonicLib.sol";

contract CloseScript is EScript {
    function run() public {
        borrower = msg.sender;
        e_account = getSubaccount(borrower, 0);
        evc = IEVC(SonicLib.EVC);

        address LIABILITY_VAULT = SonicLib.EULER_WS_VAULT;
        address HEDGED_VAULT = SonicLib.EULER_PT_STS_VAULT;
        address COLLATERAL_VAULT = SonicLib.EULER_USDC_VAULT;
        address LIABILITY_TOKEN = SonicLib.WS;
        address HEDGED_TOKEN = SonicLib.PT_STS;
        address COLLATERAL_TOKEN = SonicLib.USDC;
    }
}

Next, we are going to continue simplifying our script framework by generalizing some functions that will be reusable across all of our scripts.

The first instance of this is getting out euler-orderflow-router swap data. Since we will constantly be getting payloads to execute an exactIn swap for our various assets and write them to json payloads for use in our script, we can create a utility function for reuse and readability.

For this reason, let's jump back to our EScript.s.sol file and implement a generalize getter function.

// Escript.s.sol

    function getRoutingData(
        address _inputVault,
        address _outputVault,
        address _inputToken,
        address _outputToken,
        uint256 _amount
    ) public returns (string memory _swapJson, string memory _verifyJson) {
        requestPayload(_inputVault, _outputVault, _inputToken, _outputToken, _amount);
        _swapJson = getJsonFile("./script/payloads/swapData.json");
        _verifyJson = getJsonFile("./script/payloads/verifyData.json");
    }

Here, we define our parameters for the swap and request a payload using our previously defined requestPayload function. Since this function writes the payload data to a json file, we can just read these two files to memory and return them.

This allows us to simplify retrieving router data in our script.


// close.s.sol
        uint256 hedged_balance = assetsBalance(HEDGED_VAULT);
        (string memory swapJson, string memory verifyJson) = getRoutingData(
            HEDGED_VAULT, LIABILITY_VAULT, HEDGED_TOKEN, LIABILITY_TOKEN, hedged_balance
        );

Since, our goal is to close the position. We have first retrieved our hedged_balance the amount of the HEDGE_TOKEN PT-stS which we have deposited as collateral. We will be swapping this entire amount for the LIABILITY_TOKEN wS. Thus we pass these parameters to the getRoutingData function. We have passed the LIABILITY_VAULT as the _outputVault parameter since this will be the destination of our swap's output assets which we will claim as a deposit via the verifyJson payload execution. We will then be able to execute repayWithShares on the LIABILITY_VAULT to pay down our debt.

Now that we have our swap data taken care of, we can construct the batch to execute. We will need to make two additional operation along with our two swap operations. First, we will need to define a batch item to withdraw the HEDGED_TOKEN to the swapper address. Secondly, we will need an operation at the end of the batch to repay the debt with our new purchased LIABILITY_TOKEN.

Since these are, once again, common operations in position management, we can create some utility functions for returning these IEVC.BatchItem structs in our EScript.s.sol file.

// Escript.s.sol

    function batchWithdrawTo(address _vault, uint256 _amount, address _to) public view returns (IEVC.BatchItem memory) {
        return IEVC.BatchItem({
            onBehalfOfAccount: e_account,
            targetContract: _vault,
            value: 0,
            data: abi.encodeWithSelector(EVault.withdraw.selector, _amount, _to, e_account)
        });
    }

    function batchRepayWithShares(address _vault, uint256 _amount) public view returns (IEVC.BatchItem memory) {
        return IEVC.BatchItem({
            onBehalfOfAccount: e_account,
            targetContract: _vault,
            value: 0,
            data: abi.encodeWithSelector(EVault.repayWithShares.selector, _amount, e_account)
        });
    }

Here we define batchWithdrawTo which returns a batch item for withdrawing assets from a given vault in a given amount to a given recipient (the swapperAddress in this use case). We also define batchRepayWithShares which repays the debt of our e_account in a given vault with the e_accounts own deposit shares.

We can now construct a batch to execute this withdraw, swap and repay. Once again, this is a common pattern for on chain position management. Thus we can create another general function for returning this entire batch of BatchItems as we may want to use this pattern in many scenarios where it makes sense to repay debt by swapping a collateral, whether we are deleveraging or closing out a position.

// EScript.s.sol

    function batchSwapAndRepay(
        address _inputVault,
        address _repayVault,
        uint256 _amountIn,
        string memory _swapJson,
        string memory _verifyJson
    ) public view returns (IEVC.BatchItem[] memory items) {
        items = new IEVC.BatchItem[](4);
        items[0] = batchWithdrawTo(_inputVault, _amountIn, vm.parseJsonAddress(_swapJson, ".swapperAddress"));
        items[1] = batchPayload(vm.parseJsonAddress(_swapJson, ".swapperAddress"), vm.parseJsonBytes(_swapJson, ".swapperData"));
        items[2] = batchPayload(vm.parseJsonAddress(_verifyJson, ".verifierAddress"), vm.parseJsonBytes(_verifyJson, ".verifierData"));
        items[3] = batchRepayWithShares(_repayVault, type(uint256).max);
    }

Here, we have generalized this pattern with batchSwapAndRepay. We can easily construct a batch which withdraws the amount of assets from the _inputVault swaps and skims them according to the json payloads provided and then repays the maximum amount of debt repayable by the amount of deposit shares available or the amount of debt outstanding. This function then returns the entire batch as an array of batch items, ready for execution.

Once again, we encounter a functionality we will constantly be calling with out scripts, that of actually executing the batch.

// EScript.s.sol

    function broadcastBatch(IEVC.BatchItem[] memory _items) public {
        vm.startBroadcast();
        evc.batch(_items);
        vm.stopBroadcast();
    }

Here we simplify our function call for executing an array of BatchItems via the EVC inside of the vm's broadcast context. All we need to do is pass it our batch array.

Finally displaying the balances of our vaults will be useful for ensuring that the output of the scripts execution matches our expectations when we run the script in a fork. So, we add a logging function for our vault balances.

// EScript.s.sol

    function logPositionInfo(
        address _collateralVault,
        address _hedgedVault,
        address _liabilityVault
    ) public view {
        console.log("--------------------------------");
        console.log("COLLATERAL VAULT: ", assetsBalance(_collateralVault));
        console.log("HEDGED VAULT: ",assetsBalance(_hedgedVault));
        uint256 debt = debtBalance(_liabilityVault);
        if (debt > 0) {
            console.log("LIABILITY VAULT DEBT: ", debt);
        } else {
            console.log("LIABILITY VAULT ASSET BALANCE: ", assetsBalance(_liabilityVault));
        }
        console.log("--------------------------------");
    }

We are finally ready to jump back to our close.s.sol file and execute the batch in the context of our trade.

// close.s.sol

        broadcastBatch(
            batchSwapAndRepay(
                HEDGED_VAULT, LIABILITY_VAULT, hedged_balance,
                swapJson, verifyJson
            )
        );

        logPositionInfo(COLLATERAL_VAULT, HEDGED_VAULT, LIABILITY_VAULT);

We pass our HEDGED_VAULT, since PT-stS is the input token for our swap and repay, along with the LIABILITY_VAULT as our _repayVault and our hedged_balance as the amount. We also pass through our previously retrieved swap data. batchSwapAndRepay will return the EVC.BatchItem array that we need to execute this operation thus we can pass the entire function call as an argument directly to broadcastBatch which will handle the execution.

After that, we can call logPositionInfo() and see our balance status after the execution is completed.

Now, after running this we may notice that our position has a profit in the form of a log showing LIABILITY VAULT ASSET BALANCE: xxxxx indicating that after repaying the debt, we had excess LIABILITY_TOKENs aka profit.

So, we will now be wanting to swap these profits to cash. In order to do so, we simply call getRoutingData for our new transaction.

// close.s.sol

        uint256 collateral_balance = assetsBalance(COLLATERAL_VAULT);
        uint256 liability_balance = assetsBalance(LIABILITY_VAULT);

        (swapJson, verifyJson) = getRoutingData(
            LIABILITY_VAULT, COLLATERAL_VAULT, LIABILITY_TOKEN, COLLATERAL_TOKEN, liability_balance
        );

Here we retrieve routing data for the LIABILTY_TOKEN's liability_balance of wS and swapped for our desired output the COLLATERAL_TOKEN (USDC). we have also taken note of the COLLATERAL_VAULT collateral_balance which we will use later to calculate our net USDC profit.

Since we have the swap data, we need to construct a batch similar to our previous one except that we do not need to execute a repay function after the swapVerify payload has deposited our output token shares in the vault. So we will create another utility function to return a batch for this pattern of BatchWithdrawAndSwap

// EScript.s.sol

    /// @notice Withdraw from the inputVault, swap and deposit to the outputVault
    /// @param _swapJson The json file for the swap router data
    /// @param _verifyJson The json file for the verify router data
    function batchWithdrawAndSwap(
        address _inputVault,
        uint256 _amountIn,
        string memory _swapJson,
        string memory _verifyJson
    ) public view returns (IEVC.BatchItem[] memory items) {
        items = new IEVC.BatchItem[](3);
        items[0] = batchWithdrawTo(_inputVault, _amountIn, vm.parseJsonAddress(_swapJson, ".swapperAddress"));
        items[1] = batchPayload(vm.parseJsonAddress(_swapJson, ".swapperAddress"), vm.parseJsonBytes(_swapJson, ".swapperData"));
        items[2] = batchPayload(vm.parseJsonAddress(_verifyJson, ".verifierAddress"), vm.parseJsonBytes(_verifyJson, ".verifierData"));
    }

Exactly the same as before, except without a repay operation. Note, also, that the batchWithdrawAndSwap function only needs to "know" about the _inputVault so that it can withdraw the assets to the swapperAddress. Otherwise, the swap payload already has the context of which token to swap to and which vault will receive it. The verifierData that executes within the batch takes care of ensuring that the tokens were delivered to destination vault and were skimmed converting them to deposits on behalf of our e_account.

Now we can easily execute this batch as we did before.

// close.s.sol
        broadcastBatch(batchWithdrawAndSwap(
            LIABILITY_VAULT, liability_balance,
            swapJson, verifyJson)
        );
        logPositionInfo(COLLATERAL_VAULT, HEDGED_VAULT, LIABILITY_VAULT);
        console.log("COLLATERAL PROFIT: ", assetsBalance(COLLATERAL_VAULT) - collateral_balance);

Here, we use the LIABILITY_VAULT as the _inputVault from which to withdraw and the balance of funds to withdraw.

Once this batch is broadcast we can display our vault info. we can also get the difference in our COLLATERAL_VAULT balance to display our profit in USDC.

forge script --fork-url https://rpc.soniclabs.com --sender $SENDER script/close.s.sol --ffi

With this framework of script utilities, we can exert very fine control over the leverage and exposure of our position in addition to allowing us to move quickly and flexibly across trades with maximum capital efficiency.

From here, implementing scripts which allow us to unhedge our exposure or deleverage our position will be a breeze.

High yield defi engineering is available on demand at YieldDev Studio

Feel free to reach out on —> x for anything, technical or otherwise

Full code described above can be found on github

Full code of close.s.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import "./common/EScript.s.sol";
import {SonicLib} from "./common/SonicLib.sol";

contract CloseScript is EScript {
    function run() public {
        borrower = msg.sender;
        e_account = getSubaccount(borrower, 2);
        evc = IEVC(SonicLib.EVC);

        address LIABILITY_VAULT = SonicLib.EULER_WS_VAULT;
        address HEDGED_VAULT = SonicLib.EULER_PT_STS_VAULT;
        address COLLATERAL_VAULT = SonicLib.EULER_USDC_VAULT;
        address LIABILITY_TOKEN = SonicLib.WS;
        address HEDGED_TOKEN = SonicLib.PT_STS;
        address COLLATERAL_TOKEN = SonicLib.USDC;

        uint256 hedged_balance = assetsBalance(HEDGED_VAULT);
        (string memory swapJson, string memory verifyJson) = getRoutingData(
            HEDGED_VAULT, LIABILITY_VAULT, HEDGED_TOKEN, LIABILITY_TOKEN, hedged_balance
        );

        broadcastBatch(batchSwapAndRepay(
            HEDGED_VAULT, LIABILITY_VAULT, hedged_balance,
            swapJson, verifyJson
        ));
        logPositionInfo(COLLATERAL_VAULT, HEDGED_VAULT, LIABILITY_VAULT);

        uint256 collateral_balance = assetsBalance(COLLATERAL_VAULT);
        uint256 liability_balance = assetsBalance(LIABILITY_VAULT);

        (swapJson, verifyJson) = getRoutingData(
            LIABILITY_VAULT, COLLATERAL_VAULT, LIABILITY_TOKEN, COLLATERAL_TOKEN, liability_balance
        );

        broadcastBatch(batchWithdrawAndSwap(
            LIABILITY_VAULT, liability_balance,
            swapJson, verifyJson)
        );
        logPositionInfo(COLLATERAL_VAULT, HEDGED_VAULT, LIABILITY_VAULT);
        console.log("COLLATERAL PROFIT: ", assetsBalance(COLLATERAL_VAULT) - collateral_balance);
    }
}
0
Subscribe to my newsletter

Read articles from Yield Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yield Dev
Yield Dev