diff --git a/site/src/app/styles.scss b/site/src/app/styles.scss index 0fc30cf..ac901e1 100644 --- a/site/src/app/styles.scss +++ b/site/src/app/styles.scss @@ -314,7 +314,7 @@ } @each $filter - in ("Smart Contracts", "Agentic Payments", Frontend, Assets, APIs, ZK, Wallets, Ecosystem) + in ("Smart Contracts", "Agentic Payments", Frontend, Assets, APIs, ZK, Wallets, DeFi, Ecosystem) { .SkillsLanding__filterPanel[data-active-filter="#{$filter}"] .SkillsLanding__filterItem[data-category="#{$filter}"] { diff --git a/site/src/data/skills.ts b/site/src/data/skills.ts index d000487..de899a6 100644 --- a/site/src/data/skills.ts +++ b/site/src/data/skills.ts @@ -23,6 +23,7 @@ type FilterType = | "APIs" | "ZK" | "Wallets" + | "DeFi" | "Ecosystem"; /** @@ -38,6 +39,7 @@ export const FILTERS: readonly FilterType[] = [ "APIs", "ZK", "Wallets", + "DeFi", "Ecosystem", ] as const; @@ -122,6 +124,13 @@ export const SKILL_CARD_SOURCES: readonly SkillCardSource[] = [ description: "Use the Stellar CLI as a wallet: manage keys, fund accounts, check balances, transfer tokens, handle trustlines, and diagnose transactions.", }, + { + source: "skills/defi/SKILL.md", + category: "DeFi", + title: "DeFi Protocols (Blend)", + description: + "Act on Soroban DeFi protocols from the CLI. Includes the Blend lending playbook: discover pools, read pool and backstop state over RPC, supply, borrow, repay, and provide backstop capital.", + }, { source: "skills/zk-proofs/SKILL.md", category: "ZK", diff --git a/skills/defi/SKILL.md b/skills/defi/SKILL.md new file mode 100644 index 0000000..b90f683 --- /dev/null +++ b/skills/defi/SKILL.md @@ -0,0 +1,31 @@ +--- +name: defi +description: Interact with Stellar/Soroban DeFi protocols from the CLI: discover pools and markets, read live protocol state over RPC, and compose the transactions that act on them (lend, borrow, provide backstop/liquidity). Per-protocol playbooks map each protocol's operations to concrete `contract invoke` calls. Use when acting on a Soroban DeFi protocol rather than moving value between accounts. +user-invocable: true +argument-hint: "[defi task]" +--- + +# DeFi: acting on Soroban protocols + +The [wallet skill](../wallet/SKILL.md) covers wallet **primitives** — moving value, trustlines, transaction mechanics. It deliberately does *not* teach how any specific Soroban protocol works. + +These DeFi playbooks pick up where the wallet skill stops. Each one maps a protocol's operations to concrete `contract invoke` calls, carries the current contract references, and shows how to read the protocol's live state over RPC — so you act on the protocol instead of reverse-engineering it from raw ledger data. + +The division of labor is constant: + +- **This skill** — what the protocol is, which contract/function each operation maps to, and how to read its state. +- **The wallet skill** — keys, signing, fees, submission, and reading account/token balances. Every transaction here is signed and submitted with the wallet pipeline. + +## Playbooks + +| Protocol | What it is | Playbook | +|----------|------------|----------| +| Blend | Permissionless lending pools + a backstop insurance module | [blend/SKILL.md](blend/SKILL.md) | + + +## Related skills + +- Keys, signing, fees, submission, balances → [`../wallet/SKILL.md`](../wallet/SKILL.md) +- SEP-41 tokens and SACs these protocols call → [`../smart-contracts/SKILL.md`](../smart-contracts/SKILL.md) +- Reading chain data over RPC → [`../data/SKILL.md`](../data/SKILL.md) +- Classic assets and trustlines → [`../assets/SKILL.md`](../assets/SKILL.md) diff --git a/skills/defi/blend/SKILL.md b/skills/defi/blend/SKILL.md new file mode 100644 index 0000000..5178870 --- /dev/null +++ b/skills/defi/blend/SKILL.md @@ -0,0 +1,141 @@ +--- +name: blend +description: Blend v2 lending protocol playbook for the Stellar CLI. Covers Blend's architecture (permissionless pools, reserves, the backstop insurance module, BLND emissions), the v2 mainnet/testnet contract references, how to discover pools and read live pool/backstop state over RPC, how to check accrued BLND emissions and project position earnings, and how to build the core lending and backstop transactions with `contract invoke`. Use when supplying, borrowing, repaying, withdrawing, claiming emissions, estimating yield, or providing backstop capital on Blend from the command line. +user-invocable: true +argument-hint: "[blend task]" +--- + +# Blend v2: lending & backstop playbook + +Blend is a permissionless, over-collateralized lending protocol on Soroban. Anyone can create an isolated lending **pool**; each pool holds one or more **reserves** (assets you can supply and borrow) and is insured by a **backstop** of first-loss capital that earns a share of interest plus BLND emissions. + +This playbook covers **Blend v2** — the current deployment on both mainnet and testnet. All addresses, signatures, and semantics below are v2; the legacy v1 mainnet deployment (different addresses, different backstop `claim` shape, 21-day Q4W) is out of scope. + +This playbook maps each Blend operation to the `contract invoke` call that performs it, and shows how to read live pool/backstop state over RPC. Keys, signing, fees, and submission are handled by the [wallet skill](../../wallet/SKILL.md) — every transaction below is signed and submitted with that pipeline. + +> **Scope.** This playbook targets **Blend v2**. Confirm the deployed interface with `stellar contract info interface --id ` — see [reading state](#reading-blend-state-over-rpc). The addresses below are the **Blend v2** deployments on both networks; the older v1 mainnet deployment uses different addresses and its ABI can differ. + +## Read the file that matches the task + +| Task | File | +|------|------| +| Supply, borrow, repay, withdraw; read reserves, positions, pool config; check/claim BLND emissions; project position earnings (supply APY + emissions) | [lending.md](lending.md) | +| Deposit to a backstop, queue/withdraw (Q4W), preview/claim backstop emissions; find the most profitable backstop | [backstop.md](backstop.md) | + +## Architecture + +| Entity | What it is | +|--------|------------| +| **Pool** | One isolated lending market. Holds all reserves, user positions, and interest accounting. You interact with a pool almost entirely through its `submit` entrypoint. Each pool references one oracle and one backstop. | +| **Reserve** | A single asset within a pool. Supply is tracked as **bTokens** (supply/collateral shares), debt as **dTokens** (liability shares); their value grows via `b_rate` / `d_rate` indexes. These are internal accounting units, not transferable SEP-41 tokens. | +| **Pool Factory** | Deploys pools deterministically and records which pool addresses it created (`is_pool`). The backstop trusts only factory-deployed pools. To enumerate pools, use the backstop's reward zone — see [discovering pools](lending.md#discovering-pools). | +| **Backstop** | Holds first-loss capital per pool, denominated in the **backstop token** — a Comet (Balancer-style) 80/20 BLND:USDC LP token (confirm weights with the LP token's `get_normalized_weight`). Backstop depositors absorb bad debt in exchange for a share of interest + BLND emissions. See [backstop.md](backstop.md). | +| **Emitter** | Streams BLND emissions to the backstop, distributed to pools in the reward zone. | +| **Oracle** | A SEP-40-style price oracle the pool reads for asset prices. Address is per-pool (`PoolConfig.oracle`); read it, don't assume it. | + +## Contract references + +Only these three addresses per network are treated as fixed literals. **Everything else is derived on-chain**: the BLND token, the backstop LP token, each pool's oracle, and the pools themselves. + +| Role | Mainnet | Testnet | +|------|---------|---------| +| Pool Factory | `CDSYOAVXFY7SM5S64IZPPPYB4GVGGLMQVFREPSQQEZVIWXX5R23G4QSU` | `CDV6RX4CGPCOKGTBFS52V3LMWQGZN3LCQTXF5RVPOOCG4XVMHXQ4NTF6` | +| Backstop | `CAQQR5SWBXKIGZKPBZDH3KM5GQ5GUTPKB7JAFCINLZBC5WXPJKRG3IM7` | `CBDVWXT433PRVTUNM56C3JREF3HIZHRBA64NB2C3B2UNCKIS65ZYCLZA` | +| Emitter | `CCOQM6S7ICIUWA225O5PSJWUBEMXGFSSW2PQFO6FP4DQEKMS5DASRGRR` | `CC3WJVJINN4E3LPMNTWKK7LQZLYDQMZHZA7EZGXATPHHBPKNZRIO3KZ6` | + +Both columns are the **Blend v2** deployment, from the [blend-utils registry](https://github.com/blend-capital/blend-utils) (`mainnet.contracts.json` / `testnet.contracts.json`) and the [Blend docs deployment list](https://docs.blend.capital/mainnet-deployments). A separate v1 deployment still exists on mainnet at different addresses (v1 backstop `CAO3AGAM…`, v1 pool factory `CCZD6ESM…`); this playbook does not cover it — if a user hands you a pool address, confirm it was deployed by the v2 factory before assuming v2 semantics. + +### Network setup + +Set the three literals once. **Testnet works out of the box** (the CLI ships a testnet RPC): + +```bash +# Testnet (v2) +POOL_FACTORY=CDV6RX4CGPCOKGTBFS52V3LMWQGZN3LCQTXF5RVPOOCG4XVMHXQ4NTF6 +BACKSTOP=CBDVWXT433PRVTUNM56C3JREF3HIZHRBA64NB2C3B2UNCKIS65ZYCLZA +EMITTER=CC3WJVJINN4E3LPMNTWKK7LQZLYDQMZHZA7EZGXATPHHBPKNZRIO3KZ6 +NETWORK=testnet +ME=alice # any funded key alias — required even for reads (they only build a simulation) +``` + +**Mainnet does not work out of the box**: the CLI's built-in `mainnet` network is a placeholder with **no RPC URL** — every command fails until you add one. Pick a public endpoint from the [standards skill's mainnet RPC list](../../standards/SKILL.md#public-mainnet-rpc-endpoints) and register it first: + +```bash +# Mainnet (v2) — add an RPC endpoint first; the built-in `mainnet` alias has none +stellar network add mainnet \ + --rpc-url \ + --network-passphrase "Public Global Stellar Network ; September 2015" + +POOL_FACTORY=CDSYOAVXFY7SM5S64IZPPPYB4GVGGLMQVFREPSQQEZVIWXX5R23G4QSU +BACKSTOP=CAQQR5SWBXKIGZKPBZDH3KM5GQ5GUTPKB7JAFCINLZBC5WXPJKRG3IM7 +EMITTER=CCOQM6S7ICIUWA225O5PSJWUBEMXGFSSW2PQFO6FP4DQEKMS5DASRGRR +NETWORK=mainnet +ME=alice +``` + +Every example below passes `--network "$NETWORK"` explicitly — keep that habit. A read without `--network` silently queries whatever network the command defaults to, and a funded account "looks empty" because you're asking the wrong chain. + +### Derive the rest on-chain + +```bash +# Backstop LP token (Comet BLND:USDC) — from the Backstop +LP_TOKEN=$(stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=no -- backstop_token | tr -d '"') + +# BLND and USDC token addresses — the Comet LP token lists its underlying tokens, +# returned in weight order: [BLND, USDC]. +# (The Emitter exposes no BLND getter; derive BLND from the LP token.) +stellar contract invoke --id "$LP_TOKEN" --source "$ME" --network "$NETWORK" --send=no -- get_tokens + +# Pool weights (e.g. confirm the 80/20 BLND:USDC split — returns "8000000" = 80%) +stellar contract invoke --id "$LP_TOKEN" --source "$ME" --network "$NETWORK" --send=no -- get_normalized_weight --token +``` + +## Prerequisites + +- The [wallet skill](../../wallet/SKILL.md) set up: a funded key, a configured RPC/network, and a trustline to any classic asset you supply, repay, **or borrow/withdraw** (receiving from the pool also requires the trustline). +- A **BLND trustline before any emissions `claim`** — even the simulated preview reverts with `#13` (SAC `TrustlineMissingError`) without it. Derive BLND from the LP token (`backstop_token` → `get_tokens`, first entry), read its `CODE:ISSUER` with `name()`, then `stellar tx new change-trust --line `. +- `stellar-cli` installed. Confirm a contract's real interface before invoking: `stellar contract info interface --id `. + +### Getting the assets these flows assume + +Blend flows assume a balance of a reserve asset (USDC, etc.) or of BLND/USDC for the backstop — but Friendbot funds only XLM, and issuer `mint` is gated. Starting from XLM you have two routes: + +- **Borrow it**: supply XLM as collateral, then borrow the asset you need — see [lending.md](lending.md). (This is also the practical way to fund a backstop position from XLM.) +- **Swap for it on the DEX**: `stellar tx new path-payment-strict-receive` (or `-strict-send`) — see the [wallet skill](../../wallet/SKILL.md). + +Two gotchas: XLM's SAC address comes from `stellar contract id asset --asset native` (`contract invoke --id native` does not work). And "USDC" is not one asset — testnet's two Blend pools use **two different USDC issuers**, one of which has no DEX liquidity. Read the token's `name()` (returns `CODE:ISSUER`) before assuming which one you hold or swap for. + +## Reading Blend state over RPC + +All reads are `contract invoke` on a getter — the CLI simulates the call and returns the decoded value, no indexer involved. **Pass `--send=no` on every read.** By default the CLI decides for itself whether to submit, and against TTL-archived ledger state a plain getter will skip simulation and **auto-submit a real RestoreFootprint transaction** — a signed, fee-paying write triggered by a "read" (abandoned pools and dead oracles are exactly where you hit this). A surprise `Account not found` error on a read is the symptom of archived state: stop and investigate; don't retry, because the retry is what submits the restore. + +Note `--source-account` is still **required** to build the simulation (any funded key works); only `contract info interface` runs without one. Inspect the exact ABI first: + +```bash +stellar contract info interface --id --network "$NETWORK" +``` + +- **Pool state** (reserve list, reserve data, config, positions) → [lending.md](lending.md#reading-pool-state). +- **Backstop state** (pool balance, user balance, Q4W, emissions) → [backstop.md](backstop.md#reading-backstop-state). + +## Building transactions + +State-changing calls (`submit`, backstop `deposit`, etc.) go through `contract invoke ... --send=yes` signed by a wallet key. Token movements into the protocol (the asset you supply/repay) are ordinary SEP-41 / SAC transfers the contract pulls via authorization; the CLI collects and signs the required auth entries automatically during simulation. + +Mechanics that surprise people: + +- **`--send` is an invoke-only convention.** `stellar tx new …` operations (trustlines, path payments, account merges) take no `--send` flag and **submit by default**. +- **Soroban writes carry resource fees** — expect ~0.09 XLM per pool `submit`/`deposit` on top of the inclusion fee, deducted even though the examples don't mention a fee flag. +- **On `transaction submission timeout`**: public RPCs drop low-fee Soroban transactions under surge pricing. First verify the transaction didn't land (`stellar tx fetch result`, or check the account's sequence — see the wallet skill's [diagnose + safe-retry rules](../../wallet/accounts-and-tx.md)), then retry with a higher bid: `--inclusion-fee 1000000`. (`--fee` is deprecated; use `--inclusion-fee`.) + +## Related skills + +- Keys, signing, fees, submission, balances → [`../../wallet/SKILL.md`](../../wallet/SKILL.md) +- The DeFi index and other protocol playbooks → [`../SKILL.md`](../SKILL.md) +- Inspecting a contract's interface / SEP-41 tokens → [`../../smart-contracts/SKILL.md`](../../smart-contracts/SKILL.md) +- Reading chain data over RPC → [`../../data/SKILL.md`](../../data/SKILL.md) + +## Documentation + +- [Blend docs](https://docs.blend.capital) +- [blend-contracts-v2 (source of truth for the ABI)](https://github.com/blend-capital/blend-contracts-v2) diff --git a/skills/defi/blend/backstop.md b/skills/defi/blend/backstop.md new file mode 100644 index 0000000..9d45645 --- /dev/null +++ b/skills/defi/blend/backstop.md @@ -0,0 +1,138 @@ +# Blend backstop: deposit, Q4W, withdraw, claim + +## Contents + +- [Deposit](#deposit) +- [Queue for withdrawal (Q4W)](#queue-for-withdrawal-q4w) +- [Claim backstop emissions](#claim-backstop-emissions) +- [Reading backstop state](#reading-backstop-state) +- [Find the most profitable backstop](#find-the-most-profitable-backstop) (incl. `scripts/rank-backstops.sh`) + +The backstop is Blend's insurance layer. Depositors stake the **backstop token** — a Comet 80/20 BLND:USDC LP token (confirm weights with the LP token's `get_normalized_weight`) — behind a specific pool. In return they earn a share of that pool's interest plus BLND emissions. In exchange, backstop capital is **first-loss**: it absorbs bad debt before lenders take a hit. That risk is why exits are time-locked (see [Q4W](#queue-for-withdrawal-q4w)). + +Everything here targets the Backstop contract. `BACKSTOP`, `NETWORK`, and `ME` come from [network setup](SKILL.md#network-setup) (both networks' addresses are there): + +```bash +POOL= # the pool you are backstopping — see lending.md "Discovering pools" + +stellar contract info interface --id "$BACKSTOP" --network "$NETWORK" # confirm the ABI +``` + +You deposit the **backstop token**, not BLND or USDC directly. Get its address from the backstop, then mint LP tokens via the Comet pool — a single-sided join with USDC (or BLND) works and needs no second asset: + +```bash +LP_TOKEN=$(stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=no -- backstop_token | tr -d '"') + +# Its underlying tokens, in weight order: [BLND, USDC] +stellar contract invoke --id "$LP_TOKEN" --source "$ME" --network "$NETWORK" --send=no -- get_tokens + +# Single-sided join: deposit USDC into the Comet pool, receive LP tokens (returns the LP amount minted) +stellar contract invoke --id "$LP_TOKEN" --source "$ME" --network "$NETWORK" --send=yes \ + -- dep_tokn_amt_in_get_lp_tokns_out \ + --token_in --token_amount_in 100000000 --min_pool_amount_out 0 --user "$ME" +``` + +If all you hold is XLM, you can't join the Comet pool directly — first obtain USDC (or BLND) by **borrowing it against XLM collateral** ([lending.md](lending.md)) or swapping via a DEX path payment; make sure it's the *right* USDC (`name()` — see [getting assets](SKILL.md#getting-the-assets-these-flows-assume)). + +## Deposit + +`deposit(from, pool_address, amount)` deposits backstop LP tokens behind a pool, minting backstop shares: + +```bash +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=yes \ + -- deposit \ + --from "$ME" \ + --pool_address "$POOL" \ + --amount "1000000000" +``` + +Shares represent your claim on that pool's backstop; their value rises as the backstop accrues interest and falls if it absorbs bad debt. + +## Queue for withdrawal (Q4W) + +Backstop deposits are **not** instantly withdrawable — this stops depositors from fleeing right before a liquidation. To exit you queue, wait out the timelock, then withdraw. **While queued, the funds stay at risk** (they still absorb bad debt) and keep accruing. + +The v2 timelock is **17 days** (v1 was 21): `queue_withdrawal` returns a `Q4W` whose `exp` is exactly 17 days out. Withdrawing before `exp` fails simulation with contract error `#1001` (`NotExpired`). + +```bash +# Start the timelock on part of your position +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=yes \ + -- queue_withdrawal \ + --from "$ME" --pool_address "$POOL" --amount "1000000000" + +# Change your mind while still locked — return it to the active backstop +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=yes \ + -- dequeue_withdrawal \ + --from "$ME" --pool_address "$POOL" --amount "1000000000" + +# After the timelock elapses — withdraw the LP tokens +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=yes \ + -- withdraw \ + --from "$ME" --pool_address "$POOL" --amount "1000000000" +``` + +Check whether a queued entry has unlocked before calling `withdraw` — read the user balance and compare each Q4W entry's expiration against the current ledger time (see [reading backstop state](#reading-backstop-state)). + +## Claim backstop emissions + +In v2, `claim(from, pool_addresses, min_lp_tokens_out)` does **not** pay BLND out to you — it converts the claimed BLND into backstop LP tokens and deposits them back behind the pools you claim from (auto-compound). `min_lp_tokens_out` is a slippage floor on that conversion; the call returns the LP tokens minted: + +```bash +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=yes \ + -- claim \ + --from "$ME" \ + --pool_addresses '["'"$POOL"'"]' \ + --min_lp_tokens_out "0" # set a real floor for meaningful amounts +``` + +To preview what's claimable without submitting, simulate the same call with `--send=no` — the returned value is the LP tokens the claim would mint (use it to set a sensible `min_lp_tokens_out`). + +## Reading backstop state + +All reads simulate — pass `--send=no` so they stay reads even against archived state (see [reading Blend state](SKILL.md#reading-blend-state-over-rpc)); no indexer. + +```bash +# Total backstop data for a pool: shares, LP tokens, queued %, and LP spot price +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=no -- pool_data --pool "$POOL" + +# Your balance for a pool: shares + your Q4W entries (each with amount + expiration) +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=no \ + -- user_balance --pool "$POOL" --user "$(stellar keys address "$ME")" + +# The backstop LP token address (Comet BLND:USDC) +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=no -- backstop_token +``` + +- **`PoolBackstopData`** — `shares`, `tokens` (LP tokens deposited), `q4w_pct` (fraction of shares queued for withdrawal, **7-decimal** fixed point: `3333334` ≈ 33%), `blnd` / `usdc` (underlying amounts), `token_spot_price` (LP token spot price — useful for USD conversion). +- **`UserBalance`** — `shares`, `q4w: Vec` where `exp` is the unlock time as a **unix timestamp** (seconds) — compare against the latest ledger's close time. + +## Find the most profitable backstop + +Backstop yield for a pool comes from two streams, both measured against the pool's total backstop deposit value: + +1. **Interest share** — the pool routes a fraction of borrower interest (`PoolConfig.bstop_rate`) to the backstop. Scales with borrow volume × borrow APR × `bstop_rate`. +2. **BLND emissions** — the Emitter streams BLND to the backstop, which distributes it across reward-zone pools in proportion to non-queued backstop size. Within a pool's allocation, **70% goes to backstop depositors and 30% to pool users**. + +Approximate backstop APR: + +``` +APR ≈ (annualized_interest_share_USD + annualized_emissions_USD) / backstop_deposit_value_USD +``` + +Your own projected earnings are pro-rata: `(your user_balance.shares / pool_data.shares) × the pool-level totals above`. For already-accrued (unclaimed) emissions, simulate `claim` with `--send=no` — see [claiming](#claim-backstop-emissions). + +The bundled script [`scripts/rank-backstops.sh`](scripts/rank-backstops.sh) ranks pools by an approximate backstop APR using **RPC only**. It is a scaffold: it reads the real on-chain inputs and shows where each number comes from (its comments walk through each step); finishing the APR still requires the per-reserve reads sketched in the script's step 4. Requires stellar-cli, jq, and a funded key alias. It knows the v2 backstop for testnet and mainnet (override with `BACKSTOP=` for anything else; mainnet needs an [RPC configured](SKILL.md#network-setup) first). Run it: + +```bash +NETWORK=testnet SOURCE=alice scripts/rank-backstops.sh +``` + +Output is CSV: `pool,backstop_tokens,bstop_rate,note`, one row per reward-zone pool. + +To complete the APR, add per-pool reserve reads (`get_reserve --asset ` for `d_supply` and `d_rate`, then compute the borrow rate with [the borrow-rate formula](lending.md#the-borrow-rate)), the pool-side emission reads (`get_reserve_emissions` per reserve side, scaled by 70/30 to get the backstop's BLND/sec — see [projected earnings](lending.md#projected-earnings)), and an LP-token-to-USD conversion (`token_spot_price` from `pool_data`). Rank pools by the resulting APR. + +## Related + +- Supplying/borrowing in the pools you backstop; discovering pools → [lending.md](lending.md) +- Keys, signing, submission, and reading token balances → [`../../wallet/SKILL.md`](../../wallet/SKILL.md) +- Reading chain data over RPC → [`../../data/SKILL.md`](../../data/SKILL.md) diff --git a/skills/defi/blend/lending.md b/skills/defi/blend/lending.md new file mode 100644 index 0000000..6d265fc --- /dev/null +++ b/skills/defi/blend/lending.md @@ -0,0 +1,293 @@ +# Blend lending: supply, borrow, repay, withdraw + +## Contents + +- [The `submit` model](#the-submit-model) (incl. the `RequestType` table) +- [Supply collateral](#supply-collateral) +- [Borrow](#borrow) +- [Repay](#repay) +- [Withdraw collateral](#withdraw-collateral) +- [Batching requests atomically](#batching-requests-atomically) +- [Discovering pools](#discovering-pools) +- [Reading pool state](#reading-pool-state) +- [Emissions: check, project, claim](#emissions-check-project-claim) +- [Projected earnings](#projected-earnings) + +Every lending action on a Blend pool goes through a single entrypoint: **`submit`**. You describe what you want as a list of `Request`s, and the pool executes them atomically against your position. Supplying, borrowing, repaying, and withdrawing are all just different `RequestType`s in that list. + +Read the pool's real interface before you build anything: + +```bash +stellar contract info interface --id --network "$NETWORK" +``` + +## The `submit` model + +Signature (v2 pools also expose `submit_with_allowance` and `flash_loan` variants): + +``` +submit(from: Address, spender: Address, to: Address, requests: Vec) -> Positions +``` + +- `from` — whose position is modified (the borrower/supplier). +- `spender` — who pays tokens **into** the pool (deposits, repays). +- `to` — who receives tokens **out** of the pool (borrows, withdrawals). +- Usually all three are the same account: you. + +Each `Request` is: + +``` +Request { request_type: u32, address: Address, amount: i128 } +``` + +- `address` — the reserve **asset contract** address (the SAC/SEP-41 token, a `C...`), not the pool. +- `amount` — in the asset's native decimals (e.g. 7 for most Stellar assets; read `decimals` to be sure). + +### RequestType + +This numbering is shared by Blend v1 and v2 (including the Supply/SupplyCollateral split) — confirm against [`blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2) if in doubt: + +| Value | Name | Action | +|-------|------|--------| +| 0 | Supply | Deposit to earn interest; **not** counted as collateral | +| 1 | Withdraw | Withdraw a plain Supply position | +| 2 | SupplyCollateral | Deposit **as collateral** (enables borrowing power) | +| 3 | WithdrawCollateral | Withdraw collateral | +| 4 | Borrow | Borrow an asset (creates a dToken liability) | +| 5 | Repay | Repay debt | +| 6 | FillUserLiquidationAuction | Fill a liquidation auction | +| 7 | FillBadDebtAuction | Fill a bad-debt auction (backstop) | +| 8 | FillInterestAuction | Fill an interest auction | +| 9 | DeleteLiquidationAuction | Cancel an auction | + +So: **supply as collateral → 2**, **borrow → 4**, **repay → 5**, **withdraw collateral → 3**. + +The CLI accepts complex arguments as JSON. Each example below sets `POOL` and `ASSET` (a reserve asset address for that pool — discover them with the [reserve list](#reading-pool-state)) and `ME` (your wallet key alias). + +## Supply collateral + +```bash +POOL= +ASSET= # e.g. the USDC SAC +ME=alice # wallet key alias + +# amount is in the asset's native units; 100 units of a 7-decimal asset = 1000000000 +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=yes \ + -- submit \ + --from "$ME" --spender "$ME" --to "$ME" \ + --requests '[{"request_type":2,"address":"'"$ASSET"'","amount":"1000000000"}]' +``` + +The pool pulls the asset from `spender` via authorization; the CLI collects and signs the required auth entries during simulation. `spender` needs a balance of `ASSET` (and, for a classic asset, a trustline — see the [wallet skill](../../wallet/SKILL.md)). + +## Borrow + +You can only borrow up to your collateral's borrowing power (collateral value × collateral factor); exceeding it fails simulation with contract error `#1205` (`InvalidHf`). Borrow sends the asset to `to` — if the asset is a classic-asset SAC (e.g. USDC), `to` needs a **trustline** first (`stellar tx new change-trust --line `; get the `CODE:ISSUER` string from the token's `name()`). + +There is also a **minimum position size**: `PoolConfig.min_collateral` (read it with [`get_config`](#reading-pool-state)) is the smallest **c_factor-adjusted** collateral value — *not* your raw collateral value — that may back a borrow position, denominated in the oracle's base asset at the oracle's `decimals()`. Falling below it fails with `#1224` (`MinCollateralNotMet`). The active mainnet reward-zone pools currently set a $5 floor; with XLM's `c_factor` at 0.75 that means roughly $6.70 worth of raw XLM collateral (a few tens of XLM) before you can borrow anything at all: + +```bash +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=yes \ + -- submit \ + --from "$ME" --spender "$ME" --to "$ME" \ + --requests '[{"request_type":4,"address":"'"$ASSET"'","amount":"500000000"}]' +``` + +## Repay + +Repay pulls the asset from `spender`: + +```bash +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=yes \ + -- submit \ + --from "$ME" --spender "$ME" --to "$ME" \ + --requests '[{"request_type":5,"address":"'"$ASSET"'","amount":"500000000"}]' +``` + +### Fully closing a loan + +Your debt is always **slightly more than what you borrowed** — dTokens round up at borrow, so debt exceeds the proceeds from the very same ledger (measured: 8 stroops over within minutes). That makes the two "obvious" full-repay moves both wrong when all you hold is the loan proceeds: + +- Pass more than your balance → the pool pulls the **full amount before refunding**, and the transfer fails with `#10` (`BalanceError`). +- Pass exactly your balance → dust debt survives, and later blocks withdraw-all with `#1205` (any withdraw that leaves debt must stay healthy). + +The working recipe: + +1. **Get a small buffer (~5–10%) of the exact borrowed asset** — same `CODE:ISSUER`, check with `name()`. From XLM, use a DEX path payment: `stellar tx new path-payment-strict-receive` (see the [wallet skill](../../wallet/SKILL.md)). +2. **Repay with any `amount` between your debt and your balance** — over the debt so nothing survives, within your balance so the pull succeeds. The pool pulls only what's owed and refunds the excess to `spender` exactly, in the same transaction. + +After the repay, `get_positions` should show no liability for the reserve — then withdraw-all works. + +## Withdraw collateral + +```bash +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=yes \ + -- submit \ + --from "$ME" --spender "$ME" --to "$ME" \ + --requests '[{"request_type":3,"address":"'"$ASSET"'","amount":"1000000000"}]' +``` + +Withdrawal is blocked (`#1205`) if it would drop your position below the required collateralization while you hold debt — including **dust debt** left by an inexact repay (see [fully closing a loan](#fully-closing-a-loan)). Passing an amount larger than your balance withdraws the full balance — so to fully exit a reserve, pass an oversized amount. + +## Batching requests atomically + +One `submit` can carry multiple `Request`s that execute in order and settle atomically — either all succeed or the transaction reverts. Deposit collateral and borrow against it in a single call: + +```bash +COLLATERAL= # asset to supply as collateral +BORROW= # asset to borrow + +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=yes \ + -- submit \ + --from "$ME" --spender "$ME" --to "$ME" \ + --requests '[ + {"request_type":2,"address":"'"$COLLATERAL"'","amount":"2000000000"}, + {"request_type":4,"address":"'"$BORROW"'","amount":"500000000"} + ]' +``` + +## Discovering pools + +**Enumerate pools with `reward_zone()` on the Backstop.** It returns the list of active, emission-eligible pool addresses — which is what you want for supplying, borrowing, or yield analysis (`BACKSTOP` and `NETWORK` come from [network setup](SKILL.md#network-setup)): + +```bash +stellar contract invoke --id "$BACKSTOP" --source "$ME" --network "$NETWORK" --send=no -- reward_zone +``` + +On testnet this currently includes the blend-utils reference pool, ready to paste: + +```bash +POOL=CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF # testnet (v2) "TestnetV2" pool +``` + +To check whether an arbitrary address is a genuine v2 pool (the factory exposes only `deploy(...)` and `is_pool` — no list getter): + +```bash +stellar contract invoke --id "$POOL_FACTORY" --source "$ME" --network "$NETWORK" --send=no -- is_pool --pool_address +``` + +Don't try to enumerate pools from the factory's deploy events: RPC keeps only ~7 days of events, and the deploys are long past — `getEvents` returns nothing. Pools outside the reward zone can only be found by address (from the user, [Blend's UI](https://mainnet.blend.capital), or an indexer). + +## Reading pool state + +All reads simulate — pass `--send=no` so they stay reads even against archived state (see [reading Blend state](SKILL.md#reading-blend-state-over-rpc)). The CLI still requires `--source-account` (any funded key) to build the simulation. + +```bash +# Pool config: oracle, backstop take rate, status, max positions, min collateral +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=no -- get_config + +# Reserve list: the asset addresses this pool supports +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=no -- get_reserve_list + +# One reserve: Reserve { asset, config: ReserveConfig, data: ReserveData, scalar } +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=no -- get_reserve --asset "$ASSET" + +# A user's positions: collateral / liabilities / supply, keyed by reserve index +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=no -- get_positions --address "$(stellar keys address "$ME")" +``` + +Key fields you will read: + +- **`PoolConfig`** — `oracle` (price source), `bstop_rate` (fraction of interest routed to the backstop, 7-decimal fixed point), `status`, `max_positions`, `min_collateral` (minimum c_factor-adjusted collateral value to hold a borrow position, in the oracle's base asset at the oracle's `decimals()` — see [Borrow](#borrow)). +- **`PoolConfig.status`** — `0` Admin-Active, `1` Active, `2` Admin-On-Ice, `3` On-Ice, `4` Admin-Frozen, `5` Frozen, `6` Setup. Status > 1 disables borrowing; status > 3 also disables supplying (violations fail with `#1206`). +- **`ReserveConfig`** — `decimals`, `c_factor` (collateral factor), `l_factor` (liability factor), the kinked interest-rate curve (`r_base`, `r_one`, `r_two`, `r_three`, `util`, `max_util`, `reactivity` — all 7-decimal), `supply_cap`, `enabled`, `index`. +- **`ReserveData`** — `b_rate` / `d_rate` (supply/borrow indexes), `b_supply` / `d_supply` (total b/d tokens), `ir_mod` (rate modifier, **7-decimal** fixed point, clamped to [0.1, 10] — i.e. `1000000`–`100000000`), `backstop_credit`, `last_time`. +- **`Positions`** — `collateral`, `liabilities`, `supply` maps keyed by reserve index. + +Convert shares to underlying with the rate index: underlying supply = `bToken_balance × b_rate / 1e12`; underlying debt = `dToken_balance × d_rate / 1e12`. `b_rate` / `d_rate` are 12-decimal fixed-point (`SCALAR_12`); the result is in the asset's native decimals. + +### The borrow rate + +The current borrow APR is computable from `ReserveConfig` + `ReserveData` alone (all inputs and the result are 7-decimal fixed point; `util` here is current utilization, `target` is `ReserveConfig.util`): + +``` +util ≤ target: rate = (r_base + r_one × util/target) × ir_mod +target < util ≤ 0.95: rate = (r_base + r_one + r_two × (util − target)/(0.95 − target)) × ir_mod +util > 0.95: rate = (r_base + r_one + r_two) × ir_mod + + r_three × (util − 0.95)/0.05 # r_three leg is NOT scaled by ir_mod +``` + +with `utilization = (d_supply × d_rate) / (b_supply × b_rate)`. This is `calc_accrual` in `pool/src/pool/interest.rs` of [`blend-contracts-v2`](https://github.com/blend-capital/blend-contracts-v2), transcribed; check the source if in doubt. + +## Emissions: check, project, claim + +Suppliers and borrowers accrue BLND emissions per reserve **side**. Each reserve has two emission streams, identified by a `reserve_token_id`: + +- **borrow side** (dTokens / liabilities): `reserve_index × 2` +- **supply side** (bTokens / supply + collateral): `reserve_index × 2 + 1` + +`reserve_index` is `ReserveConfig.index` from `get_reserve`. Not every stream is funded — `get_reserve_emissions` returns `None` (void) for streams with no emissions configured. + +### Check accrued emissions + +```bash +# The stream itself: eps (BLND/sec — 14 total decimals: BLND's 7 + a 7-decimal scalar), index, expiration +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=no \ + -- get_reserve_emissions --reserve_token_index 1 + +# Your accrued-but-unclaimed BLND on that stream: UserEmissionData { accrued, index } +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=no \ + -- get_user_emissions --user "$(stellar keys address "$ME")" --reserve_token_index 1 +``` + +`accrued` only updates when your position is touched, so BLND earned since your last interaction isn't in it. For the exact claimable total, **simulate the claim**: run it with `--send=no` and read the returned amount without submitting anything. + +> **Prerequisite:** `to` needs a **BLND trustline** before any claim — even this simulated preview reverts with `#13` (`TrustlineMissingError`) without one. See [prerequisites](SKILL.md#prerequisites) for deriving BLND and adding the trustline. + +```bash +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=no \ + -- claim --from "$ME" --reserve_token_ids '[0,1]' --to "$ME" +``` + +### Claim + +Same call submitted for real — `claim(from, reserve_token_ids, to)` transfers the BLND to `to`: + +```bash +stellar contract invoke --id "$POOL" --source "$ME" --network "$NETWORK" --send=yes \ + -- claim \ + --from "$ME" \ + --reserve_token_ids '[0,1]' \ + --to "$ME" +``` + +Backstop emissions are separate — see [backstop.md](backstop.md#claim-backstop-emissions). + +## Projected earnings + +A position earns (or costs) from two streams; every input below is read over RPC via `get_reserve`, `get_config`, and the emission getters. + +**1. Interest.** Borrowers pay the reserve's borrow rate; the backstop takes a `bstop_rate` cut and suppliers split the rest, so: + +``` +utilization = (d_supply × d_rate) / (b_supply × b_rate) # underlying borrowed / supplied +supply_rate ≈ borrow_rate × utilization × (1 − bstop_rate) # bstop_rate is 7-decimal fixed point +``` + +`borrow_rate` is computed from `ReserveConfig` + `ReserveData` with the formula in [the borrow rate](#the-borrow-rate). Projected yearly interest ≈ `your_supplied_underlying × supply_rate` (or `your_debt × borrow_rate` on the cost side). + +**2. BLND emissions.** Your share of a stream is proportional to your b/dTokens over the side's total: + +``` +your_blnd_per_year = (eps / 1e14) × 31,536,000 × (your_b_or_d_tokens / total_b_or_d_supply) +``` + +The `1e14` is BLND's 7 decimals plus an extra 7-decimal scalar baked into `eps`; the result is whole BLND. + +Check the stream's `expiration` — emissions stop when it lapses (streams refresh via `gulp_emissions` while the pool is in the [reward zone](backstop.md#reading-backstop-state)). Note only 30% of a pool's emission allocation flows to pool users, split across reserve sides by pool config; the other 70% goes to backstop depositors. + +To express BLND earnings in USD, read the BLND price from the Comet LP (pool oracles list only the pool's reserve assets, so they usually can't price BLND). `LP_TOKEN`, `BLND`, and `USDC` come from [deriving contracts on-chain](SKILL.md#derive-the-rest-on-chain): + +```bash +# BLND price in USDC — 7-decimal fixed point, e.g. "3008070" = 0.30 USDC per BLND +stellar contract invoke --id "$LP_TOKEN" --source "$ME" --network "$NETWORK" --send=no \ + -- get_spot_price --token_in "$USDC" --token_out "$BLND" +``` + +To price a reserve asset in USD (the interest side), use the pool's oracle (`PoolConfig.oracle` from `get_config`, SEP-40). The `Asset` argument is an enum — pass it as JSON: + +```bash +# Returns PriceData { price, timestamp }; scale price by the oracle's decimals() (typically 7 or 14) +stellar contract invoke --id "$ORACLE" --source "$ME" --network "$NETWORK" --send=no \ + -- lastprice --asset '{"Stellar":"'"$ASSET"'"}' +``` diff --git a/skills/defi/blend/scripts/rank-backstops.sh b/skills/defi/blend/scripts/rank-backstops.sh new file mode 100755 index 0000000..71afc8f --- /dev/null +++ b/skills/defi/blend/scripts/rank-backstops.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# rank-backstops.sh — approximate backstop APR per Blend pool, RPC only. +# Requires: stellar-cli, jq, a funded key alias (reads simulate but need a source account). +# Usage: NETWORK=testnet SOURCE=alice ./rank-backstops.sh +# NETWORK=mainnet SOURCE=alice ./rank-backstops.sh (needs `stellar network add mainnet` first) +set -euo pipefail + +NETWORK=${NETWORK:-testnet} +SOURCE=${SOURCE:?set SOURCE to a funded key alias} + +# Blend v2 backstop per network; pass BACKSTOP explicitly to override. +if [[ -z "${BACKSTOP:-}" ]]; then + case "$NETWORK" in + mainnet) BACKSTOP=CAQQR5SWBXKIGZKPBZDH3KM5GQ5GUTPKB7JAFCINLZBC5WXPJKRG3IM7 ;; + testnet) BACKSTOP=CBDVWXT433PRVTUNM56C3JREF3HIZHRBA64NB2C3B2UNCKIS65ZYCLZA ;; + *) echo "error: no default backstop for NETWORK=$NETWORK — set BACKSTOP=C..." >&2; exit 1 ;; + esac +fi + +# Errors surface on stderr; --send=no keeps every call a pure read. +invoke() { stellar contract invoke --id "$1" --source "$SOURCE" --network "$NETWORK" --send=no -- "${@:2}"; } + +# 1. Get the candidate pools. The reward zone is the practical source of active, +# emission-eligible pools (see lending.md "Discovering pools"). +pools=$(invoke "$BACKSTOP" reward_zone | jq -r '.[]') +if [[ -z "$pools" ]]; then + echo "error: reward_zone returned no pools on $NETWORK (backstop $BACKSTOP)" >&2 + exit 1 +fi + +echo "pool,backstop_tokens,bstop_rate,note" +for POOL in $pools; do + # 2. Backstop size (APR denominator): LP tokens deposited for this pool. + # pool_data also returns token_spot_price for LP -> USD conversion. + bal=$(invoke "$BACKSTOP" pool_data --pool "$POOL") + tokens=$(echo "$bal" | jq -r '.tokens // "0"') + + # 3. Interest-share input: the pool's backstop take rate. + cfg=$(invoke "$POOL" get_config) + bstop_rate=$(echo "$cfg" | jq -r '.bstop_rate // "0"') + + # 4. To finish the APR you still need, per pool: + # - sum over reserves of (d_supply * d_rate * borrow_rate) * bstop_rate -> interest to backstop + # (borrow_rate: compute from get_reserve fields with the formula in + # lending.md "The borrow rate") + # - backstop BLND/sec: no direct getter, but pool-side streams are readable — + # sum get_reserve_emissions eps over the pool's reserve sides, then + # backstop_eps = pool_eps_sum * 70/30 + # - LP-token -> USD: token_spot_price from pool_data + echo "$POOL,$tokens,$bstop_rate,partial" +done diff --git a/skills/standards/SKILL.md b/skills/standards/SKILL.md index 133de57..b58092a 100644 --- a/skills/standards/SKILL.md +++ b/skills/standards/SKILL.md @@ -753,6 +753,24 @@ Always verify CAP status and network support before treating any ZK primitive as - [NOWNodes](https://nownodes.io) - All networks incl. Futurenet - [GetBlock](https://getblock.io) - Testnet & Mainnet +#### Public mainnet RPC endpoints + +SDF provides no public **mainnet** RPC, and the Stellar CLI's built-in `mainnet` network has **no RPC URL** — nothing works against mainnet until you register an endpoint. No-signup public endpoints (each verified with a `getHealth` probe; most keep ~1–7 days of history — check `ledgerRetentionWindow` in the `getHealth` response): + +| Endpoint | Operator | +|----------|----------| +| `https://mainnet.sorobanrpc.com` | Community-run public endpoint | +| `https://rpc.ankr.com/stellar_soroban` | Ankr (public tier) | +| `https://soroban-rpc.creit.tech` | Creit Tech (xBull) | + +```bash +stellar network add mainnet \ + --rpc-url https://mainnet.sorobanrpc.com \ + --network-passphrase "Public Global Stellar Network ; September 2015" +``` + +Public endpoints are rate-limited, best-effort, and may drop low-fee Soroban transactions under load (retry with a higher `--inclusion-fee`) — use a provider above for production. + ## Protocol & Governance ### Stellar Protocol