Interacting with Vaults
This guide explains how to interact with Euler Vault Kit (EVK) vaults programmatically. We'll cover basic operations, advanced features, and best practices.
Note: While it's possible to interact with vaults directly, the recommended approach is to use the Ethereum Vault Connector (EVC) as the primary entry point. The EVC provides batching, sub-accounts, simulations, and other advanced features that make interactions more efficient and flexible.
Basic Operations
Depositing Assets
To deposit assets into a vault, you'll need to:
- Approve the vault to spend your tokens
- Call the deposit function
// First, approve the vault to spend your tokens
IERC20(underlying).approve(vault, amount);
// Then deposit
IEVault(vault).deposit(amount, receiver);
The deposit
function takes two parameters:
amount
: The amount of underlying tokens to depositreceiver
: The address that will receive the vault shares. This can be:- Your own address
- An EVC sub-account address (for isolated positions)
- Another address (if you're depositing on behalf of someone else)
Using Permit2 for Gasless Approvals
EVK vaults support Permit2 for gasless approvals. This allows users to approve token spending through a signature rather than a separate transaction. For more information, see Permit2 documentation.
- First, users need to approve the Permit2 contract once:
IERC20(underlying).approve(PERMIT2_ADDRESS, type(uint256).max);
- Then, they can use the permit function to grant approval through a signature:
// The permit function in Permit2
function permit(
address owner,
PermitSingle memory permitSingle,
bytes calldata signature
) external;
This two-step process might seem counterintuitive, but it's designed this way for several reasons:
- It allows for better security by separating the approval of the Permit2 contract (which is a one-time operation) from individual token approvals to specific vaults
- It enables batching of multiple operations in a single transaction
- It provides a consistent interface for all tokens, even those that don't natively support permits
- It comes in particularly handy wherever EIP-7702 is not available
The EVK's approval system works as follows:
- When a user wants to deposit assets, the vault first attempts to use Permit2 to transfer the assets
- If Permit2 fails (e.g., no permit exists), the vault falls back to using regular
transferFrom
with the vault's address as the recipient - This approach is gas-efficient because:
- Permit2 is attempted first and only if it fails do we fall back to regular approvals
- The one-time Permit2 approval can be reused across multiple vaults and protocols
- Batching operations with permits reduces the number of transactions needed
This implementation allows for a seamless user experience while maintaining security and gas efficiency. Users can choose to use either method, but using Permit2 is generally more gas-efficient and provides a better user experience through batching capabilities.
Withdrawing Assets
To withdraw assets from a vault:
IEVault(vault).withdraw(amount, receiver, owner);
The withdraw
function takes three parameters:
amount
: The amount of underlying tokens to withdrawreceiver
: The address that will receive the underlying tokensowner
: The address that owns the vault shares. This can be:- Your own address
- An EVC sub-account address (if withdrawing from a sub-account)
- Another address (if you're withdrawing on behalf of someone else, after getting their approval)
Borrowing Assets
Before borrowing, you need to (assuming the collateral is already deposited):
- Enable the vault as collateral in the EVC
- Enable the vault as a controller in the EVC
// Enable the vault as collateral
IEVC(evc).enableCollateral(account, collateralVault);
// Enable the vault as a controller
IEVC(evc).enableController(account, borrowVault);
// Now you can borrow
IEVault(vault).borrow(amount, receiver);
The borrow
function takes two parameters:
amount
: The amount of underlying tokens to borrowreceiver
: The address that will receive the borrowed tokens
Note that the debt will be owned by the account that is a caller of the function (or the one on behalf of which the operation was executed).
Repaying Debt
To repay borrowed assets:
// First approve the vault to spend your tokens (or use Permit2)
IERC20(token).approve(vault, amount);
// Then repay the debt
IEVault(vault).repay(amount, receiver);
The repay
function takes two parameters:
amount
: The amount of underlying tokens to repayreceiver
: The address that owns the debt to be repaid
Note that you can only repay debt for the account that owns the debt (the account that enabled the controller).
Advanced Operations
Batching Operations
The EVC allows you to batch multiple operations in a single transaction. This is done using the BatchItem
struct:
struct BatchItem {
// The target contract to be called
address targetContract;
// The account on behalf of which the operation is to be performed
// Must be address(0) if the target contract is the EVC itself
address onBehalfOfAccount;
// The amount of value to be forwarded with the call
// If type(uint256).max, the whole balance of the EVC contract will be forwarded
// Must be 0 if the target contract is the EVC itself
uint256 value;
// The encoded data which is called on the target contract
bytes data;
}
The targetContract
and onBehalfOfAccount
fields are particularly important:
targetContract
: The address of the contract to call. This can be:- A vault contract for operations like
deposit
,withdraw
,borrow
,repay
- The EVC itself for operations like
enableCollateral
,enableController
- Any other contract that is EVC compatible or does not rely on
msg.sender
- A vault contract for operations like
onBehalfOfAccount
: The account that is meant to be a caller for the operation. This can be:- Your own address
- An EVC sub-account address
address(0)
if the target contract is the EVC itself
Example of batching operations:
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](4);
// Note: This example assumes the tokens are already approved for the vault
// Deposit collateral
items[0] = IEVC.BatchItem({
targetContract: collateralVault,
onBehalfOfAccount: account,
value: 0,
data: abi.encodeWithSelector(IEVault.deposit.selector, collateralAmount, account)
});
// Enable collateral
items[1] = IEVC.BatchItem({
targetContract: evc,
onBehalfOfAccount: address(0),
value: 0,
data: abi.encodeWithSelector(IEVC.enableCollateral.selector, account, collateralVault)
});
// Enable controller
items[2] = IEVC.BatchItem({
targetContract: evc,
onBehalfOfAccount: address(0),
value: 0,
data: abi.encodeWithSelector(IEVC.enableController.selector, account, borrowVault)
});
// Borrow
items[3] = IEVC.BatchItem({
targetContract: borrowVault,
onBehalfOfAccount: account,
value: 0,
data: abi.encodeWithSelector(IEVault.borrow.selector, amount, receiver)
});
IEVC(evc).batch(items);
Flash Liquidity
The EVC allows you to borrow and repay in a single transaction without requiring collateral. This is useful for flash loans and other atomic operations. Here's how to do it:
IEVC.BatchItem[] memory items = new IEVC.BatchItem[](4);
// Enable controller (required for borrowing)
items[0] = IEVC.BatchItem({
targetContract: evc,
onBehalfOfAccount: address(0),
value: 0,
data: abi.encodeWithSelector(IEVC.enableController.selector, account, vault)
});
// Borrow
items[1] = IEVC.BatchItem({
targetContract: vault,
onBehalfOfAccount: account,
value: 0,
data: abi.encodeWithSelector(IEVault.borrow.selector, amount, account)
});
// Repay
items[2] = IEVC.BatchItem({
targetContract: vault,
onBehalfOfAccount: account,
value: 0,
data: abi.encodeWithSelector(IEVault.repay.selector, amount, account)
});
// Disable controller
items[3] = IEVC.BatchItem({
targetContract: vault,
onBehalfOfAccount: account,
value: 0,
data: abi.encodeWithSelector(IEVault.disableController.selector)
});
IEVC(evc).batch(items);
This pattern allows you to:
- Enable the controller for borrowing
- Borrow assets without collateral
- Use the borrowed assets for any purpose (you can add additional batch item in between 1 and 2)
- Repay the debt in the same transaction and freely disable controller
- All while maintaining atomicity and safety
Working with Pull-Based Oracles (Pyth)
Some Euler V2 vaults rely on pull-based oracles like Pyth, which require special handling compared to traditional push-based oracles. These oracles have very short staleness periods (2-3 minutes) and require users to update prices before interacting with contracts.
The Challenge
When working with Pyth-powered vaults, you may encounter:
- Transactions reverting due to oracle errors
- Intermittent failures when querying vault data
- Operations failing unpredictably based on timing
Solution: Update Prices in Your Batches
For any interaction with Pyth-powered vaults, you need to:
- Fetch fresh price data from the Pyth API
- Include a price update as the first item in your EVC batch
- Execute your intended operations
Example: Borrowing with Price Update
// 1. Fetch price updates from Pyth API
// Note: Price feed IDs can typically be obtained by calling the Pyth oracle adapter
// that the vault relies on
const priceIds = ['0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace'] // ETH/USD
// 2. Get price update data from Hermes API
const pythApiUrl = 'https://hermes.pyth.network/v2/updates/price/latest'
const response = await fetch(`${pythApiUrl}?ids[]=${priceIds.join('&ids[]=')}&encoding=hex`)
const data = await response.json()
const priceUpdateData = data.binary.data
// Alternative using curl:
// curl "https://hermes.pyth.network/v2/updates/price/latest?ids[]=0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace&encoding=hex"
// 3. Create batch with price update first
const batchItems = [
// First: Update Pyth prices. Note that Pyth might require you to pass value as an update fee.
{
targetContract: pythOracleAddress,
onBehalfOfAccount: account,
value: 0n,
data: encodeFunctionData({
abi: pythAbi,
functionName: 'updatePriceFeeds',
args: [priceUpdateData]
})
},
// Then: Your borrow operation
{
targetContract: vaultAddress,
onBehalfOfAccount: account,
value: 0n,
data: encodeFunctionData({
abi: vaultAbi,
functionName: 'borrow',
args: [amount, receiver]
})
}
]
// 4. Execute the batch
await walletClient.writeContract({
address: evcAddress,
abi: evcAbi,
functionName: 'batch',
args: [batchItems]
})
Querying Data with Simulations
For read-only operations, use batchSimulation
to simulate price updates without executing them:
// Prepare simulation items
const simulationItems = [
// First: Simulate price update. Note that Pyth might require you to pass value as an update fee.
{
targetContract: pythOracleAddress,
onBehalfOfAccount: account,
value: 0n,
data: encodeFunctionData({
abi: pythAbi,
functionName: 'updatePriceFeeds',
args: [priceUpdateData]
})
},
// Then: Query account data
{
targetContract: lensAddress,
onBehalfOfAccount: account,
value: 0n,
data: encodeFunctionData({
abi: lensAbi,
functionName: 'getAccountInfo',
args: [account, vault]
})
}
]
// Simulate the batch
const { result } = await publicClient.simulate({
address: evcAddress,
abi: evcAbi,
functionName: 'batchSimulation',
args: [simulationItems]
})
Key Points
- Always check if a vault uses Pyth: You can detect this by querying the oracle address the vault relies on and checking if it's of Pyth type
- Update fees: Pyth may charge fees for price updates - check
getUpdateFee()
and include the fee in your transaction value
For detailed information on working with Pyth oracles, see the Working with Pyth Oracles guide.
For more information about Pyth oracles, visit the official Pyth documentation.
Understanding Assets vs. Shares
EVK vaults implement the ERC-4626 standard, which means they operate with two types of units:
Concept | Description |
---|---|
Assets | The actual tokens (USDC, DAI, etc.) with their respective decimals |
Shares | Internal accounting units that represent proportional ownership of the vault's assets |
The relationship between assets and shares is determined by the vault's exchange rate, which grows as interest accrues:
exchangeRate = (cash + totalBorrows + VIRTUAL_DEPOSIT) / (totalShares + VIRTUAL_DEPOSIT)
The VIRTUAL_DEPOSIT
constant is defined as 1e6
.