Imported from okx/plugin-store (
skills/euler-v2-plugin/SKILL.md). Install upstream withnpx skills add okx/plugin-store --skill euler-v2-plugin. Copyright stays with the author.
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-callon an--unsigned-txwhose 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 reportsUpdate available, you MUST stop, surface the message to the user verbatim, and obtain explicit confirmation before running the suggestednpx skills add …command. Do not run the update silently. After a confirmed update completes, re-read this SKILL.md before proceeding.
# Check for skill updates (1-hour cache).
# Network scope: this block calls raw.githubusercontent.com for the version probe only.
# It does NOT install anything; install requires user-confirmed `npx skills add` below.
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)
# 1. Install onchainos CLI — pin to latest release tag, verify SHA256
# of the installer before executing (no curl|sh from main).
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
# 2. Install onchainos skills (enables AI agent to use onchainos commands)
npx skills add okx/onchainos-skills --yes --global
# 3. Install plugin-store skills (enables plugin discovery and management)
npx skills add okx/plugin-store --skill plugin-store --yes --global
Install euler-v2-plugin binary + launcher (auto-injected)
# Install shared infrastructure (launcher + update checker, only once)
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
# Clean up old installation
rm -f "$HOME/.local/bin/euler-v2-plugin" "$HOME/.local/bin/.euler-v2-plugin-core" 2>/dev/null
# Download binary
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
# Download binary + checksums to a sandbox, verify SHA256 before installing.
# Fail-closed: any mismatch / missing checksum entry refuses the install.
# Matches the producer-side workflow at
# .github/workflows/plugin-publish.yml which uploads `checksums.txt`
# alongside the 9 platform binaries under each release tag.
BIN_TMP=$(mktemp -d)
TAG="plugins/euler-v2-plugin@0.1.2"
# Robust asset download. Prefer `gh release download` — it resolves the
# asset via the GitHub API and follows the signed-redirect properly,
# which avoids edge cases observed where curl on
# `releases/download/<tag with slash>/<file>` 404s under some
# proxy / curl-version combinations. Falls back to raw curl if gh is
# not installed.
_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"
# Symlink CLI name to universal launcher
ln -sf "$LAUNCHER" ~/.local/bin/euler-v2-plugin
# Register version
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), andLTVBorrow(c)for each collateral - Each collateral's
balanceOf(user)andpreviewRedeem(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 trackedcash, mints corresponding shares to the user. NotransferFromis 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
- Source code: https://github.com/GeoGu360/plugin-store/tree/main/skills/euler-v2-plugin
- Euler v2 docs: see the EVK whitepaper on the Euler Finance docs site
- Euler app: https://app.euler.finance
Changelog
v0.1.1 (2026-05-07)
- feat:
wallet contract-call(executed only on--confirmfor state-changing commands likeborrow/enable-collateral/claim-rewards) now passes--biz-type dappand--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--confirmflag before any contract call is signed. - fix (EVM-012): critical safety reads in
health-factorno longer silently render asHF = INFINITYon RPC failure. Two specific cases:controller.debtOf(user)failure inside the canonical multicall now bails with a structured RPC error instead of falling back todebt = 0(which would have rendereddebt_in_uoa = 0 → HF = INFINITY → status: safeeven 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 asRPC_ERRORrather than misleadingly clean health.
- fix (EVM-012):
quickstartper-vaultbalanceOf/debtOfreads were previously zeroed silently on RPC failure, which could route users with active positions to theno_fundsstatus when a transient RPC blip prevented reading their actual holdings. Failures are now counted into a newvault_rpc_failuresfield 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 + skimfor supply,repayWithSharesfor repay). - Multicall3-bundled reads for vault enumeration + position scanning.