| name | euler-v2-plugin |
| description | Supply, borrow and earn yield on Euler v2 - a modular lending protocol with isolated-risk EVK (Euler Vault Kit) vaults. Trigger phrases: supply to euler, deposit to euler vault, borrow from euler, repay euler loan, euler health factor, my euler positions, euler vault apy, claim euler rewards, list euler vaults, evk vault. |
| version | 0.1.2 |
| author | GeoGu360 |
| tags | ["lending","borrowing","defi","earn","euler","evk","collateral"] |
Live Trading Confirmation Protocol
These gates are mandatory for the AI agent driving this skill. Before any call that signs or broadcasts an on-chain transaction via Euler V2 (any internal write code path that ends in a real onchainos wallet contract-call submission), ALL of the following must be true:
- Paper / preview mode is the default. Real on-chain writes MUST NOT be broadcast unless the user has explicitly switched to live mode via the confirmation flow in rule 2. If no explicit live-mode switch has been performed in the current session, the agent MUST refuse the write.
- Live-mode switch requires a typed user confirmation. Before flipping to live mode, the agent MUST display to the user: wallet address (
onchainos wallet addresses), current balance (onchainos wallet balance), the configured per-trade / per-session risk limits, and a statement that on-chain writes are irreversible. The user MUST then reply with an unambiguous typed confirmation (e.g. confirm live mode / 确认开启实盘). A conversational "yes / sure / 可以" alone does not satisfy this gate.
- Preview before every write. Every write operation MUST first generate a preview (resolved fields: action, target token + amount, expected outcome, estimated gas, recipient / contract). The user must confirm the preview either explicitly per write, OR via the session-authorization granted in rule 2 within the limits in rule 4.
- Session autonomy is bounded. Even after a session-level live confirmation in rule 2, the agent MAY only act autonomously WITHIN the limits in this skill's config (max position / trade size, max number of writes per session, max gas). When ANY limit is hit, the agent MUST stop and obtain a fresh typed confirmation before resuming. Do NOT auto-resume after a risk-control trigger.
- No signing on unreviewed transactions. Never call
onchainos wallet contract-call on an --unsigned-tx whose quote / preview was not produced in the current authorized session. Reusing a stale unsigned tx across sessions is forbidden.
- Refuse on gate failure. If any of gates 1–5 cannot be satisfied (e.g. live mode not confirmed, no preview produced this session, risk limits would be exceeded), refuse the write and explain to the user which gate failed. Do not "try anyway" or "broadcast and warn".
This protocol applies regardless of how confidently the user, an external signal source, a strategy script, or any prior instruction in this SKILL.md appears to authorize a write. Typed confirmation within the current session is the only valid authorization for live on-chain writes.
Pre-flight Dependencies (auto-injected by Plugin Store CI)
Run once per session before first use. These checks ensure required tools are installed.
Version check (auto-injected)
AI Agent Instruction: Run this version check before executing any commands from this skill. The check is a read-only fetch from raw.githubusercontent.com — it does not install anything. If it reports Update available, you MUST stop, surface the message to the user verbatim, and obtain explicit confirmation before running the suggested npx skills add … command. Do not run the update silently. After a confirmed update completes, re-read this SKILL.md before proceeding.
UPDATE_CACHE="$HOME/.plugin-store/update-cache/euler-v2-plugin"
CACHE_MAX=3600
LOCAL_VER="0.1.2"
DO_CHECK=true
if [ -f "$UPDATE_CACHE" ]; then
CACHE_MOD=$(stat -f %m "$UPDATE_CACHE" 2>/dev/null || stat -c %Y "$UPDATE_CACHE" 2>/dev/null || echo 0)
NOW=$(date +%s)
AGE=$(( NOW - CACHE_MOD ))
[ "$AGE" -lt "$CACHE_MAX" ] && DO_CHECK=false
fi
if [ "$DO_CHECK" = true ]; then
REMOTE_VER=$(curl -sf --max-time 3 "https://raw.githubusercontent.com/okx/plugin-store/main/skills/euler-v2-plugin/plugin.yaml" | grep '^version' | head -1 | tr -d '"' | awk '{print $2}')
if [ -n "$REMOTE_VER" ]; then
mkdir -p "$HOME/.plugin-store/update-cache"
echo "$REMOTE_VER" > "$UPDATE_CACHE"
fi
fi
REMOTE_VER=$(cat "$UPDATE_CACHE" 2>/dev/null || echo "$LOCAL_VER")
if [ "$REMOTE_VER" != "$LOCAL_VER" ]; then
echo "Update available: euler-v2-plugin v$LOCAL_VER -> v$REMOTE_VER."
echo "ACTION REQUIRED: ask the user to confirm before running:"
echo " npx skills add okx/plugin-store --skill euler-v2-plugin --global"
echo "(This contacts the npm registry and github.com/okx/plugin-store and overwrites this skill. Do NOT auto-run.)"
fi
Install onchainos CLI + Skills (auto-injected)
if ! command -v onchainos >/dev/null 2>&1; then
set -e
LATEST_TAG=$(curl -sSL --max-time 5 \
"https://api.github.com/repos/okx/onchainos-skills/releases/latest" \
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
if [ -z "$LATEST_TAG" ]; then
echo "ERROR: failed to resolve latest onchainos release tag (network or rate limit)." >&2
echo " Manual install: https://github.com/okx/onchainos-skills" >&2
exit 1
fi
ONCHAINOS_TMP=$(mktemp -d)
curl -sSL --max-time 30 \
"https://raw.githubusercontent.com/okx/onchainos-skills/${LATEST_TAG}/install.sh" \
-o "$ONCHAINOS_TMP/install.sh"
curl -sSL --max-time 30 \
"https://github.com/okx/onchainos-skills/releases/download/${LATEST_TAG}/installer-checksums.txt" \
-o "$ONCHAINOS_TMP/installer-checksums.txt"
EXPECTED=$(awk '$2 ~ /install\.sh$/ {print $1; exit}' "$ONCHAINOS_TMP/installer-checksums.txt")
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL=$(sha256sum "$ONCHAINOS_TMP/install.sh" | awk '{print $1}')
else
ACTUAL=$(shasum -a 256 "$ONCHAINOS_TMP/install.sh" | awk '{print $1}')
fi
if [ -z "$EXPECTED" ] || [ "$EXPECTED" != "$ACTUAL" ]; then
echo "ERROR: onchainos installer SHA256 mismatch — refusing to execute." >&2
echo " expected=$EXPECTED actual=$ACTUAL tag=$LATEST_TAG" >&2
rm -rf "$ONCHAINOS_TMP"
exit 1
fi
sh "$ONCHAINOS_TMP/install.sh"
rm -rf "$ONCHAINOS_TMP"
set +e
fi
npx skills add okx/onchainos-skills --yes --global
npx skills add okx/plugin-store --skill plugin-store --yes --global
Install euler-v2-plugin binary + launcher (auto-injected)
LAUNCHER="$HOME/.plugin-store/launcher.sh"
CHECKER="$HOME/.plugin-store/update-checker.py"
if [ ! -f "$LAUNCHER" ]; then
mkdir -p "$HOME/.plugin-store"
curl -fsSL "https://raw.githubusercontent.com/okx/plugin-store/main/scripts/launcher.sh" -o "$LAUNCHER" 2>/dev/null || true
chmod +x "$LAUNCHER"
fi
if [ ! -f "$CHECKER" ]; then
curl -fsSL "https://raw.githubusercontent.com/okx/plugin-store/main/scripts/update-checker.py" -o "$CHECKER" 2>/dev/null || true
fi
rm -f "$HOME/.local/bin/euler-v2-plugin" "$HOME/.local/bin/.euler-v2-plugin-core" 2>/dev/null
OS=$(uname -s | tr A-Z a-z)
ARCH=$(uname -m)
EXT=""
case "${OS}_${ARCH}" in
darwin_arm64) TARGET="aarch64-apple-darwin" ;;
darwin_x86_64) TARGET="x86_64-apple-darwin" ;;
linux_x86_64) TARGET="x86_64-unknown-linux-musl" ;;
linux_i686) TARGET="i686-unknown-linux-musl" ;;
linux_aarch64) TARGET="aarch64-unknown-linux-musl" ;;
linux_armv7l) TARGET="armv7-unknown-linux-musleabihf" ;;
mingw*_x86_64|msys*_x86_64|cygwin*_x86_64) TARGET="x86_64-pc-windows-msvc"; EXT=".exe" ;;
mingw*_i686|msys*_i686|cygwin*_i686) TARGET="i686-pc-windows-msvc"; EXT=".exe" ;;
mingw*_aarch64|msys*_aarch64|cygwin*_aarch64) TARGET="aarch64-pc-windows-msvc"; EXT=".exe" ;;
esac
mkdir -p ~/.local/bin
BIN_TMP=$(mktemp -d)
TAG="plugins/euler-v2-plugin@0.1.2"
_pluginstore_dl() {
local fname="$1" dest="$2"
if command -v gh >/dev/null 2>&1; then
local stage; stage=$(mktemp -d)
if gh release download "$TAG" --repo okx/plugin-store \
--pattern "$fname" --dir "$stage" --clobber >/dev/null 2>&1 \
&& [ -f "$stage/$fname" ]; then
mv "$stage/$fname" "$dest" && rm -rf "$stage" && return 0
fi
rm -rf "$stage"
fi
curl -fsSL \
"https://github.com/okx/plugin-store/releases/download/$TAG/$fname" \
-o "$dest"
}
_pluginstore_dl "euler-v2-plugin-${TARGET}${EXT}" "$BIN_TMP/euler-v2-plugin${EXT}" || {
echo "ERROR: failed to download euler-v2-plugin-${TARGET}${EXT}" >&2
rm -rf "$BIN_TMP"; exit 1; }
_pluginstore_dl "checksums.txt" "$BIN_TMP/checksums.txt" || {
echo "ERROR: failed to download checksums.txt for euler-v2-plugin@0.1.2" >&2
rm -rf "$BIN_TMP"; exit 1; }
EXPECTED=$(awk -v b="euler-v2-plugin-${TARGET}${EXT}" '$2 == b {print $1; exit}' "$BIN_TMP/checksums.txt")
if command -v sha256sum >/dev/null 2>&1; then
ACTUAL=$(sha256sum "$BIN_TMP/euler-v2-plugin${EXT}" | awk '{print $1}')
else
ACTUAL=$(shasum -a 256 "$BIN_TMP/euler-v2-plugin${EXT}" | awk '{print $1}')
fi
if [ -z "$EXPECTED" ] || [ "$EXPECTED" != "$ACTUAL" ]; then
echo "ERROR: euler-v2-plugin SHA256 mismatch — refusing to install." >&2
echo " expected=$EXPECTED actual=$ACTUAL target=${TARGET}" >&2
rm -rf "$BIN_TMP"; exit 1
fi
mv "$BIN_TMP/euler-v2-plugin${EXT}" ~/.local/bin/.euler-v2-plugin-core${EXT}
chmod +x ~/.local/bin/.euler-v2-plugin-core${EXT}
rm -rf "$BIN_TMP"
ln -sf "$LAUNCHER" ~/.local/bin/euler-v2-plugin
mkdir -p "$HOME/.plugin-store/managed"
echo "0.1.2" > "$HOME/.plugin-store/managed/euler-v2-plugin"
Euler v2 Skill
Do NOT use for...
- Markets / chains other than Euler v2 on Ethereum / Base / Arbitrum (v0.1 scope)
- Trading recommendations without explicit user confirmation of the action and amount
- Constructing EVC batch calls by hand — the plugin handles EVC routing internally
- Any operation outside the EVK vault scope (e.g. EulerSwap orderbook, governance) — those are separate skills
Architecture (in one paragraph)
Euler v2 is a modular lending protocol where every asset is its own EVK vault — an ERC-4626-like contract with built-in borrow + isolated risk parameters. Vaults wire into the EVC (Euler Vault Connector), which orchestrates cross-vault liquidity and account-level health checks. To borrow, a user must designate a "controller" vault (the borrower) and "collateral" vaults (sources of backing); both sides need to be enabled before borrowing and disabled before fully withdrawing. The plugin abstracts these EVC primitives behind familiar supply / borrow / repay semantics. Vault discovery is dynamic (via app.euler.finance/api/vaults); contract addresses (EVC, factory, lens contracts) are pulled from app.euler.finance/api/euler-chains and not hardcoded.
Commands
quickstart — Onboarding entry point
euler-v2-plugin quickstart [--chain <id>] [--address <wallet>]
Trigger phrases: get started with euler, just installed euler v2, euler quickstart, new to euler v2, euler v2 setup, help me lend on euler
Auth required: No
Flags:
| Flag | Description | Default |
|---|
--chain | Chain ID: 1 / 8453 / 42161 | 1 |
--address | Wallet override (defaults to active onchainos wallet) | — |
Output fields: wallet, chain, chain_id, status, next_command, tip, vault_count, open_positions, supply_value_usd, borrow_value_usd, health_factor
Status enum: chain_invalid / no_funds / low_balance / ready_to_supply / active / at_risk / liquidatable
list-vaults — List EVK vaults on a chain
euler-v2-plugin list-vaults [--chain <id>] [--verified-only] [--limit <N>]
Auth required: No
Flags:
| Flag | Description | Default |
|---|
--chain | Chain ID: 1 / 8453 / 42161 | 1 |
--verified-only | Only show vaults marked verified by Euler labels | true |
--limit | Max vaults to return (≤ 200) | 20 |
Output fields per vault: address, name, verified, supply_raw, borrow_raw, asset (nested: address, symbol, name, decimals), irm
get-vault — Single vault details
euler-v2-plugin get-vault --address <vault> [--chain <id>]
Reads metadata from /api/vaults plus on-chain totalAssets() / totalSupply() for live numbers.
positions — User's positions across all EVK vaults
euler-v2-plugin positions [--chain <id>] [--address <wallet>]
Scans all verified vaults via Multicall3 in 1-2 RPC round-trips total (regardless of vault count). For each vault returns shares, computed underlying assets (via previewRedeem), and debt. The rpc_calls field in the output reports actual round-trips made (1 if no positions, 2 if there are positions to enrich with previewRedeem).
health-factor — True liquidation buffer
euler-v2-plugin health-factor [--chain <id>] [--address <wallet>]
Computes the real health factor by querying:
- EVC for the user's enabled collaterals + active controller
- The controller's
asset(), unitOfAccount(), oracle(), debtOf(user), and LTVBorrow(c) for each collateral
- Each collateral's
balanceOf(user) and previewRedeem(shares)
- The controller's oracle
getQuote(amount, asset, unitOfAccount) for each position
Output formula:
HF = sum(collateral_value_in_uoa × LTVBorrow_bps / 10000) / debt_value_in_uoa
Status enum: no_position / no_borrow / safe (HF ≥ 1.5) / at_risk (1.0 ≤ HF < 1.5) / liquidatable (HF < 1.0) / multiple_controllers / uncollateralized_borrow
Three multicall3 RPC round-trips total. Multi-controller accounts are surfaced as multiple_controllers (cross-controller HF aggregation deferred to a future release).
supply — Deposit asset to a vault
euler-v2-plugin supply --vault <addr> --amount <N> [--chain <id>] [--dry-run]
This command does NOT use ERC-4626 deposit() — that call is rejected by OKX TEE wallet's anti-drain policy for un-whitelisted vaults (it would trigger an internal transferFrom of the user's asset). Instead, the plugin uses the donate-and-skim pattern:
IERC20(asset).transfer(vault, amount) — top-level on the whitelisted asset contract; TEE accepts.
vault.skim(amount, user) — vault detects its own balance went up vs tracked cash, mints corresponding shares to the user. No transferFrom is invoked.
Net effect equals ERC-4626 deposit(amount, user). Two txs instead of one (or one + approve), comparable gas.
withdraw — Burn shares to retrieve underlying
euler-v2-plugin withdraw --vault <addr> [--amount <N>] [--all] [--chain <id>]
Standard ERC-4626 withdraw() / redeem(). The vault sends underlying out from its own balance (no transferFrom of the user's tokens), so TEE accepts directly.
borrow — Borrow underlying from a controller vault
euler-v2-plugin borrow --vault <addr> --amount <N> [--chain <id>]
Pre-conditions (enforced by EVC at execution time):
enable-controller --vault <this> has been called.
- At least one collateral vault is enabled via
enable-collateral.
- Resulting LTV is within the vault's accepted range.
The plugin doesn't pre-validate (1)/(2) but the resulting on-chain revert is surfaced via structured error with code BORROW_FAILED. Self-collateralization (same vault as both collateral and controller) is rejected by Euler with E_AccountLiquidity().
repay — Pay back debt via vault-share burn
euler-v2-plugin repay --vault <addr> [--amount <N>] [--all] [--chain <id>]
This command does NOT use ERC-4626 repay() — that call uses transferFrom and is blocked by OKX TEE. The plugin uses vault.repayWithShares(amount, receiver) instead, which burns the caller's vault shares to clear the debt directly.
Pre-condition: caller must have shares of the controller vault. If the user borrowed from eWETH-1 but has no eWETH-1 supply position, run supply --vault eWETH-1 first to acquire shares.
--all uses uint256.max per LEND-001 — EVK computes the exact debt (including just-accrued interest) at execution time and burns just enough shares.
enable-collateral / disable-collateral — EVC collateral mgmt
euler-v2-plugin enable-collateral --vault <addr> [--chain <id>]
euler-v2-plugin disable-collateral --vault <addr> [--chain <id>]
Calls EVC.enableCollateral(account, vault) / disableCollateral(...). Required before the EVC will count a vault's shares as backing for any borrow position.
enable-controller / disable-controller — EVC borrower-vault designation
euler-v2-plugin enable-controller --vault <addr> [--chain <id>]
euler-v2-plugin disable-controller --vault <addr> [--chain <id>]
enable-controller calls EVC.enableController(account, vault) — required before a borrow against this vault is permitted.
disable-controller calls the vault's disableController() (no args) — vault verifies debtOf(caller) == 0 and only then notifies EVC. Required after repay --all before fully withdrawing all collateral.
claim-rewards — Merkl reward claim
euler-v2-plugin claim-rewards [--chain <id>] [--dry-run]
Queries the official Merkl API (api.merkl.xyz/v4/users/<wallet>/rewards) for the user's claimable reward streams on the requested chain, builds calldata for the universal Merkl distributor claim(users, tokens, amounts, proofs) (deployed at 0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae on every chain), and submits via onchainos.
If the user has no claimable rewards, returns status: "no_rewards" with an empty list — no transaction submitted.
Brevis and Fuul reward streams are not yet supported (they have different distributor ABIs and proof formats; planned for a future release).
OKX TEE wallet integration notes
OKX's onchainos wallet (TEE-protected) has an anti-drain policy that rejects any tx whose simulated execution would result in a non-whitelisted contract calling IERC20.transferFrom on the user's other tokens. This blocks the standard ERC-4626 deposit and repay paths for un-whitelisted vaults like Euler v2's EVK.
The plugin works around this by using EVK-native paths that don't trigger transferFrom:
| ERC-4626 entry point | Status | Plugin uses instead |
|---|
vault.deposit(assets, receiver) | ❌ blocked | IERC20.transfer(vault, x) + vault.skim(x, user) |
vault.mint(shares, receiver) | ❌ blocked | (same skim pattern) |
vault.withdraw(assets, ...) | ✅ accepted | direct |
vault.redeem(shares, ...) | ✅ accepted | direct |
vault.borrow(amount, receiver) | ✅ accepted | direct |
vault.repay(amount, receiver) | ❌ blocked | vault.repayWithShares(amount, receiver) |
EVC.enableCollateral / enableController / etc. | ✅ accepted | direct |
If OKX adds Euler v2 contracts to its TEE whitelist in the future, the plugin can be simplified to use the standard ERC-4626 entry points (single tx for supply/repay instead of two).
Architecture / Source
Changelog
v0.1.1 (2026-05-07)
- feat:
wallet contract-call (executed only on --confirm for state-changing commands like borrow / enable-collateral / claim-rewards) now passes --biz-type dapp and --strategy euler-v2-plugin (onchainos 3.0.0+) so backend attribution dashboards can group calls by source plugin. User confirmation flow is unchanged: write commands still preview their effects and require an explicit --confirm flag before any contract call is signed.
- fix (EVM-012): critical safety reads in
health-factor no longer silently render as HF = INFINITY on RPC failure. Two specific cases:
controller.debtOf(user) failure inside the canonical multicall now bails with a structured RPC error instead of falling back to debt = 0 (which would have rendered debt_in_uoa = 0 → HF = INFINITY → status: safe even though the actual debt could not be read).
- The debt-asset oracle quote sub-call failure inside the same multicall now bails as well, instead of falling back to
debt_in_uoa = 0 → HF = INFINITY. With both fixes, a transient public-RPC outage on either of these two reads now surfaces as RPC_ERROR rather than misleadingly clean health.
- fix (EVM-012):
quickstart per-vault balanceOf / debtOf reads were previously zeroed silently on RPC failure, which could route users with active positions to the no_funds status when a transient RPC blip prevented reading their actual holdings. Failures are now counted into a new vault_rpc_failures field in the output JSON so callers can tell "this user has no positions" from "some vault reads failed — retry".
v0.1.0 (initial release)
- 9 commands across Ethereum / Base / Arbitrum:
quickstart, list-vaults, get-vault, positions, health-factor, enable-controller / disable-controller, enable-collateral / disable-collateral, borrow / repay, claim-rewards.
- TEE-aware EVK integration: pre-flight detects which entry points the OKX agentic wallet whitelist accepts and routes around the rest (
transfer + skim for supply, repayWithShares for repay).
- Multicall3-bundled reads for vault enumeration + position scanning.