Skip to content

feat: add best-rate lending router action provider#2

Open
zaryab2000 wants to merge 3 commits into
feat/lendingRouterfrom
claude/dazzling-hypatia-jVytO
Open

feat: add best-rate lending router action provider#2
zaryab2000 wants to merge 3 commits into
feat/lendingRouterfrom
claude/dazzling-hypatia-jVytO

Conversation

@zaryab2000

Copy link
Copy Markdown
Owner

Summary

  • New ActionProvider: lendingRouter — a cross-protocol lending router for Base mainnet (chain ID 8453)
  • Compares live supply/borrow APY across Compound III, Aave v3, Moonwell, and Morpho Blue, then routes deposits and borrows to the optimal venue
  • 5 actions: compare_lending_rates, get_aggregated_position, route_supply, route_borrow, rebalance

What's included

File Purpose
lendingRouterActionProvider.ts Provider class with 5 @CreateAction methods
schemas.ts Zod v4 schemas with .describe() on every field
constants.ts Per-protocol addresses, ABIs, scaling constants
adapters/compound.ts Compound III on-chain rate/position reader
adapters/aave.ts Aave v3 on-chain rate/position reader + tx encoding
adapters/moonwell.ts Moonwell on-chain rate/position reader + mint encoding
adapters/morpho.ts Morpho Blue GraphQL rate reader + position stub
utils.ts Rate ranking, health aggregation, APY formatting
lendingRouterActionProvider.test.ts 29 Jest unit tests (mocked wallet)
index.ts Re-exports
README.md Per-provider documentation
Changeset Patch: "Added a best-rate lending router action provider"

v1 Protocol Coverage

Protocol Rate Reads Position Reads Supply Borrow
Compound III ✅ on-chain ✅ on-chain
Aave v3 ✅ on-chain ✅ on-chain
Moonwell ✅ on-chain ✅ on-chain ✅ mint ❌ v2
Morpho Blue ✅ GraphQL ⚠️ stub ❌ v2 ❌ v2

v1 Limitations

  • Borrow routing: Compound and Aave only (Moonwell/Morpho borrow actions don't exist in AgentKit yet)
  • Morpho position tracking is minimal (full support requires market enumeration)
  • Rebalance is advisory — returns a plan rather than auto-executing
  • Base mainnet only (chainId === "8453")

Test plan

  • pnpm build — clean
  • pnpm test — 29/29 passed
  • pnpm run lint — clean
  • pnpm run format — clean
  • Factory re-exported from src/action-providers/index.ts
  • Changeset added (patch, past tense)
  • supportsNetwork returns true only for Base mainnet
  • All actions return Promise<string>; errors returned not thrown
  • Zod schemas: .describe() on every field, no .strip()

https://claude.ai/code/session_01H9og3kM8YwPCcUCHnDxTxw


Generated by Claude Code

Cross-protocol lending router that compares live supply/borrow APY across
Compound III, Aave v3, Moonwell, and Morpho Blue on Base mainnet (8453),
then routes deposits and borrows to the optimal venue.

Actions:
- compare_lending_rates: Read live APY from all 4 protocols, rank best-first
- get_aggregated_position: Cross-protocol portfolio view with health factors
- route_supply: Auto-route supply to best-rate venue (all 4 protocols)
- route_borrow: Auto-route borrow to cheapest venue (Compound/Aave v1)
- rebalance: Advisory rebalance plan when rates drift

v1 limitations: borrow routing is Compound/Aave only; Morpho position
tracking is minimal; rebalance is advisory.

https://claude.ai/code/session_01H9og3kM8YwPCcUCHnDxTxw
@github-actions github-actions Bot added documentation Improvements or additions to documentation typescript action provider labels Jun 5, 2026
@zaryab2000

Copy link
Copy Markdown
Owner Author

@claude start review

@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown

Claude finished @zaryab2000's task in 2m 45s —— View job


Code Review — lendingRouter ActionProvider

Todo list:

  • Gather context and read PR files
  • Review main provider (lendingRouterActionProvider.ts)
  • Review schemas (schemas.ts)
  • Review adapters (compound, aave, moonwell, morpho)
  • Review constants, utils, index, README
  • Review tests (lendingRouterActionProvider.test.ts)
  • Check index.ts re-export and changeset
  • Post consolidated review findings

Overall this is a well-structured provider — good separation into adapters, correct use of @CreateAction, Promise<string> returns, errors caught and returned (not thrown), Zod v4 schemas with .describe() everywhere, supportsNetwork scoped correctly to Base mainnet. The checklist items in the PR description are all satisfied. Below are findings that should be addressed before merging upstream.


Critical Bugs

1. Compound always queries the USDC Comet regardless of asset

COMPOUND_COMET_ADDRESS is hardcoded to the single USDC Comet (0xb125…). Compound III has separate Comet deployments per base asset (USDC, WETH, cbBTC, etc.). When you call getCompoundRates(wallet, "WETH", "supply"), the code reads from the USDC Comet and returns a USDC supply rate labeled as if it were for WETH. This makes the rate-comparison and routing actively wrong for any asset other than USDC.

[adapters/compound.ts:42-67] — Fix this →


2. Moonwell position reads use 18 decimals for all tokens

getMoonwellPosition calls formatUnits(supplyBal, 18) and formatUnits(borrowBal, 18) unconditionally. balanceOfUnderlying returns the underlying token amount, which is in that token's own decimals (6 for USDC, 18 for WETH). For a USDC position, this would display the balance as 1 / 10^12 times the true value.

[adapters/moonwell.ts:98-115] — Fix this →


3. Moonwell health factor is not comparable with other protocols

getAccountLiquidity returns USD liquidity surplus (not a ratio). The code synthesizes a health factor as 1 + liquidity where liquidity is a dollar amount. For a user with $1 million in surplus liquidity this gives healthFactor = 1_000_001, while Aave would give a ratio like 1.5. The findLowestHealth comparison across protocols is therefore meaningless and will always flag the Moonwell position regardless of actual risk.

[adapters/moonwell.ts:75-83]

A correct approach is to use (liquidity + totalBorrowValue) / totalBorrowValue — but the Comptroller doesn't expose those separately. The best v1 fix is to document this limitation clearly and return healthSource: "liquidity-surplus-usd (not comparable)", or to cap it at some sentinel rather than 1 + dollarAmount.


4. encodeMoonwellMint has an unused mToken parameter

The function accepts mToken: Address but never references it in the body — the encodeFunctionData call only uses the ABI and function name. The actual to address is correctly set in the provider's sendTransaction call, but the dead parameter is confusing.

[adapters/moonwell.ts:139-145] — Fix this →


5. rebalance doesn't read user positions — it compares protocol rates only

The rebalance action compares the highest-rate protocol against the lowest-rate protocol across all four, regardless of where the user actually has funds. If the user has no funds in the "worst" protocol, the advisory plan is irrelevant. The action should call getAggregatedPosition first to find which protocols the user is actively supplying to, then compare those against the best available rate.

[lendingRouterActionProvider.ts:357-407]


6. Morpho marketAddress is a bytes32 market key, not an address

best.uniqueKey is a bytes32 market identifier (e.g. 0xabc...32hex...chars), not a 20-byte Ethereum address. Casting it as Address and returning it in the marketAddress field (which the provider uses as an address in routing) will cause any future Morpho supply execution to send a transaction to an invalid address.

[adapters/morpho.ts:88]


Notable Issues

7. getCompoundRates notes field hardcodes "Comet USDC market"

Even after fixing issue #1, notes should reflect the actual asset/market being queried rather than always saying USDC. [adapters/compound.ts:66]

8. No health-factor pre-check for Compound borrows

routeBorrow checks Aave health before executing, but not Compound. Compound III will revert on-chain, but the UX is better if the pre-check catches it and returns a readable message. [lendingRouterActionProvider.ts:299-304]

9. Sequential RPC calls in getCompoundPosition

The collateral loop in getCompoundPosition makes 3-4 sequential readContract calls per collateral asset. For a market with many collateral types this can be very slow. Batching with publicClient.multicall would improve this. Not a correctness issue, but worth noting for a protocol integration that will see real use. [adapters/compound.ts:111-151]

10. cbeth mToken address looks incorrect

MOONWELL_MTOKEN_ADDRESSES.cbeth = "0x3bf93770f2d4a0F9a5a5F3d1a1e3a0c9B6C7E3e5". This address ends in a suspicious repeated pattern and does not match any known Moonwell cbETH market. The Moonwell protocol's official docs/contracts list the correct addresses; this one should be verified. [constants.ts:239]


Minor / Style

11. Variable named apr stored in apy field

In compound.ts and moonwell.ts, a variable called apr (simple linear annualization) is stored in the apy field of RateResult. True APY would use compounding. The math produces a linear APR, not a compound APY. Either rename the field to apr or compute the actual compound APY: ((1 + rate_per_second)^seconds_per_year - 1) * 100. For small rates the difference is minor, but the naming inconsistency could mislead LLM consumers. [adapters/compound.ts:59, adapters/moonwell.ts:46]

12. Test for routeSupply toward Moonwell is missing

The test suite has preferProtocol: "compound" covered but no test for preferProtocol: "moonwell". Given that Moonwell uses a distinct mToken lookup and encode path, that code path has no coverage. [lendingRouterActionProvider.test.ts]

13. getAggregatedPosition with explicit user parameter is not tested

The two tests that exist both omit user and rely on walletProvider.getAddress(). A test passing an explicit address would confirm the address plumbing works. [lendingRouterActionProvider.test.ts:240-275]


Checklist Compliance

Requirement Status
Actions return Promise<string>
Errors returned, not thrown
Zod v4 .describe() on every field, no .strip()
supportsNetwork correct (Base mainnet only)
Factory exported + re-exported from index.ts
Per-provider README.md
Changeset (patch, past tense)
pnpm test / pnpm build / lint / format passing ✅ (per PR)

Summary: 3 of the 6 critical issues (#1 Compound multi-market, #2 Moonwell decimals, coinbase#6 Morpho address cast) are correctness bugs that produce wrong outputs or would cause runtime failures for non-USDC assets or future Morpho execution. These should be fixed before merging. Issues #3 (health factor comparability) and #5 (rebalance reads positions, not just rates) are design-level and worth addressing even if they require a bit more work.

…h checks

- Replace single COMPOUND_COMET_ADDRESS with COMPOUND_COMET_ADDRESSES map (USDC + WETH Comets)
- Fix executeSupply/executeBorrow to look up correct Comet by asset symbol
- Add Compound health pre-check in routeBorrow alongside existing Aave check
- Rewrite rebalance to read user positions and compare against current protocol rate
- Fix RateResult.marketAddress → marketId (string, not Address — supports Morpho bytes32)
- Add PositionResult.healthComparable flag; skip non-comparable in findLowestHealth
- Fix encodeMoonwellMint signature (remove unused mToken param)
- Fix Moonwell position to use per-token decimals and non-comparable health
- Fix Morpho position user param type and add healthComparable: false
- Fix cbeth/cbbtc mToken addresses in constants
- Update tests for multi-Comet mocks and position-aware rebalance

https://claude.ai/code/session_01H9og3kM8YwPCcUCHnDxTxw
@zaryab2000

Copy link
Copy Markdown
Owner Author

@claude review the fixes

@claude

claude Bot commented Jun 6, 2026

Copy link
Copy Markdown

Claude finished @zaryab2000's task in 1m 59s —— View job


Fix Review — lendingRouter ActionProvider

Todo list:

  • Load tools and initialize
  • Read current state of adapter files
  • Check critical bug fixes from prior review
  • Check notable/minor issue fixes
  • Post consolidated findings

All 6 critical bugs from the first review are fixed. Here's the full scorecard:


Critical Bugs — All Fixed ✅

1. Compound multi-Comet (was: always queried USDC Comet) — ✅ Fixed
COMPOUND_COMET_ADDRESSES is now a Record<string, Address> mapping usdc and weth to their respective Comets. getCometForAsset() returns null for unsupported assets, causing getCompoundRates() to return null (skipped from results). Both executeSupply() and executeBorrow() look up the correct Comet by assetSymbol.toLowerCase() and throw a clear error if not found. [constants.ts:22-25, adapters/compound.ts:34-76]

2. Moonwell decimals (was: hardcoded 18 for all tokens) — ✅ Fixed
MOONWELL_UNDERLYING_DECIMALS in constants.ts maps each symbol to its correct decimals (usdc: 6, cbbtc: 8, others: 18). getMoonwellPosition() now passes MOONWELL_UNDERLYING_DECIMALS[symbol] ?? 18 to formatUnits. [constants.ts:293-299, adapters/moonwell.ts:92]

3. Moonwell health factor not comparable across protocols — ✅ Fixed
healthFactor now stores the raw USD liquidity surplus (not 1 + liquidity). More importantly, healthComparable: false means findLowestHealth() skips Moonwell entirely, and healthSource is set to "Comptroller liquidity surplus (USD, not a ratio)" so LLM consumers see the caveat. [adapters/moonwell.ts:80-85, utils.ts:44]

4. Unused mToken parameter in encodeMoonwellMint — ✅ Fixed
Signature is now encodeMoonwellMint(amount: bigint). Call site in executeSupply() passes only amount. [adapters/moonwell.ts:143, lendingRouterActionProvider.ts:531]

5. rebalance read rates-only, ignored user positions — ✅ Fixed
Rebalance now fetches rates and positions in parallel via Promise.all. It filters activeProtocols (positions with a supply of the target asset), returns a "no-op" with suggestion if none are found, and computes improvementBps relative to the user's actual current protocol. [lendingRouterActionProvider.ts:370-442]

6. Morpho marketId cast as Address (was: bytes32 key treated as 20-byte address) — ✅ Fixed
RateResult.marketId is typed as string in utils.ts. getMorphoRates() assigns best.uniqueKey directly without casting. The field name marketId is correctly scoped (not called marketAddress). [utils.ts:4, adapters/morpho.ts:88]


Notable Issues — All Fixed ✅

7. Compound notes hardcoded "Comet USDC market" — ✅ Fixed
Now uses Comet ${asset.toUpperCase()} market. [adapters/compound.ts:75]

8. No Compound health-factor pre-check before borrow — ✅ Fixed
routeBorrow() now checks both Aave (chosen.protocol === "aave") and Compound (chosen.protocol === "compound") health before executing. [lendingRouterActionProvider.ts:299-309]


Remaining Items (Minor — Not Blocking)

9. Sequential RPC calls in getCompoundPosition — Still present
The collateral loop makes multiple sequential readContract calls per collateral asset. Not a correctness issue; multicall batching is the improvement path for v2. [adapters/compound.ts:124-163]

10. cbeth mToken address updated but not verified — Needs confirmation
Address changed from 0x3bf93770f2d4a0F9a5a5F3d1a1e3a0c9B6C7E3e5 (suspicious repeated pattern) to 0x3bf93770f2d4a794c3d9EBEfBAeBAE2a8f09A5E5. The suspicious pattern is gone, but this should be cross-checked against Moonwell's official deployment registry before the PR is merged upstream. [constants.ts:243]

11. APR/APY field naming — Still present
getCompoundRates() and getMoonwellRates() compute a linear APR and store it in the apy field. The naming could mislead consumers. Not a correctness bug for v1; worth noting in v2. [adapters/compound.ts:68-72, adapters/moonwell.ts:47-50]

12. routeSupply → Moonwell path has no test — Still missing
There is no test with preferProtocol: "moonwell". The mToken lookup and encodeMoonwellMint code path remains uncovered. [lendingRouterActionProvider.test.ts]

13. getAggregatedPosition explicit user param not tested — Still missing
Both getAggregatedPosition tests omit the user argument. [lendingRouterActionProvider.test.ts:247-282]


Summary

All 6 critical correctness bugs and both notable issues are fixed. The rebalance redesign and Morpho type fix are particularly well done. Items 12–13 (test coverage) and item 10 (address verification) are the only open points worth addressing before merging upstream, though they don't block the build or tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action provider documentation Improvements or additions to documentation typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants