Skip to main content

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:

  1. Approve the vault to spend your tokens
  2. 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 deposit
  • receiver: 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.

  1. First, users need to approve the Permit2 contract once:
IERC20(underlying).approve(PERMIT2_ADDRESS, type(uint256).max);
  1. 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:

  1. When a user wants to deposit assets, the vault first attempts to use Permit2 to transfer the assets
  2. If Permit2 fails (e.g., no permit exists), the vault falls back to using regular transferFrom with the vault's address as the recipient
  3. 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 withdraw
  • receiver: The address that will receive the underlying tokens
  • owner: 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):

  1. Enable the vault as collateral in the EVC
  2. 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 borrow
  • receiver: 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 repay
  • receiver: 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
  • 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:

  1. Fetch fresh price data from the Pyth API
  2. Include a price update as the first item in your EVC batch
  3. 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:

ConceptDescription
AssetsThe actual tokens (USDC, DAI, etc.) with their respective decimals
SharesInternal 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.