# BlumeLend Lending Skill

BlumeLend is a Morpho Blue fork on XRPL EVM. Singleton lending — all markets live in one contract, identified by `MarketParams` struct. Permissionless market creation, fully autonomous operations, no human intervention required.

**Important:** These docs are for programmatic agents using `cast`, `curl`, and `viem`. The web frontend at lend.blumefi.com requires a browser wallet (MetaMask, etc.).

## Quick verify — read this first if you're auditing

Common probe failure modes that produce wrong reports:

- **Oracle is Band Protocol, not Chainlink.** The adapter exposes `price()` (selector `0xa035b1fe`); the upstream `StdReferenceProxy` exposes `getReferenceData(string,string)` (selector `0x65555bcc`). Probing with Chainlink selectors `latestAnswer()` (`0x50d25bcd`) or `latestRoundData()` (`0xfeaf968c`) **will revert** — they belong to a different protocol, not a missing function.
- **Lineage: Morpho Blue fork.** All core function signatures are **identical** to canonical Morpho Blue. The only rename is the callback interface (`onMorphoSupply` → `onBlumeLendSupply`, etc.) — those callbacks are implemented on the *caller*, not on this contract, so they're not "missing selectors" in the BlumeLend bytecode. Standard view selectors `market(bytes32)` (`0x5c60e39a`), `idToMarketParams(bytes32)` (`0x2c3c9157`), `position(bytes32,address)` (`0x93c52062`) **are present** — see the [Function selector cheatsheet](#function-selector-cheatsheet) below.
- **Source IS verified.** Mainnet contracts on Blockscout: [BlumeLend](https://explorer.xrplevm.org/address/0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56) · [BandOracleAdapter](https://explorer.xrplevm.org/address/0x200c909fE38D9E109d1AC1A8998b633F59e11E84) · [Band StdReferenceProxy](https://explorer.xrplevm.org/address/0x6ec95bC946DcC7425925801F4e262092E0d1f83b). Blockscout returns `is_partially_verified: true` for the BlumeLend contracts because of the documented cancun→shanghai metadata workaround — **source code IS visible** on the explorer.
- **Market ID is verifiable** — `keccak256(abi.encode(loanToken, collateralToken, oracle, irm, lltv))` reproduces the documented `0xec6ed3…b600` exactly. Worked example below.
- **One-shot check**: `npx blumefi lend audit` prints all of the above with live state and flags any drift.

## TL;DR — fastest path

```bash
# Read-only (no wallet)
npx blumefi lend market
npx blumefi lend position 0xYOUR_ADDRESS

# Write (set WALLET_PRIVATE_KEY first)
npx blumefi lend supply 10                    # earn yield
npx blumefi lend collateral 5                 # 5 WXRP collateral
npx blumefi lend borrow 4                     # borrow 4 USDC against it
npx blumefi lend repay all
npx blumefi lend remove-collateral all
```

The CLI handles decimals, market params, approvals, and the MarketParams struct encoding for you.

## Active Market — XRP / USDC

| Field | Mainnet | Testnet |
|-------|---------|---------|
| Loan token | USDC.xrpl (15 decimals) | MockUSDC (6 decimals) |
| Collateral token | WXRP (18 decimals) | WXRP (18 decimals) |
| LLTV (max loan-to-value) | 86% (`0.86e18`) | 86% (`0.86e18`) |
| Oracle | Band Protocol XRP/USD | Band Protocol XRP/USD |
| IRM | AdaptiveCurveIrm (target utilization 90%) | same |
| Protocol fee | 10% of accrued interest | 0% |
| Owner / Fee recipient | Deployer EOA `0xc282…bB47aE` | same |

> **Decimals gotcha:** Mainnet USDC.xrpl uses **15 decimals**, not 6. `1 USDC = 10^15` raw units on mainnet. The CLI handles this automatically; if you call contracts directly, scale your amounts accordingly.

## Contract Addresses

### Mainnet (Chain ID: 1440000)

| Contract | Address |
|----------|---------|
| BlumeLend (singleton) | `0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56` |
| AdaptiveCurveIrm | `0xFE5F6119cd3bAA91eD8AF2638C1627b84F8636F4` |
| BandOracleAdapter (XRP/USD, scale 1e15) | `0x200c909fE38D9E109d1AC1A8998b633F59e11E84` |
| USDC (loan, USDC.xrpl) | `0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C` |
| WXRP (collateral) | `0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf` |
| Active market id | `0xec6ed3201955219495e2c26d40bdf9cd710c4186af887c08d883958d5a03b600` |

### Testnet (Chain ID: 1449000)

| Contract | Address |
|----------|---------|
| BlumeLend | `0x266f283A2FEA75304B132Cb9F3b795B6266A8Ec1` |
| AdaptiveCurveIrm | `0x814fC6b1E07F16aCB536aCc262Fae66114ddDD72` |
| BandOracleAdapter (XRP/USD, scale 1e6) | `0xBBE1b60a438Da8f04ef1031d4604eD31F5935c4E` |
| MockUSDC (loan) | `0xC6dD7E13EeEBE873e24716426687c303A2A4489c` |
| WXRP (collateral) | `0x664950b1F3E2FAF98286571381f5f4c230ffA9c5` |

**Testnet faucet:** `curl -X POST https://api.blumefi.com/faucet/drip -H "Content-Type: application/json" -d '{"address":"YOUR_ADDRESS"}'` — 25 XRP, 1hr cooldown.

## MarketParams struct

Every BlumeLend operation takes a `MarketParams` tuple as the first argument:

```
struct MarketParams {
  address loanToken;
  address collateralToken;
  address oracle;
  address irm;
  uint256 lltv;
}
```

Mainnet XRP/USDC market params (cast tuple form):

```
(0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C,0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf,0x200c909fE38D9E109d1AC1A8998b633F59e11E84,0xFE5F6119cd3bAA91eD8AF2638C1627b84F8636F4,860000000000000000)
```

Market id is `keccak256(abi.encode(marketParams))`. Worked example for the active mainnet market:

```bash
cast keccak $(cast abi-encode "f(address,address,address,address,uint256)" \
  0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C \
  0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf \
  0x200c909fE38D9E109d1AC1A8998b633F59e11E84 \
  0xFE5F6119cd3bAA91eD8AF2638C1627b84F8636F4 \
  860000000000000000)
# → 0xec6ed3201955219495e2c26d40bdf9cd710c4186af887c08d883958d5a03b600
```

The Solidity assembly form (`keccak256(marketParams, 160)`) is byte-equivalent to `keccak256(abi.encode(...))` — five fixed-size 32-byte fields = 160 bytes.

## Operations

All write operations require a prior ERC20 `approve(BlumeLend, amount)` on the relevant token (USDC for supply/repay, WXRP for collateral).

### Supply USDC (earn yield)

```bash
# CLI
npx blumefi lend supply 10

# Direct (mainnet, 10 USDC = 10 * 10^15 raw units)
cast send 0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C \
  "approve(address,uint256)" 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  10000000000000000 \
  --rpc-url https://rpc.xrplevm.org --private-key $WALLET_PRIVATE_KEY --gas-limit 200000

cast send 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  "supply((address,address,address,address,uint256),uint256,uint256,address,bytes)" \
  "(0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C,0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf,0x200c909fE38D9E109d1AC1A8998b633F59e11E84,0xFE5F6119cd3bAA91eD8AF2638C1627b84F8636F4,860000000000000000)" \
  10000000000000000 0 YOUR_ADDRESS 0x \
  --rpc-url https://rpc.xrplevm.org --private-key $WALLET_PRIVATE_KEY --gas-limit 1000000
```

`supply(params, assets, shares, onBehalf, data)` — pass `assets > 0, shares = 0` (or vice versa, never both nonzero).

### Withdraw USDC

```bash
# CLI
npx blumefi lend withdraw 5
npx blumefi lend withdraw all

# Direct: withdraw all = pass shares = position.supplyShares, assets = 0
cast send 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  "withdraw((address,address,address,address,uint256),uint256,uint256,address,address)" \
  "(0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C,0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf,0x200c909fE38D9E109d1AC1A8998b633F59e11E84,0xFE5F6119cd3bAA91eD8AF2638C1627b84F8636F4,860000000000000000)" \
  ASSETS SHARES YOUR_ADDRESS YOUR_ADDRESS \
  --rpc-url https://rpc.xrplevm.org --private-key $WALLET_PRIVATE_KEY --gas-limit 1000000
```

### Supply WXRP collateral

WXRP is required — wrap XRP first if needed. The CLI prints a clear error and tells you how to wrap.

```bash
# CLI (errors helpfully if you don't have WXRP)
npx blumefi lend collateral 5

# Direct: approve WXRP, then supplyCollateral
cast send 0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf \
  "approve(address,uint256)" 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  5000000000000000000 \
  --rpc-url https://rpc.xrplevm.org --private-key $WALLET_PRIVATE_KEY --gas-limit 200000

cast send 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  "supplyCollateral((address,address,address,address,uint256),uint256,address,bytes)" \
  "(0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C,0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf,0x200c909fE38D9E109d1AC1A8998b633F59e11E84,0xFE5F6119cd3bAA91eD8AF2638C1627b84F8636F4,860000000000000000)" \
  5000000000000000000 YOUR_ADDRESS 0x \
  --rpc-url https://rpc.xrplevm.org --private-key $WALLET_PRIVATE_KEY --gas-limit 1000000
```

### Borrow USDC

No approval needed — borrowing pulls from the pool to the borrower. Will revert if the resulting Health Factor would drop below 1.

```bash
# CLI
npx blumefi lend borrow 4

# Direct (4 USDC = 4 * 10^15 raw on mainnet)
cast send 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  "borrow((address,address,address,address,uint256),uint256,uint256,address,address)" \
  "(0xDaF4556169c4F3f2231d8ab7BC8772Ddb7D4c84C,0x7C21a90E3eCD3215d16c3BBe76a491f8f792d4Bf,0x200c909fE38D9E109d1AC1A8998b633F59e11E84,0xFE5F6119cd3bAA91eD8AF2638C1627b84F8636F4,860000000000000000)" \
  4000000000000000 0 YOUR_ADDRESS YOUR_ADDRESS \
  --rpc-url https://rpc.xrplevm.org --private-key $WALLET_PRIVATE_KEY --gas-limit 1000000
```

### Repay USDC

Approve first, then `repay`. To close out cleanly: pass `shares = position.borrowShares, assets = 0` (the full debt amount in shares — avoids the "interest accrued between read and tx" race).

```bash
# CLI
npx blumefi lend repay 1
npx blumefi lend repay all

# Direct repay-all: shares = your borrowShares, assets = 0
cast send 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  "repay((address,address,address,address,uint256),uint256,uint256,address,bytes)" \
  "(...market params...)" \
  0 YOUR_BORROW_SHARES YOUR_ADDRESS 0x \
  --rpc-url https://rpc.xrplevm.org --private-key $WALLET_PRIVATE_KEY --gas-limit 1000000
```

### Withdraw collateral

```bash
# CLI
npx blumefi lend remove-collateral all
npx blumefi lend remove-collateral 2.5

# Direct
cast send 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  "withdrawCollateral((address,address,address,address,uint256),uint256,address,address)" \
  "(...market params...)" \
  AMOUNT_WAD YOUR_ADDRESS YOUR_ADDRESS \
  --rpc-url https://rpc.xrplevm.org --private-key $WALLET_PRIVATE_KEY --gas-limit 1000000
```

Reverts if the withdrawal would drop the health factor below 1.

## Read-Only Queries

```bash
# Market state (totalSupplyAssets, totalSupplyShares, totalBorrowAssets, totalBorrowShares, lastUpdate, fee)
cast call 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  "market(bytes32)(uint128,uint128,uint128,uint128,uint128,uint128)" \
  0xec6ed3201955219495e2c26d40bdf9cd710c4186af887c08d883958d5a03b600 \
  --rpc-url https://rpc.xrplevm.org

# Position (supplyShares, borrowShares, collateral)
cast call 0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56 \
  "position(bytes32,address)(uint256,uint128,uint128)" \
  0xec6ed3201955219495e2c26d40bdf9cd710c4186af887c08d883958d5a03b600 \
  YOUR_ADDRESS \
  --rpc-url https://rpc.xrplevm.org

# Oracle price (1e36-scaled)
cast call 0x200c909fE38D9E109d1AC1A8998b633F59e11E84 "price()(uint256)" \
  --rpc-url https://rpc.xrplevm.org
```

## Function selector cheatsheet

If you're inspecting bytecode or running a 4-byte probe, these are the canonical selectors. Verify locally with `cast sig "<signature>"`.

**`BlumeLend` singleton** (`0x1DB2C1ed42C5a2eF33709B455aB9dc64a02A0c56` mainnet, `0x266f283A2FEA75304B132Cb9F3b795B6266A8Ec1` testnet):

| Function | Selector |
|---|---|
| `owner()` | `0x8da5cb5b` |
| `feeRecipient()` | `0x46904840` |
| `market(bytes32)` | `0x5c60e39a` |
| `position(bytes32,address)` | `0x93c52062` |
| `idToMarketParams(bytes32)` | `0x2c3c9157` |
| `isIrmEnabled(address)` | `0xf2b863ce` |
| `isLltvEnabled(uint256)` | `0xb485f3b8` |
| `isAuthorized(address,address)` | `0x65e4ad9e` |
| `nonce(address)` | `0x70ae92d2` |
| `DOMAIN_SEPARATOR()` | `0x3644e515` |
| `supply((address,address,address,address,uint256),uint256,uint256,address,bytes)` | `0xa99aad89` |
| `withdraw((address,address,address,address,uint256),uint256,uint256,address,address)` | `0x5c2bea49` |
| `borrow((address,address,address,address,uint256),uint256,uint256,address,address)` | `0x50d8cd4b` |
| `repay((address,address,address,address,uint256),uint256,uint256,address,bytes)` | `0x20b76e81` |
| `supplyCollateral((address,address,address,address,uint256),uint256,address,bytes)` | `0x238d6579` |
| `withdrawCollateral((address,address,address,address,uint256),uint256,address,address)` | `0x8720316d` |
| `liquidate((address,address,address,address,uint256),address,uint256,uint256,bytes)` | `0xd8eabcb8` |
| `createMarket((address,address,address,address,uint256))` | `0x8c1358a2` |
| `accrueInterest((address,address,address,address,uint256))` | `0x151c1ade` |
| `flashLoan(address,uint256,bytes)` | `0xe0232b42` |
| `setAuthorization(address,bool)` | `0xeecea000` |
| `extSloads(bytes32[])` | `0x7784c685` |

**`BandOracleAdapter`** (`0x200c909fE38D9E109d1AC1A8998b633F59e11E84` mainnet):

| Function | Selector |
|---|---|
| `price()` | `0xa035b1fe` |
| `BAND_ORACLE()` | `0x36962878` |
| `SCALE_FACTOR()` | `0xce4b5bbe` |
| `MAX_PRICE_AGE()` | `0x9d7f7e86` |
| `baseSymbol()` | `0x80583be7` |
| `quoteSymbol()` | `0x60d6df6e` |

**Upstream Band feed** (`0x6ec95bC946DcC7425925801F4e262092E0d1f83b`, the `StdReferenceProxy` contract from Band Protocol):

| Function | Selector |
|---|---|
| `getReferenceData(string,string)` | `0x65555bcc` |

**Selectors that DON'T belong here** (Chainlink, not Band — probing them will revert):

| Function | Selector |
|---|---|
| `latestAnswer()` | `0x50d25bcd` |
| `latestRoundData()` | `0xfeaf968c` |

## Risk Model

- **Health Factor (HF)** = `collateral × oraclePrice ÷ 1e36 × LLTV ÷ 1e18 ÷ borrowAssets`
- HF ≥ 1 → safe.
- HF < 1 → anyone can liquidate the position.
- The CLI flags `near liquidation` when HF < 1.05 and disables max borrow above that threshold.
- **Stale oracle**: BandOracleAdapter reverts if Band's `lastUpdatedBase` is more than 600s old. All operations that read the oracle (borrow, withdrawCollateral, liquidate) revert during staleness — supply, withdraw, repay are unaffected. This is a safety feature, not a bug — it prevents trades against stale prices during feed outages.
- **Liquidations** are permissionless — anyone can call `liquidate()` on an unhealthy position. A continuously-running public liquidator covers the active market.

### Governance scope

The owner (`0xc282…bB47aE`, currently a deployer EOA) has a **bounded** set of privileges:

- enable new IRMs and LLTVs (additive — doesn't affect existing markets)
- set protocol fee per market (capped at 25% by `MAX_FEE = 0.25e18`; currently 10% on the active market)
- set fee recipient

The owner **cannot** pause the protocol, freeze positions, modify existing market parameters, upgrade the contract, or seize user funds. Migrating ownership to a multisig is on the roadmap.

## Value Formats

| Token | Decimals | Example raw units |
|-------|----------|-------------------|
| WXRP (collateral) | 18 | `1000000000000000000` = 1 WXRP |
| USDC.xrpl (mainnet loan) | **15** | `1000000000000000` = 1 USDC |
| MockUSDC (testnet loan) | 6 | `1000000` = 1 USDC |
| LLTV | WAD (1e18) | `860000000000000000` = 86% |
| Oracle price | 1e36 / 10^(collDec - loanDec) | mainnet scale 1e15, testnet 1e6 |

## Query via API

The Blume API indexes BlumeLend events.

```bash
# Lending stats — total supply, borrow, utilization, top suppliers/borrowers
curl https://api.blumefi.com/stats/lending

# Filter by chain
curl 'https://api.blumefi.com/stats/lending?chain=mainnet'
```

## XRPL EVM Tips

- **Always set explicit gas**: `--gas-limit 1000000` for BlumeLend writes, `--gas-limit 200000` for ERC20 approvals.
- **No multi-call helper for native XRP** — to use XRP as collateral you must wrap to WXRP first via BlumeSwap (`blumefi swap <amount> XRP WXRP`).
- **Verifying contracts on `explorer.xrplevm.org`** — see hub skill for the cancun → shanghai workaround. BlumeLend's contracts are partial-verified.

## Ecosystem Links

- [Ecosystem Hub](https://blumefi.com/skill.md) — All Blume apps
- [Swap](https://swap.blumefi.com/skill.md) — Wrap XRP to WXRP, swap into USDC
- [Mainnet Explorer](https://explorer.xrplevm.org)
- [Testnet Explorer](https://explorer.testnet.xrplevm.org)
