EVC For On-chain Management: Migrate a Leveraged Position from Silo to Euler


Euler's Ethereum Vault Connector (EVC) framework can be a powerful tool for managing on chain positions. The EVC, essentially, acts as an authenticated multicall contract. Allowing several operations to be batched while maintaining an awareness of the execution context, which in turn allows it to provide a flexible environment for financial operations.
The EVC offers several benefits for on chain trading including; standardized authentication for operator contracts to act on behalf of user accounts and built in sub accounts for segregating a user's EOA into multiple liability positions.
One of its most powerful features for scripting on chain trades is it's batch
mechanism which allows several operations to be combined into a single batch call that will execute atomically. Making it much easier to create a more streamlined and gas efficient execution script and guaranteeing consistency when structuring complex positions. For lending vaults that are EVC aware and are implemented via the EVCs standardized liquidity constraints, it also provides a checks deferred execution context for batched operations.
This means that a trader can perform any number of complex operations on an account and as long the account is eventually solvent, the transaction will be valid. This eliminates the common necessity of calling a standardized flashloan and instead provides flash liquidity access out of the box.
The common, manual procedure of "looping" a defi lending position can be scripted away using an EVC batch by simply taking the maximum loan upfront, swapping into the collateral asset, and depositing into a vault thus satisfying the solvency checks at the end of the batch.
Below I describe how EVC batching can be used to migrate a position from one lending protocol (one which is not EVC aware ) onto an Euler vault based lending market in a streamlined, all solidity script, without using a flashloan.
Let's say we have an open leveraged position of PT-wstkscusd on silo with a liability of USDC.e and we would like to transfer this open position to the equivalent lending market on Euler without unwinding the position and reopening it and without access to extra capital.
We simply need to pay off our silo USDC.e debt, withdraw the PT-wstkscusd collateral from silo and deposit it into Euler. Thanks to the EVC deferred checks mechanism we can source the liquidity for this migration by taking our Euler loan upfront, satisfying our silo liabilities and eventually becoming solvent when we move the collateral over.
Let's Jump into the code and go over some of the finer points of integrating with these protocols.
First we will need to import the contract interfaces that we will be interacting with
// EvcShift.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import { ISilo } from "silo-core/contracts/interfaces/ISilo.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "evc/interfaces/IEthereumVaultConnector.sol";
import { EVault } from "evk/EVault/EVault.sol";
import { EVCUtil } from "evc/utils/EVCUtil.sol";
Than to setup our special purpose contract for executing the migration we will inherit EVCUtil, here we only need to define the canonical EVC contract address deployed by Euler and effective for the lending market we are operating on:
contract EvcShift {
constructor(address _evc) EVCUtil(_evc) {}
}
Next we need to define three utility functions for our operation. A view function to read our repayment and withdrawal amounts from silo
, an approval function to provide an allowance to the Euler deposit vault for our collateral, and a function that executes our silo repayment on behalf of the borrower. The repaySiloAndWithdraw
will be called from inside our batch, since an external call from the evc to a contract that is not EVC aware, sees the EVC
address as msg.sender
in order that we may pay the debt from funds held in this contract, we write a special purpose function so that silo's transferFrom
call see's our contract as msg.sender
function repaySiloAndWithdraw(
address _silo,
address _collateralSilo,
address _borrower,
uint256 _amount,
uint256 _maxWithdraw
) external {
IERC20(ISilo(_silo).asset()).approve(address(_silo), _amount);
ISilo(_silo).repay(_amount, _borrower);
ISilo(_collateralSilo).withdraw(_maxWithdraw, address(this), _borrower, ISilo.CollateralType.Protected);
}
function getMigrationAmounts(
address _repaySilo,
address _collateralSilo,
address _collateralShare,
address _borrower
) internal view returns (uint256, uint256) {
uint256 _amount = ISilo(_repaySilo).maxRepay(_borrower);
uint256 _maxWithdraw = ISilo(_collateralSilo).convertToAssets(
IERC20(_collateralShare).balanceOf(address(_borrower)),
ISilo.AssetType.Protected
);
return (_amount, _maxWithdraw);
}
function approveDepositVault(
address _collateralAsset,
address _depositVault,
uint256 _amount
) internal {
IERC20(_collateralAsset).approve(address(_depositVault), _amount);
}
Now, we can implement a function that executes our entire batch. First we provide as arguments all of the variables we will need for the entire operation. Note, we provide _collateralShare
address as the protected collateral share token from the silo collateral vault since, protected (non-borrowable) assets on silo have a special status and share token address. The share token is held by our _borrower
EOA and are used to secure the solvency of our account with regard to our debt. In order to allow withdrawal of the assets these protected shares represent by our contract, our EOA simply needs to execute and ERC20 approval on the protected share contract providing an allowance to our special purpose EvcShift
contract prior to executing our migrateLoan
function.
Since our contract will be able to execute a withdraw for the silo assets, we will want to ensure that only the borrower can call this function. We will also enforce that the _e_account
Euler sub account is in fact a sub account of the borrower to prevent any mistakes.
Also, note that we provide _e_account
argument, this is the address of our Euler subaccount of our EOA to which we want to migrate the position. We will also derive this prior to executing the migrateLoan
function.
We will execute both of these actions in the script we write to deploy and execute this transaction. Additionally, we will approve our evcShift
contract as an operator in the evc
context to act on behalf of our _e_account
. This will all be done in solidity using Forge's scripting.
Further, in our migrateLoan
function we have used our utility functions to get the borrowers max repayment and withdraw amounts from the Silo
vaults and have preemptively issued an allowance for the Euler _depositVault
to eventually transfer our collateral assets allowing for deposit.
function migrateLoan(
address _repaySilo,
address _collateralSilo,
address _borrowToken,
address _collateralShare,
address _borrower,
address _e_account,
address _depositVault,
address _borrowVault
) public {
require(msg.sender == _borrower, "Only borrower can call this function");
require(_haveCommonOwner(_borrower, _e_account), "_e_account must be subaccount");
(uint256 _amount, uint256 _maxWithdraw) = getMigrationAmounts(_repaySilo, _collateralSilo, _collateralShare, _borrower);
approveDepositVault(ISilo(_collateralSilo).asset(), _depositVault, _maxWithdraw);
}
Now we are ready to define our batch of operations to securely migrate this position. This is also in our migrateLoan
function. First we declare a list of our BatchItems
, we will use 7 operations in this batch.
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](6);
Now in order to take a loan or deposit into Euler we need to set both vaults as collateral and our borrowing vault as a controller. This allows the EVC to seize our assets in the event of a solvency violation. We can see that we encode the transaction data according to the ABI. However, for these operations we are calling the EVC directly and thus must set the onBehalfOfAccount
as address(0)
this is because the will use a delegate call, preserving the msg.sender
and authenticating the caller (our evcShift
contract) as an operator for the _e_account
for which we are enabling.
items[0] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(evc),
value: 0,
data: abi.encodeWithSelector(IEVC.enableCollateral.selector, _e_account, _borrowVault)
});
items[1] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(evc),
value: 0,
data: abi.encodeWithSelector(IEVC.enableCollateral.selector, _e_account, _depositVault)
});
items[2] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(evc),
value: 0,
data: abi.encodeWithSelector(IEVC.enableController.selector, _e_account, _borrowVault)
});
Once the vaults are enabled, we can take a borrow out of the _borrowVault
on behalf of our _e_account
. We have taken this loan against the _e_account
with the receiver of the funds being our EvcShift
contract. Since the _borrowVault
is EVC aware it authenticates that EvcShift
contract we are calling from is authorized to act on behalf of our _e_account
.
items[3] = IEVC.BatchItem({
onBehalfOfAccount: _e_account,
targetContract: _borrowVault,
value: 0,
data: abi.encodeWithSelector(EVault.borrow.selector, _amount, address(this))
});
Once we have the USDC.e to pay off our loan we can now call our repaySiloAndWithdraw
function, paying off the debt of _borrower
since anyone can repay anyone else's debt in silo, no special permissions are needed here. The repaySilAndWithdraw
function will also call withdraw on the collateral silo. Now, there should be no liability in the silo
vault and we can now withdraw the collateral asset's into our EvcShift
contract. Recall that we would need to have already granted this authority to our EvcShift
contract by having our _borrower
EOA approve an allowance.
items[4] = IEVC.BatchItem({
onBehalfOfAccount: address(this),
targetContract: address(this),
value: 0,
data: abi.encodeWithSelector(this.repaySiloAndWithdraw.selector, _repaySilo, _collateralSilo, _borrower, _amount, _maxWithdraw)
Finally, to prevent all the previous operations from reverting, we deposit our collateral into the Euler _depositVault
to satisfy our solvency requirements created by the loan. Since the collateral assets were withdrawn into our EvcShift
contract we call this function on behalf of the EvcShift
itself with the _e_account
as the receiver.
items[5] = IEVC.BatchItem({
onBehalfOfAccount: address(this),
targetContract: _depositVault,
value: 0,
data: abi.encodeWithSelector(
EVault.deposit.selector,
_maxWithdraw,
_e_account
)
});
Now that we have structured all the operations in our batch we can execute it through the evc
contract and have the EvcShift
contract release its operational authority over our _e_account
for safety
evc.batch(items);
evc.setAccountOperator(_e_account, address(this), false);
Now we can write our deploy and execution script. Start by importing the necessary contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Script, console} from "forge-std/Script.sol";
import {EvcShift} from "../src/EvcShift.sol";
import {ISiloConfig} from "silo-core/contracts/interfaces/ISiloConfig.sol";
import {ISilo} from "silo-core/contracts/interfaces/ISilo.sol";
import {SonicLib} from "../test/common/SonicLib.sol";
import "evc/interfaces/IEthereumVaultConnector.sol";
import { EVault } from "evk/EVault/EVault.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
Next, we set up the script, we will also add a utility function to derive our subaccount according to the EVC context:
contract EvcShiftScript is Script {
EvcShift public evcShift;
address borrower;
ISiloConfig siloConfig;
address ptUSDCSilo;
address usdcSilo;
function getSubaccount(address _account, uint256 _index) public returns (address) {
return address(uint160(uint160(_account)^_index));
}
}
Next we define our run
function, start by getting our silo config data:
function run() public {
borrower = msg.sender;
address subaccount = getSubaccount(borrower, 1);
siloConfig = ISiloConfig(SonicLib.Silo_Config_Address_wstkscUSDC);
(ptUSDCSilo, usdcSilo) = siloConfig.getSilos();
}
We are now ready to start the broadcasting portion of our script. First we deploy our migration contract, than we approve the contract to withdraw our silo
collateral and set it as an operator on our sub account:
vm.startBroadcast();
evcShift = new EvcShift(SonicLib.Euler_Vault_Connector);
ISilo(SonicLib.Silo_Address_ProtectedSHare_PT).approve(address(evcShift), UINT256_MAX);
IEVC(SonicLib.Euler_Vault_Connector).setAccountOperator(subaccount, address(evcShift), true);
bool isAuthorized = IEVC(SonicLib.Euler_Vault_Connector).isAccountOperatorAuthorized(subaccount, address(evcShift));
console.log("isAuthorized: ", isAuthorized);
At this point, we can execute the migrations function.
evcShift.migrateLoan(
usdcSilo,
ptUSDCSilo,
address(ISilo(usdcSilo).asset()),
SonicLib.Silo_Address_ProtectedSHare_PT,
borrower,
subaccount, // EVC subaccount
address(SonicLib.Euler_Deposit_Vault),
address(SonicLib.Euler_Borrow_Vault)
);
Once that is complete. We will want to revoke all approval on silo just in case and ensure the operator released authorization.
ISilo(SonicLib.Silo_Address_ProtectedSHare_PT).approve(address(evcShift), 0);
console.log("Silo Allowance: ", ISilo(SonicLib.Silo_Address_ProtectedSHare_PT).allowance(address(evcShift), address(SonicLib.Silo_Address_ProtectedSHare_PT)));
isAuthorized = IEVC(SonicLib.Euler_Vault_Connector).isAccountOperatorAuthorized(subaccount, address(evcShift));
console.log("isAuthorized: ", isAuthorized);
if (isAuthorized) revert();
vm.stopBroadcast();
We can than print everything out to ensure consistency.
console.log("maxRepayment USDC Silo: ", ISilo(usdcSilo).maxRepay(borrower));
console.log("maxWithdraw PTUSDC Silo: ", ISilo(ptUSDCSilo).maxWithdraw(borrower, ISilo.CollateralType.Protected));
console.log("Assets Stuck in Shift: ", IERC20(address(ISilo(ptUSDCSilo).asset())).balanceOf(address(evcShift)));
console.log("Debt of subaccount: ", EVault(SonicLib.Euler_Borrow_Vault).debtOf(subaccount));
console.log("Collateral of subaccount: ", EVault(SonicLib.Euler_Deposit_Vault).balanceOf(subaccount));
We can than run our script with fork to test it is working. forge script script/EvcShift.s.sol --fork-url $RPC_URL --sender $BORROWER_EOA
Full Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import { ISilo } from "silo-core/contracts/interfaces/ISilo.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "evc/interfaces/IEthereumVaultConnector.sol";
import { EVault } from "evk/EVault/EVault.sol";
import { EVCUtil } from "evc/utils/EVCUtil.sol";
contract EvcShift is EVCUtil {
constructor(address _evc) EVCUtil(_evc) {}
function repaySiloAndWithdraw(
address _silo,
address _collateralSilo,
address _borrower,
uint256 _amount,
uint256 _maxWithdraw
) external {
IERC20(ISilo(_silo).asset()).approve(address(_silo), _amount);
ISilo(_silo).repay(_amount, _borrower);
ISilo(_collateralSilo).withdraw(_maxWithdraw, address(this), _borrower, ISilo.CollateralType.Protected);
}
function getMigrationAmounts(
address _repaySilo,
address _collateralSilo,
address _collateralShare,
address _borrower
) internal view returns (uint256, uint256) {
uint256 _amount = ISilo(_repaySilo).maxRepay(_borrower);
uint256 _maxWithdraw = ISilo(_collateralSilo).convertToAssets(
IERC20(_collateralShare).balanceOf(address(_borrower)),
ISilo.AssetType.Protected
);
return (_amount, _maxWithdraw);
}
function approveDepositVault(
address _collateralAsset,
address _depositVault,
uint256 _amount
) internal {
IERC20(_collateralAsset).approve(address(_depositVault), _amount);
}
function migrateLoan(
address _repaySilo,
address _collateralSilo,
address _borrowToken,
address _collateralShare,
address _borrower,
address _e_account,
address _depositVault,
address _borrowVault
) public {
require(msg.sender == _borrower, "Only borrower can call this function");
require(_haveCommonOwner(_borrower, _e_account), "Borrower and EVC account must have a common owner");
(uint256 _amount, uint256 _maxWithdraw) = getMigrationAmounts(_repaySilo, _collateralSilo, _collateralShare, _borrower);
approveDepositVault(ISilo(_collateralSilo).asset(), _depositVault, _maxWithdraw);
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](6);
items[0] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(evc),
value: 0,
data: abi.encodeWithSelector(IEVC.enableCollateral.selector, _e_account, _borrowVault)
});
items[1] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(evc),
value: 0,
data: abi.encodeWithSelector(IEVC.enableCollateral.selector, _e_account, _depositVault)
});
items[2] = IEVC.BatchItem({
onBehalfOfAccount: address(0),
targetContract: address(evc),
value: 0,
data: abi.encodeWithSelector(IEVC.enableController.selector, _e_account, _borrowVault)
});
items[3] = IEVC.BatchItem({
onBehalfOfAccount: _e_account,
targetContract: _borrowVault,
value: 0,
data: abi.encodeWithSelector(EVault.borrow.selector, _amount, address(this))
});
items[4] = IEVC.BatchItem({
onBehalfOfAccount: address(this),
targetContract: address(this),
value: 0,
data: abi.encodeWithSelector(this.repaySiloAndWithdraw.selector, _repaySilo, _collateralSilo, _borrower, _amount, _maxWithdraw)
});
items[5] = IEVC.BatchItem({
onBehalfOfAccount: address(this),
targetContract: _depositVault,
value: 0,
data: abi.encodeWithSelector(
EVault.deposit.selector,
_maxWithdraw,
_e_account
)
});
evc.batch(items);
evc.setAccountOperator(_e_account, address(this), false);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Script, console} from "forge-std/Script.sol";
import {EvcShift} from "../src/EvcShift.sol";
import {ISiloConfig} from "silo-core/contracts/interfaces/ISiloConfig.sol";
import {ISilo} from "silo-core/contracts/interfaces/ISilo.sol";
import {SonicLib} from "../test/common/SonicLib.sol";
import "evc/interfaces/IEthereumVaultConnector.sol";
import { EVault } from "evk/EVault/EVault.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract EvcShiftScript is Script {
EvcShift public evcShift;
address borrower;
ISiloConfig siloConfig;
address ptUSDCSilo;
address usdcSilo;
// function setUp() public {}
function getSubaccount(address _account, uint256 _index) public returns (address) {
return address(uint160(uint160(_account)^_index));
}
function run() public {
borrower = msg.sender;
console.log("Borrower: ", borrower);
address subaccount = getSubaccount(borrower, 1);
siloConfig = ISiloConfig(SonicLib.Silo_Config_Address_wstkscUSDC);
(ptUSDCSilo, usdcSilo) = siloConfig.getSilos();
uint256 maxRepayment = ISilo(usdcSilo).maxRepay(borrower);
vm.startBroadcast();
evcShift = new EvcShift(SonicLib.Euler_Vault_Connector);
ISilo(SonicLib.Silo_Address_ProtectedSHare_PT).approve(address(evcShift), UINT256_MAX);
IEVC(SonicLib.Euler_Vault_Connector).setAccountOperator(subaccount, address(evcShift), true);
bool isAuthorized = IEVC(SonicLib.Euler_Vault_Connector).isAccountOperatorAuthorized(subaccount, address(evcShift));
console.log("isAuthorized: ", isAuthorized);
evcShift.migrateLoan(
usdcSilo,
ptUSDCSilo,
address(ISilo(usdcSilo).asset()),
SonicLib.Silo_Address_ProtectedSHare_PT,
borrower,
subaccount, // EVC subaccount
address(SonicLib.Euler_Deposit_Vault),
address(SonicLib.Euler_Borrow_Vault)
);
ISilo(SonicLib.Silo_Address_ProtectedSHare_PT).approve(address(evcShift), 0);
console.log("Silo Allowance: ", ISilo(SonicLib.Silo_Address_ProtectedSHare_PT).allowance(address(evcShift), address(SonicLib.Silo_Address_ProtectedSHare_PT)));
isAuthorized = IEVC(SonicLib.Euler_Vault_Connector).isAccountOperatorAuthorized(subaccount, address(evcShift));
console.log("isAuthorized: ", isAuthorized);
if (isAuthorized) revert();
vm.stopBroadcast();
console.log("maxRepayment USDC Silo: ", ISilo(usdcSilo).maxRepay(borrower));
console.log("maxWithdraw PTUSDC Silo: ", ISilo(ptUSDCSilo).maxWithdraw(borrower, ISilo.CollateralType.Protected));
console.log("Assets Stuck in Shift: ", IERC20(address(ISilo(ptUSDCSilo).asset())).balanceOf(address(evcShift)));
console.log("Debt of subaccount: ", EVault(SonicLib.Euler_Borrow_Vault).debtOf(subaccount));
console.log("Collateral of subaccount: ", EVault(SonicLib.Euler_Deposit_Vault).balanceOf(subaccount));
}
}
Subscribe to my newsletter
Read articles from Yield Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
