YO Protocol Plugin
[!IMPORTANT] Complete the short Base MCP onboarding flow defined inSKILL.mdbefore calling anything here. The user’s wallet address — required for positions and writes — is fetched lazily viaget_walletswhen a flow needs it. This plugin is 100% on-chain: it makes no HTTP calls and needs no allowlist.
Overview
YO Protocol is an ERC-4626 yield aggregator with ERC-7540-style async redemption, live on Base (8453), Ethereum (1), and Arbitrum (42161). Both deposit and withdraw go through the YO Gateway (0xF1EeE0957267b1A474323Ff9CfF7719E964969FA, same address on every chain) — never direct vault calls. The Gateway handles slippage, partner attribution, and instant/async routing: a single redeem settles instantly when the vault has idle assets, otherwise it queues and a YO solver fulfills it on-chain within ~24h — the user never sends a second transaction.
This plugin builds unsigned calldata from a baked-in vault registry and submits it via Base MCP send_calls; all reads go through chain_rpc_request (eth_call). Everything is derived on-chain — no external service, no CLI, no allowlist. Live APY/yield is not on-chain data, so getVaults reports on-chain TVL and share price but not APY. Deposits are same-chain only — cross-chain/zap routing is out of scope; use the YO dapp for that.
Surface Routing
Everything routes through Base MCP’s own onchain tools, so the plugin behaves identically on every surface (Claude Code, Cursor, Codex, Claude.ai, ChatGPT). No shell, no allowlist.
No shell-less or chat-only fallback is needed because
chain_rpc_request and send_calls are Base MCP server tools available on all surfaces. Operate only on base, ethereum, arbitrum (the chains Base MCP supports where YO is deployed). YO also has deployments on chains Base MCP can’t reach (e.g. X Layer, Katana) — those are omitted; direct the user to the YO dapp for them.
Orchestration
All reads arechain_rpc_request{ chain, method:"eth_call", params:[{ to, data }, "latest"] }. Encode calldata by left-padding each argument to a 32-byte word and concatenating after the 4-byte selector (see Calldata reference). Always pass the chain name (base/ethereum/arbitrum), never the numeric ID.
getVaults
The vault set is the baked-in Vault registry (verified on-chain). For live numbers, read each vault on its chain:yoPositions
A user’s holdings, read on-chain — block-fresh. This mirrors the dapp (usePositionValues → useChainSharesBalances + useChainConvertToAssetsBatch): a logical position aggregates every deployment that shares a vault id across chains.
convertToAssets, not previewRedeem, for displayed value — previewRedeem deducts the withdrawal fee and understates the holding. Report in underlying-asset units; do not attempt a USD conversion, and never use get_portfolio for YO vault tokens (its pricer mis-handles ERC-4626 shares — see Notes).
deposit
Same-chain deposit only. Approve the underlying to the Gateway, then callGateway.deposit.
partnerId is uint32 — pass 0. For native ETH into yoETH there is no ERC-20 to approve: the underlying is WETH, so the user must hold WETH first (wrap, then the two-call batch above). This plugin is same-chain only — cross-chain/zap deposits are out of scope; use the YO dapp.
withdraw
Approve the vault share token to the Gateway (first time only), then callGateway.redeem.
pendingRedeemRequest(user) on the vault — when pendingShares == 0 and the user’s underlying balance has grown, it settled. No user-side claim transaction is needed for async redeems — a YO solver fulfills them on-chain.
Submission
Target tool:send_calls (EIP-5792 batch of unsigned { to, value, data } calls). The plugin produces raw calldata locally — there is no semantic swap/send step and no external service in the path; reads use chain_rpc_request and never touch send_calls.
Mapping into send_calls:
chain— the deployment’s chain name:base,ethereum, orarbitrum(never the numeric ID).calls[]— ordered, approval before action:- deposit →
[ approve(underlying → Gateway, amountIn), Gateway.deposit(...) ] - withdraw →
[ approve(vault share → Gateway, maxUint256)?, Gateway.redeem(...) ](approve omitted whenallowance ≥ shares)
- deposit →
- Each call:
to= target contract,value="0x0"(no native value — yoETH uses WETH),data= selector + 32-byte-padded args. - After submission, poll
get_request_status(requestId)for confirmation. See../references/approval-mode.mdfor the approval URL flow and../references/batch-calls.mdfor batching.
Example Prompts
“Show me the YO vaults.”- List the Vault registry.
- For each deployment on its chain:
chain_rpc_requesttotalAssets()+totalSupply()→ TVL and share price. - Present vault, chain(s), underlying, TVL, share price. Note APY isn’t available on-chain.
get_wallets→ user.- For yoUSD on
base,ethereum,arbitrum(all USDC):balanceOf(user)(skip 0) →convertToAssets(shares); sum the three (same underlying). pendingRedeemRequest(user)on each → flag any in-flight redeem.- Report the summed total as USDC. If the user also holds yoUSDT, report it as a separate USDT line.
get_wallets→ user;amountIn = 1000000.convertToShares(1000000)on yoUSD/base →expectedShares;minSharesOut = expectedShares*9950/10000.send_calls(chain="base", calls=[ approve(USDC → Gateway, 1000000), Gateway.deposit(yoUSD, 1000000, minSharesOut, user, 0) ]).- User approves →
get_request_status.
get_wallets→ user.balanceOf(user)on yoUSD/base →shares;previewRedeem(shares)→expectedAssets;minAssetsOut = expectedAssets*9950/10000.allowance(user, Gateway)→ approve needed?send_calls(chain="base", calls=[ approve(yoUSD → Gateway, max)?, Gateway.redeem(yoUSD, shares, minAssetsOut, user, 0) ]).- User approves →
get_request_status.
Risks & Warnings
- slippage — deposits set
minSharesOutand withdraws setminAssetsOutfrom a fresh on-chain quote with a default 50 bps tolerance; the call reverts rather than filling materially worse. Show the user the expected shares/assets before they approve. Never silently widen slippage — if a quote looks off, stop and surface it. - irreversible —
send_callsbroadcasts on-chain writes that cannot be undone. Verify vault address, amount, recipient (user), and chain before presenting the approval. A queued async redeem settles on its own (~24h) — don’t resubmit it as a second transaction. - Never ask for or use a private key. Never use a local signer,
cast send, or browser-wallet helper. Do not sign or broadcast outside Base MCP. - Approvals go to the Gateway, not the vault: underlying-token approve for deposits, vault-share approve for withdraws.
amountIn/sharesare raw base-unit integer strings — never scientific notation.
Notes
Chains
The same vault
id is deployed at the same share-token address on every chain (e.g. yoUSD is 0x0000000f… on Base, Ethereum, and Arbitrum) — the differentiator is the chain you call it on.
Vault registry
Baked-in and verified on-chain (symbol, decimals, andasset() read live on each chain). One row per deployment; the share token is the ERC-4626 vault you read/redeem, the underlying is what you approve for deposits.
The share-token address is identical across a vault’s chains; only the underlying address differs per chain (e.g. WETH is
0x4200…0006 on Base but 0xC02a…56Cc2 on Ethereum). Deployments that share a vault id and the same underlying symbol sum into one position — yoUSD (USDC on base/eth/arb), yoETH (WETH on base/eth), yoBTC (cbBTC on base/eth), yoEUR (EURC on base/eth), yoloUSD (USDC on base/eth/arb). yoUSDT is a yoUSD secondary with a different underlying (USDT) — report it as its own USDT line, never added to the USDC total. yoUSD is also deployed on X Layer (196, USDTO) and Katana (747474, vbUSDC); Base MCP can’t reach those, so they’re omitted from on-chain totals — note that to the user and point them at the YO dapp.
Gateway: 0xF1EeE0957267b1A474323Ff9CfF7719E964969FA — verified deployed on Base, Ethereum, and Arbitrum (identical bytecode). It is the spender for all approvals: approve the underlying for deposits, the vault share token for withdraws.
[!CAUTION] Never report YO positions fromget_portfolio. Its pricer does not understand ERC-4626 vault shares and mis-prices them by 10–10,000×. Report positions in underlying-asset units fromconvertToAssets(shares)instead.get_portfoliois fine for plain ERC-20 holdings — just not YO vault tokens.
Calldata reference
Encode args by left-padding each value to a 32-byte word and concatenating after the 4-byte selector. Read selectors (eth_call against the share token):
Write selectors (
send_calls calldata):
[!WARNING]partnerIdisuint32. Encoding it asuint256changes the selector to a function that does not exist on the Gateway and the call reverts. The signature must be exactly(address,uint256,uint256,address,uint32). PasspartnerId = 0when no partner ID is assigned.maxUint256=0xff…ff(64 f’s).