Clearing Manager Integration

For the Pool Clearing Manager: roles, contracts and signatures, preflight reads, batch sizes, the config-override decision, Safe batching and recovery.

This guide is for the Pool Clearing Manager — the operator who runs the per-epoch clearing for one or more Kasu lending pools. It covers everything required to either build an internal tool from scratch or wire the on-chain clearing flow into existing operational infrastructure (Safe Transaction Builder, custom bots, Defender, internal dashboards, etc.).

The reference implementation is kasu-clearing (the dashboard deployed at clearing.kasu.finance). This page extracts the contract surface, the operational sequence, and the off-chain data it relies on so an integrator can replicate the parts they need without porting the UI.

Audience and scope#

The Clearing Manager is the on-chain account (typically a Safe multisig) that holds ROLE_POOL_CLEARING_MANAGER for a given lending pool. Their responsibilities:

  • Trigger clearing every epoch, in the clearing window, for every pool they own.
  • Decide and submit per-epoch overrides (draw amount, tranche ratios, excess liquidity) when the default pool configuration is not appropriate for the upcoming epoch.
  • Re-submit follow-up doClearing() transactions when batched steps need more than one call.
  • Drive recovery actions when clearing was missed or partially executed.

This page does not cover pool configuration changes, fund recovery, force withdrawals, or first-loss capital management — those belong to the Pool Manager / Pool Funds Manager roles. See Roles & Access Control and Admin Operations.

Background reading before integrating:

On-chain surface#

Every clearing-related write goes through LendingPoolManager, which forwards to the singleton ClearingCoordinator after a role check. The coordinator owns the per-pool state machine. Reads can be split: status and epoch state live on ClearingCoordinator and SystemVariables; pool balances, configuration and tranche state live on the individual LendingPool.

Roles#

RoleConstantRequired for
ROLE_POOL_CLEARING_MANAGERkeccak256("ROLE_POOL_CLEARING_MANAGER")LendingPoolManager.doClearing()

ROLE_POOL_CLEARING_MANAGER is granted per-pool via KasuController.grantLendingPoolRole(lendingPool, ROLE_POOL_CLEARING_MANAGER, account). Use KasuController.hasLendingPoolRole(lendingPool, role, account) to verify before each run.

Contracts#

ContractPurpose for the integrator
LendingPoolManagerSingle write entry point — doClearing()
ClearingCoordinatorStatus reads — lendingPoolClearingStatus, isLendingPoolClearingPending, nextLendingPoolClearingEpoch, lendingPoolMaxDrawAmount
SystemVariablesEpoch state — currentEpochNumber, isClearingTime, epochStartTimestamp, nextEpochStartTimestamp
LendingPool (per-pool proxy)Pool reads — availableFunds, userOwedAmount, feesOwedAmount, poolConfiguration, clearingConfiguration, lendingPoolInfo
KasuControllerRole checks — hasLendingPoolRole

For deployed addresses on each network, see Deployed Addresses.

Key function signatures#

// LendingPoolManager
function doClearing(
    address lendingPool,
    uint256 targetEpoch,
    uint256 fixedTermDepositBatchSize,
    uint256 priorityCalculationBatchSize,
    uint256 acceptRequestsBatchSize,
    ClearingConfiguration calldata clearingConfigOverride,
    bool isConfigOverridden
) external;

// ClearingCoordinator
function lendingPoolClearingStatus(address lendingPool, uint256 epoch)
    external view returns (ClearingStatus);
function isLendingPoolClearingPending(address lendingPool)
    external view returns (bool);
function nextLendingPoolClearingEpoch(address lendingPool)
    external view returns (uint256);
function lendingPoolMaxDrawAmount(address lendingPool)
    external view returns (uint256);

// SystemVariables
function currentEpochNumber() external view returns (uint256);
function isClearingTime() external view returns (bool);
function nextEpochStartTimestamp() external view returns (uint256);

ClearingConfiguration is the per-epoch override struct:

struct ClearingConfiguration {
    uint256 drawAmount;              // USDC units (6 decimals on Base/XDC-USDC; AUDD-native units on XDC AUDD)
    uint256[] trancheDesiredRatios;  // one entry per tranche, FULL_PERCENT = 100_00 (4 decimals, sum must equal 100_00)
    uint256 maxExcessPercentage;     // FULL_PERCENT scale
    uint256 minExcessPercentage;     // FULL_PERCENT scale, must be <= max
}

ClearingStatus enum (returned from lendingPoolClearingStatus):

ValueMeaning
0 UNINITIALIZEDClearing has not been started for this epoch. The first doClearing() call will execute Step 1.
1 STEP1_PENDINGInterest applied; fixed-term interests still being processed in batches.
2 STEP2_PENDINGPending request priority calculation in batches.
3 STEP3_PENDINGShould be transient — Step 3 (accepted amounts) runs atomically in the same call as the transition.
4 STEP4_PENDINGProcessing accepted requests in batches.
5 STEP5_PENDINGShould be transient — Step 5 (draw funds) runs atomically.
6 ENDEDClearing complete for this epoch; nextLendingPoolClearingEpoch has incremented.

You will normally observe UNINITIALIZED → STEP2_PENDING → STEP4_PENDING → ENDED. The STEP1, STEP3, STEP5 states only stick if a step ran out of gas or a follow-up step's precondition was not met within the same transaction.

Operational sequence#

A complete per-epoch run, for one pool, is:

1. Wait until SystemVariables.isClearingTime() == true
2. LendingPoolManager.doClearing(...)   (loop until lendingPoolClearingStatus == ENDED)

Kasu runs the per-epoch KSU price update and loyalty-level processing internally before clearing opens — integrators do not need to call those. On Lite deployments (XDC AUDD, XDC USDC) loyalty does not exist at all.

Preflight reads#

Before submitting any transactions, read the following and use them to decide whether to proceed and what to submit:

ReadDecision
SystemVariables.isClearingTime()If false, do not submit — doClearing will revert with TargetEpochClearingNotStarted
SystemVariables.currentEpochNumber()This is your targetEpoch
ClearingCoordinator.nextLendingPoolClearingEpoch(pool)Must equal targetEpoch for a fresh run, or targetEpoch + 1 if already cleared for this epoch. If it lags by more than 1, the pool has a missed clearing (see Recovery)
ClearingCoordinator.lendingPoolClearingStatus(pool, targetEpoch)Tells you which step the next doClearing will execute
ClearingCoordinator.lendingPoolMaxDrawAmount(pool)Upper bound for the drawAmount override — exceeding this will revert in Step 3
LendingPool.poolConfiguration().desiredDrawAmountThe on-file desired draw — use as the default if not overriding
LendingPool.userOwedAmount()Outstanding USDC owed back to LPs from prior draws
LendingPool.feesOwedAmount()Performance fees that will be paid to FeeManager at end of clearing
KasuController.hasLendingPoolRole(pool, ROLE_POOL_CLEARING_MANAGER, sender)Hard precondition

Batch sizes#

doClearing takes three batch sizes — one per batched step. They are only consumed by the step currently executing in that call, so it is safe to send max-uint for all three on every call:

ParameterStepWhat it bounds
fixedTermDepositBatchSizeStep 1Number of fixed-term deposits whose interest is applied
priorityCalculationBatchSizeStep 2Number of pending requests scanned for priority
acceptRequestsBatchSizeStep 4Number of pending requests executed

kasu-clearing uses 1000 as the default (useDoClearing.ts:36-38) and the production Hardhat script uses ethers.MaxUint256 (scripts/doClearing.ts). For an internal tool, choose a value that fits your chain's block gas limit with a comfortable margin — e.g. start with 200500 on Base, and lower for chains with tighter gas limits. If a transaction reverts on out-of-gas, simply re-submit with a smaller batch — partial progress is checkpointed on-chain.

The default-config vs override-config decision#

ClearingConfiguration is captured on the lending pool itself (LendingPool.clearingConfiguration()) and rebuilt on every call from:

  • poolConfiguration().desiredDrawAmount
  • The current tranches[*].ratio values
  • targetExcessLiquidityPercentage and minimumExcessLiquidityPercentage

If you pass isConfigOverridden = false, Step 3 uses those stored values. If you pass true, the clearingConfigOverride you supply is used for this clearing only — the on-chain pool config is not modified.

Override when:

  • The desired draw exceeds lendingPoolMaxDrawAmount (Step 3 will revert otherwise — use the override to lower it).
  • Operations decided a different draw mid-epoch and you don't want to write a separate updateDesiredDrawAmount transaction first.
  • You want a one-off ratio shift without rolling it forward.

Use the stored config when nothing has changed and you trust the on-file values. The kasu-clearing dashboard always sends isConfigOverridden = true because the UI gathers fresh values from the operator on each run — that is a UI choice, not a contract requirement.

Multi-pool batching via Safe#

The reference implementation can batch multiple pools' clearings into a single Safe transaction. The pattern (see useSafeBatchClearing.ts):

  • Encode 5 doClearing() calls per pool (one for each possible step transition) with isConfigOverridden = true and the override struct.
  • Build a Safe transaction with all encoded calls flat-listed.
  • Propose via Safe Transaction Service; signers approve and execute together.

The 5-calls-per-pool default is conservative — most pools end clearing in 2–3 calls. Excess calls become no-ops (ClearingStatus.ENDEDClearingAlreadyExecuted revert), so the batch must use a Safe pattern that tolerates per-call reverts, or the caller must size the call count to match the actual expected step count for that pool.

If you build your own multi-pool runner, prefer one transaction per doClearing call so reverts only roll back the offending call. Sequencing them via a queue (Defender, custom bot) is simpler to reason about than a giant atomic batch.

Errors you'll hit#

These are the failure modes worth handling explicitly. All come from IClearingCoordinator.sol or the underlying steps.

ErrorCauseResolution
ClearingAlreadyExecuted(epoch)Sent doClearing for an epoch already at ENDED.Advance targetEpoch to nextLendingPoolClearingEpoch(pool). Treat as a benign duplicate in retry logic.
TargetEpochClearingNotStarted(epoch)isClearingTime() is false, or targetEpoch > currentEpoch.Wait for the clearing window.
InvalidClearingTargetEpochForLendingPool(pool, target, next)targetEpoch doesn't match nextLendingPoolClearingEpoch(pool).Use next from the revert (or read it first). The most common cause is a missed clearing — see Recovery.
PoolConfigurationIsIncorrect("...") (from verifyClearingConfig)Override has bad ratios (don't sum to FULL_PERCENT), minExcessPercentage > maxExcessPercentage, or nonzero values on a stopped pool.Fix the override before resubmitting.
Step 3 revert on draw amountdrawAmount > availableFunds + pendingDepositAmount.Override with a lower drawAmount. lendingPoolMaxDrawAmount(pool) gives the ceiling.

kasu-clearing parses these via src/lib/tx/parseErrors.ts — pattern that errs to copy if you want named error reporting in your tool.

Recovery#

The clearing window is the final 48 hours of each 7-day epoch. Two failure modes need explicit handling.

Halted at Step 1 (interest applied, no requests processed)#

If clearing ran Step 1 but the clearing window closed before Step 2 advanced, the coordinator forces clearingStatus = ENDED for that epoch. nextLendingPoolClearingEpoch advances, but user requests are left in the queue and roll over.

Action: nothing. The requests will be processed in the next epoch's clearing. The integrator's bot should detect this by noticing that lendingPoolClearingStatus(pool, prevEpoch) == ENDED while the pool's subgraph still shows Requested-status requests for prevEpoch — those will reappear under the next epoch.

Halted at Step 4 (partial request processing)#

If clearing entered Step 4 but ran out of gas mid-batch and the window then closed, the coordinator does not auto-end. doClearing must be called repeatedly until the status reaches ENDED — even after the clearing window. This is a contract requirement, not a courtesy.

Action: the Clearing Manager loops doClearing(pool, sameTargetEpoch, ...) until the read returns ENDED. The override config from the first Step 4 call is sticky for that epoch (Step 3 already ran), so the override struct values on follow-up calls are ignored — just send isConfigOverridden = false with zeroed values.

Detecting a stale pool#

isPending = clearingCoordinator.isLendingPoolClearingPending(pool)

returns true when either (a) nextLendingPoolClearingEpoch < currentEpoch, meaning a past epoch never reached ENDED, or (b) it's the current clearing window and the current epoch's clearing isn't done. Use it as the single signal for "this pool needs my attention right now."

Off-chain data via the Kasu SDK#

On-chain reads are sufficient to run clearing. For context and decision-making (sizing batches, displaying the request queue, post-clearing audit), use the official Kasu TypeScript SDK rather than querying the underlying data sources directly. The SDK encapsulates the subgraph endpoints, schemas, and CMS lookups per chain — if Kasu redeploys a subgraph or migrates a backend, the SDK upgrades the contract for integrators automatically.

npm install @kasufinance/kasu-sdk
import { Kasu } from '@kasufinance/kasu-sdk';

const kasu = Kasu.create({ chain: 'base', signerOrProvider: provider });

// Pool overview (TVL, tranches, available capacity, APY)
const strategy = await kasu.strategies.getById(poolAddress);

// Lower-level service access for clearing-specific data
const poolOverview = await kasu.services.DataService.getPoolOverview(epochId);

The SDK exposes per-chain configs for Base, XDC AUDD (xdc) and XDC USDC (xdc-usdc), plus the retired plume for its frozen history. See SDK Integration for the full surface.

If you need data the SDK does not currently expose (e.g. per-epoch lendingPoolClearings block ranges, raw UserRequest entities), open an issue against kasu-sdk rather than pinning to a Goldsky URL — the subgraph endpoints are an internal implementation detail and change without notice.

For loan-book reconciliation tied to draws (end-borrower balances, loan tickets), the kasu-agreements backend at allocations.api.kasu.finance is the authoritative source. Request an API key from Kasu ops.

Minimal integration recipe#

The smallest viable runner — pseudocode, transaction signing left to your own infrastructure:

async function runClearing(pool: Address, override?: ClearingConfiguration) {
  // 1. Verify we're in the window and have the role
  if (!(await systemVariables.read.isClearingTime())) throw "wait for clearing window";
  const sender = await wallet.getAddress();
  const hasRole = await kasuController.read.hasLendingPoolRole(
    [pool, ROLE_POOL_CLEARING_MANAGER, sender]
  );
  if (!hasRole) throw "not authorized as clearing manager for this pool";

  const targetEpoch = await systemVariables.read.currentEpochNumber();

  // 2. Decide the override
  const cfg = override ?? {
    drawAmount: 0n,
    trancheDesiredRatios: [],
    maxExcessPercentage: 0n,
    minExcessPercentage: 0n,
  };
  const useOverride = override !== undefined;

  // 3. Loop doClearing until ENDED
  for (;;) {
    const status = await clearingCoordinator.read.lendingPoolClearingStatus(
      [pool, targetEpoch]
    );
    if (status === ClearingStatus.ENDED) return;

    await lendingPoolManager.write.doClearing([
      pool,
      targetEpoch,
      500n,       // fixedTermDepositBatchSize
      500n,       // priorityCalculationBatchSize
      500n,       // acceptRequestsBatchSize
      cfg,
      useOverride,
    ]);
  }
}

Things this leaves out that a production runner needs:

  • Retry on transient RPC failures with exponential backoff (especially on XDC — see the XDC RPC notes about load-balanced node staleness).
  • Per-call gas estimation so batch sizes adapt to queue length.
  • Logging the ClearingExecuted event to confirm each transaction reached the expected status.
  • Detecting partial Step 4 over multiple transactions and refusing to send further unrelated calls until the pool is fully cleared.
  • Wiring to your alerting (PagerDuty / Slack / Linear) so that "missed clearing window" surfaces immediately.

Events to index#

If you want post-hoc audit or to drive UI off events, these are the minimum:

ContractEventUse
ClearingCoordinatorClearingExecuted(lendingPool, epoch, status)Tracks every doClearing transition, including the final ENDED
ClearingCoordinatorClearingConfigSet(lendingPool, epoch, config)Records the effective config (override or default) for that epoch
LendingPoolInterestApplied(tranche, epoch, interestAmount)Confirms Step 1 ran for each tranche
LendingPoolDepositAccepted(user, tranche, amount, shares)Per-user deposit fills from Step 4
LendingPoolWithdrawalAccepted(user, tranche, shares, amount)Per-user withdrawal fills from Step 4
LendingPoolFundsDrawn(amount)Step 5 — actual draw amount
LendingPoolOwedFundsRepaid(amountForUsers, amountForFees)Repayments between clearings (drives the "available to draw next epoch" number)
LendingPoolPaidFees(feesPaid)Performance fees paid to FeeManager at end of clearing

For repayments specifically (which happen between clearings, not during them), index OwedFundsRepaid over the block range between two ClearingExecuted events for the same pool — that gives you per-epoch fund flow.

See also#