Independent open source module (Apache-2.0) for WDK by Tether. Not made or endorsed by Tether or Euler. Built by Liam Lakevold. Download: full source (zip, with tests) or npm tarball. Test output: unit, mainnet fork.
wdk-protocol-lending-euler-evm
Euler v2 lending module for WDK (Wallet Development Kit) by Tether, for EVM wallet accounts.
It gives any WDK EVM wallet (@tetherto/wdk-wallet-evm, and ERC-4337 accounts through the same config override the Aave module uses) the standard WDK lending interface on top of Euler v2: the Euler Vault Kit (EVK) vaults and the Ethereum Vault Connector (EVC). Same package shape and method names as @tetherto/wdk-protocol-lending-aave-evm, scaffolded from the @tetherto/create-wdk-module lending template.
What it does
supply,withdraw,borrow,repayand theirquote*counterparts, with the WDKSupplyOptions/WithdrawOptions/BorrowOptions/RepayOptionsshapes.- One transaction borrow. If the borrow vault is not yet the account's controller,
borrowsends a single EVCbatchthat enables the controller and borrows, so an EOA does not need two transactions. - Full exits without dust.
withdrawwithMaxUint256redeems every share;repaywithMaxUint256repays the whole debt including interest accrued up to inclusion. setUseVaultAsCollateral(vault, bool), the Euler counterpart of Aave'ssetUseReserveAsCollateral.disableController(vault)to release collateral from a vault's control once its debt is zero.getAccountData()returns enabled controllers and collaterals, collateral value (on liquidation LTVs) and borrow capacity (on borrow LTVs) as computed by the controller vault, the liability value, the debt, and a health factor scaled by 1e18 that reads like Aave's: below 1e18 the account can be liquidated.getSupplied(token)andgetVaultAddress(token)(the address to approve before a supply or a repay).- Pre-flight checks that fail with a clear message before anything is signed: balance, vault asset matches the token, supply cap (
maxDeposit), withdrawable amount, vault cash, collateral enabled before a borrow, one controller at a time (a borrow from a second vault is refused with the reason), no repay above the debt or without debt, and operations disabled by vault governance (hookConfig). Account health itself is left to the transaction simulation, which runs before signing. - No per-chain address map. The EVC address is read from the vault itself (
vault.EVC()), so the module works on every chain Euler v2 is deployed on, and it refuses to mix vaults from different EVCs in one instance.
Why vaults are passed in
Euler v2 is vault based. Each vault holds one asset, and one asset can have many vaults, each with its own oracle, LTVs and interest rate model. A token address alone does not say which vault to use, so the integrator maps tokens to vaults once in the configuration, or passes a vault option per call. EULER_PRESETS[1] holds the Euler Prime vaults on Ethereum (USD₮, USDC, WETH) as an example.
Installation
npm install wdk-protocol-lending-euler-evm @tetherto/wdk-wallet-evm
Usage
import WalletManagerEvm from '@tetherto/wdk-wallet-evm'
import EulerProtocolEvm, { EULER_PRESETS } from 'wdk-protocol-lending-euler-evm'
const USDT = '0xdAC17F958D2ee523a2206206994597C13D831ec7'
const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
const wallet = new WalletManagerEvm(seedPhrase, { provider: 'https://your-ethereum-rpc' })
const account = await wallet.getAccount(0)
const euler = new EulerProtocolEvm(account, { vaults: EULER_PRESETS[1] })
// 1. Deposit WETH and use it as collateral.
const eWETH = await euler.getVaultAddress(WETH)
await account.approve({ token: WETH, spender: eWETH, amount: 10n ** 18n })
await euler.supply({ token: WETH, amount: 10n ** 18n })
await euler.setUseVaultAsCollateral(eWETH, true)
// 2. Borrow 500 USD₮ (one transaction, the controller is enabled inside an EVC batch).
await euler.borrow({ token: USDT, amount: 500_000_000n })
// 3. Check the position.
const { debt, healthFactor } = await euler.getAccountData()
// 4. Repay everything, release the controller, withdraw everything.
const eUSDT = await euler.getVaultAddress(USDT)
await account.approve({ token: USDT, spender: eUSDT, amount: debt + 1_000_000n })
await euler.repay({ token: USDT, amount: 2n ** 256n - 1n })
await euler.disableController(eUSDT)
await euler.withdraw({ token: WETH, amount: 2n ** 256n - 1n })
Every write method accepts the same optional second argument as the Aave module, which an ERC-4337 account uses to override its paymaster or sponsorship configuration. Standard accounts ignore it.
API
| Method | Notes |
|---|---|
new EulerProtocolEvm(account, { vaults?, gasLimitBuffer? }) |
vaults: { [token]: vault }. gasLimitBuffer: percent added to the gas estimate for standard accounts (default 20, 0 to disable). Full or read-only account. |
supply({ token, amount, onBehalfOf?, vault? }, config?) |
vault.deposit(amount, receiver). Approve the vault first. |
withdraw({ token, amount, to?, vault? }, config?) |
vault.withdraw, or vault.redeem(all shares) for MaxUint256. |
borrow({ token, amount, vault? }, config?) |
EVC batch([enableController?, borrow]). onBehalfOf must be omitted or equal the wallet: Euler debt always belongs to the borrower. |
repay({ token, amount, onBehalfOf?, vault? }, config?) |
vault.repay(amount, debtor). MaxUint256 repays the full debt. Approve the vault first. |
quoteSupply / quoteWithdraw / quoteBorrow / quoteRepay |
{ fee }. Works with read-only accounts. |
setUseVaultAsCollateral(vault, useAsCollateral, config?) |
EVC enableCollateral / disableCollateral. |
disableController(vault, config?) |
Only once the debt in that vault is zero. |
getAccountData(account?) |
{ controllers, collaterals, collateralValue, borrowCollateralValue, liabilityValue, debt, healthFactor }. |
getSupplied(token, { vault?, account? }) |
Deposit value in the token's base unit. |
getVaultAddress(token, vault?) |
The vault used for a token. |
Tests
npm install
npm test # 46 offline unit tests: validation, read-only guards, pre-flight checks, gas buffer, exact calldata of every call
npm run lint # standard
npm run build:types
End to end against the real Euler Prime vaults on an Ethereum mainnet fork, with a real WDK wallet:
anvil --fork-url <ethereum rpc> --port 18547 --chain-id 1
FORK_RPC=http://127.0.0.1:18547 npm run test:fork
The fork suite (16 tests) wraps ETH, supplies 1 WETH to eWETH-2, enables it as collateral, borrows 500 USD₮ from eUSDT-2 in one transaction, checks the health factor, refuses a borrow from a second vault, lets an hour of interest accrue, refuses a repay above the debt, repays the full debt with MaxUint256, withdraws partially and then fully while the controller is still enabled (EVK reports maxRedeem = 0 in that state), and disables the controller. It also checks that an over-limit borrow and an unhealthy collateral withdrawal are rejected at simulation before signing, and, impersonating the vault governor on the fork, that a deposit disabled through hookConfig is caught before sending.
Notes and limits
-
Gas limit buffer. Euler vaults accrue interest on their first touch in each block, so a gas estimate made in one block can be a little short when the transaction is mined in the next; the fork tests hit exactly this (an out-of-gas at the final storage write of a deposit). For standard EVM accounts the module therefore sets an explicit
gasLimitof the estimate plus 20%, which WDK honours. ERC-4337 accounts are left to their bundler. Only gas actually used is paid. -
Sub-accounts (Euler's 256 sub-accounts per address) are not exposed yet; every call acts on the wallet's main account.
- Permit2 and EIP-2612 signatures are not used; approvals go straight to the vault, as with the Aave module.
getAccountDatareports values in the controller vault's unit of account (USD for the Euler Prime vaults).
License
Apache-2.0
Source files
- README.md
- LICENSE
- package.json
- index.js
- bare.js
- tsconfig.json
- .editorconfig
- src/euler-protocol-evm.js
- src/euler-abis.js
- src/euler-presets.js
- types/index.d.ts
- types/src/euler-protocol-evm.d.ts
- types/src/euler-abis.d.ts
- types/src/euler-presets.d.ts
- tests/unit/euler-protocol-evm.test.js
- tests/fork/euler-protocol-evm.fork.test.js
- scripts/scan-vaults.mjs