Developer docs
All Sullend Labs projects are open-source and MIT licensed. Documentation is maintained alongside the code in the same repository — if something is out of date, pull requests are welcome and reviewed within five business days.
These docs assume you are building on an EVM-compatible chain and are comfortable with TypeScript and Solidity basics. Where concepts require additional context, we link to external references rather than reproduce documentation that already exists.
If you encounter behavior not covered here, open an issue in the relevant repository before assuming it is a bug. Many apparent edge cases have intentional explanations that aren't obvious from the interface alone.
Sullend SDK
Installation
Install the SDK via npm, pnpm, or yarn. The package ships ESM and CJS builds. No additional configuration is required for either module system.
npm install @sullend/sdk
With pnpm:
pnpm add @sullend/sdk
The SDK has no runtime dependencies beyond your provider library. Peer dependencies are ethers.js v6 or viem — install whichever you already use. Neither is required if you implement the provider interface directly.
Quick start
Initialize the client with a provider and start reading protocol state. The example below uses viem, but the same initialization works with an ethers.js v6 provider or any object implementing the read interface.
import { SullendClient } from '@sullend/sdk'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const provider = createPublicClient({
chain: mainnet,
transport: http()
})
const client = new SullendClient({ provider })
// Read pool state — batched automatically via Multicall3
const pool = await client.corelend.getPool('0x...')
console.log(pool.supplyRate, pool.utilizationRate)
// Read multiple pools in one RPC call
const [poolA, poolB, poolC] = await client.corelend.getPools([
'0x...A',
'0x...B',
'0x...C'
])
All read calls that accept an array of addresses are batched via Multicall3 automatically. You do not need to configure or invoke the batch explicitly — passing multiple addresses is sufficient.
Multicall batching
The SDK uses Multicall3 to batch read calls. When you call any method
that accepts an array of addresses or identifiers, the SDK constructs a
single eth_call to the Multicall3 contract instead of
issuing one RPC request per item.
For a dashboard reading 40 pool states, this reduces the RPC footprint from 40 sequential requests to one. For a liquidation bot reading positions across multiple protocols, the latency reduction is material.
// Without batching: 40 RPC calls
const pools = await Promise.all(addresses.map(a => client.corelend.getPool(a)))
// With SDK batching: 1 RPC call
const pools = await client.corelend.getPools(addresses)
Multicall3 is deployed on all major EVM chains. If you are deploying
to a chain where it is not available, pass multicall: false
to the client options and the SDK will fall back to individual calls.
const client = new SullendClient({
provider,
multicall: false // disables batching on chains without Multicall3
})
API reference
The complete API reference is generated from TypeScript source and published at docs.sullendlabs.xyz/sdk/api. Because types are generated from contract ABIs, the reference is always consistent with the deployed contracts.
Key namespaces on the client object:
client.corelend— CoreLend pool reads and write preparationclient.govern— Govern proposal and voting readsclient.multicall— Direct Multicall3 access for custom batchingclient.provider— The underlying provider, unmodified
Write methods (transactions) return prepared transaction objects, not submitted transactions. Signing and submission remain in your application layer. This keeps the SDK compatible with any wallet integration pattern without taking a dependency on a specific signer implementation.
CoreLend Protocol
Architecture
CoreLend separates protocol concerns into four independent contracts. Each contract has a single responsibility and communicates with others through narrow, stable interfaces. The design makes each component independently auditable and individually replaceable without redeploying the full system.
- PoolCore.sol — holds deposit/borrow state per asset pair. Knows nothing about how interest is calculated or where prices come from. Delegates both concerns to registered contracts.
- RateModel.sol — calculates interest rates given utilization. Stateless pure function. Can be swapped per pool without touching PoolCore.
- OracleRouter.sol — normalizes prices from multiple oracle sources into a single interface. PoolCore calls one function; OracleRouter decides which oracle is authoritative.
- Liquidator.sol — handles undercollateralized position resolution. Calls back into PoolCore to execute the liquidation; does not hold state itself.
Each contract can be replaced without redeploying the others, provided the interface is maintained. An oracle provider migration requires only a new OracleRouter deployment and a single registration call on PoolCore. No position state is migrated. No user action is required.
Pool mechanics
Each CoreLend pool is isolated to a single asset pair. Deposits and borrows in one pool do not affect the liquidity or risk parameters of any other pool. A bad debt event — a position that cannot be fully liquidated — is contained to the pool where it occurred.
Positions are represented as share tokens rather than raw asset amounts. When you deposit, you receive shares in the pool's supply side. The share price increases as interest accrues. When you withdraw, your shares are redeemed for the underlying asset plus accumulated interest. This model eliminates a class of rounding errors present in some alternative designs.
Collateral factors and liquidation thresholds are set per pool at deployment and can be adjusted by the pool operator within bounds defined at the factory level. Tightening parameters (lower collateral factor) takes effect immediately. Loosening parameters (higher collateral factor) is subject to a timelock to prevent oracle-manipulation attacks that exploit the window between parameter change and position adjustment.
Deployment
CoreLend is currently available on Sepolia testnet only. Mainnet deployment will follow the completion and publication of the third-party audit report.
# Clone and install
git clone https://github.com/sullendlabs/corelend
cd corelend
pnpm install
# Run unit tests
pnpm test
# Run forked mainnet integration tests (requires RPC_URL)
RPC_URL=https://... pnpm test:fork
# Deploy to Sepolia
DEPLOYER_KEY=0x... pnpm deploy:sepolia
The deployment script outputs the addresses of all deployed contracts and
writes a deployment manifest to deployments/{chainId}.json.
This manifest is the source of truth for the SDK's contract address
configuration on that chain.
Contract reference
Natspec documentation is generated from source using
forge doc and published alongside each release.
The generated reference includes all public functions, events,
custom errors, and documented invariants.
Key functions on PoolCore:
deposit(asset, amount, onBehalfOf)— supply assets to a pool and receive sharesborrow(asset, amount, onBehalfOf)— borrow against deposited collateralrepay(asset, amount, onBehalfOf)— repay an outstanding borrow positionwithdraw(asset, shares, recipient)— redeem supply shares for underlying assetgetPoolState(asset)— read current supply rate, borrow rate, and utilization
Govern
Overview
Govern is a minimal DAO governance toolkit currently in active development. The contracts are available in the repository for review and comment, but are not yet audited and must not be used in production or with real funds.
The design goal is the smallest possible governance system that covers the primary use case: a community-controlled protocol that needs proposal creation, token-weighted voting, time-delayed execution, and a treasury. Everything outside that scope is explicitly excluded unless there is a compelling reason to include it.
Target release: Q3 2025. Full documentation will expand as the contracts stabilize toward the audit-ready state.
Modules
- Governor.sol — proposal creation, voting window management, quorum checking, and execution gating. The central coordination contract.
- Timelock.sol — delay controller between proposal passage and execution. Interface-compatible with OpenZeppelin TimelockController. Minimum delay is set at deployment and cannot be reduced below the configured floor.
- Treasury.sol — custody of ETH and ERC-20 assets. Accepts deposits permissionlessly. Executes withdrawals only via a passed and executed proposal routed through the Timelock. No admin keys; governance is the only execution path.
- VotingToken.sol — ERC-20 with built-in delegation. Voting power is tracked via a checkpoint system. Snapshot at proposal creation, not at vote time, eliminating the flash loan voting attack vector.
Voting mechanics
Voting power is determined by token balance at the block the proposal was created. Delegation is supported — a holder can delegate their voting power to any address without transferring their tokens. Self-delegation is required to activate voting power; undelegated tokens do not count toward any vote.
Quorum is defined as a percentage of the total supply at proposal creation time, not the circulating supply. This prevents quorum manipulation via token burns or supply changes during the voting window.
Proposals have three configurable time parameters: voting delay (blocks between proposal creation and voting start), voting period (blocks during which votes are accepted), and timelock delay (seconds between vote passage and earliest execution). All three are set by governance and subject to the minimum values defined at deployment.
Contributing
Before you open a PR
Open an issue before submitting large PRs. For bug fixes and small improvements, PRs are welcome directly without a prior issue. For new features, architectural changes, or anything that modifies public interfaces, an issue discussion first saves everyone time.
All contributions require:
- Tests covering the changed behavior — not just coverage, but meaningful tests against the edge cases your change introduces or resolves
- Updated documentation if the public API changes — doc changes go in the same PR, not a follow-up
- A clear commit message following conventional commits format
- No new dependencies without discussion — open an issue first if you think a dependency is warranted
Review timeline
We review PRs within 5 business days. If a PR has been open longer than that without response, ping the issue or add a comment — it may have been missed in triage. We do not close PRs without explanation.
Security issues
Do not open public issues for security vulnerabilities. Send a private report to the email address in the repository's SECURITY.md file. We acknowledge reports within 48 hours and aim to publish a fix and disclosure within 14 days of confirmation.