Lend — SomniaLend through the SDK
SomniaLend is a third-party money market on Somnia mainnet and testnet (an Aave v3.0 fork —
docs.somnialend.finance). The SDK wraps its
deployed contracts behind the client.lend namespace,
so trading capital can earn while idle: supply USDso between sessions, post it
as collateral, borrow working capital against it.
It lives on the master client as the lend namespace. Because SomniaLend is
third-party, its addresses are not in the deployments manifests — wire them in
config, with the published deployments available as constants:
import { SOMNIA_MAINNET_LEND } from "@somnia-chain/markets-sdk";
const exchange = new SomniaMarkets({
...config,
addresses: { ...addresses, lend: SOMNIA_MAINNET_LEND },
});
const lend = exchange.client.lend;
SOMNIA_MAINNET_LEND (chain 5031) and SOMNIA_TESTNET_LEND (chain 50312)
carry the two published deployments (Pool, PoolAddressesProvider,
UiPoolDataProviderV3, WrappedTokenGatewayV3), verified against the on-chain
contracts. Set one as addresses.lend and reach the surface through
client.lend; that is the only entry point, so a lend read always rides the
client's own chain transport.
Everything else you need without the client comes from the root entry
(@somnia-chain/markets-sdk): the types, both deployment constants, the
ray-math helpers (lendRayRateToApy, rayMul, …) and the verified minimal ABIs.
Targeting a different deployment means a different client — the addresses and
the chain travel together, so new SomniaMarkets({ chain, wsRpcUrl, addresses: { lend } }). (A createLend(client, addresses) factory used to be exported; it took
a whole client to use two of its members, and let one chain's addresses be grafted
onto another chain's socket, which type-checked and silently read nothing.)
Reads
Two calls cover the whole surface — both are chain reads over the client's WebSocket, current to head, no indexer involvement:
const reserves = await lend.listReserves(); // every listed asset
const account = await lend.getAccount(me); // health factor + positions
listReserves is one aggregated eth_call (UiPoolDataProviderV3): per reserve
you get the config (LTV / liquidation threshold / caps / flags), live rates and
indexes, available liquidity, total debt, and the oracle price. getAccount
joins the Pool's risk aggregate (health factor, borrowing power) with every
non-empty supplied/borrowed position.
Units, in the SDK-wide convention (bigint raw units everywhere):
- Rates and indexes are ray (1e27). Convert a rate for display with
lendRayRateToApy(r.liquidityRateRay)— the per-second-compounded APY fraction Aave UIs show. healthFactoris a wad (1e18). Below1e18the position is liquidatable; a debt-free account reportsmaxUint256.*Baseaggregates (totalCollateralBase,availableBorrowsBase, reserve prices) are denominated in the oracle base currency — USD with 8 decimals on this deployment (baseCurrencyDecimalsrides along).borrowCap/supplyCapare whole tokens (Aave convention), not raw units;0means uncapped.- Balances (
aTokenBalance,variableDebt,totalSupplied, …) are raw underlying units, accrued to the read's timestamp with Aave's own interest math (linear for supply, compounded for debt) — they match what the Pool would settle, not the last stored checkpoint.
Writes
const lender = lend.createLender({ privateKey }); // or account / walletClient
await lender.supply(usdso, 1_000n * 10n ** 18n); // auto-approves the Pool
await lender.borrow(usdce, 500n * 10n ** 6n); // variable rate only
await lender.repay(usdce, maxUint256); // maxUint256 = full debt
await lender.withdraw(usdso, maxUint256); // maxUint256 = full balance
await lender.setUseAsCollateral(usdso, true);
Same doctrine as the trader: writes resolve once mined (with the receipt), gas
is a fixed generous ceiling, fees are the client's fixed EIP-1559 config, and
ERC-20 pulls auto-approve maxUint256 once per (token, spender) with an
in-memory grant cache (approve: false opts out per call;
clearApprovalCache() resets). Borrowing is variable-rate only — stable
borrowing isn't surfaced.
Native SOMI has gateway-routed siblings so you never touch WSOMI yourself:
supplyNative / withdrawNative / borrowNative / repayNative.
Two of them need a one-time grant the SDK also handles automatically:
withdrawNative approves the gateway to pull your aWSOMI, and borrowNative
delegates borrowing power on the WSOMI variable-debt token
(approveDelegation). repayNative doesn't take maxUint256 — overpay
slightly instead; the gateway refunds the excess.
Risk notes
- A borrow that would push the health factor below 1 reverts; watch
getAccount(me).healthFactorand size againstavailableBorrowsBase. - Liquidation pays the liquidator
liquidationBonusBpsout of your collateral — keep headroom, especially against WSOMI's price. - Reserve flags matter:
isFrozenblocks new supplies/borrows,isPausedblocks everything; check them before sizing. - The SDK talks to SomniaLend's contracts as deployed — protocol risk (upgrades, oracle, listing params) is SomniaLend's admin surface, not this SDK's. See their risks page.