In-Depth: Closing a Delta Neutral PT Leveraged Long


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_TOKEN
s 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);
}
}
Subscribe to my newsletter
Read articles from Yield Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
