Skip to content

Functions

Note

Some of the parameters require adapter and data to be set.
- If the adapter is an ERC20, the data parameter is the amount.
- For ERC721, the data parameter is the token ID.

Access control

The pool has no owner. Administrative functions are gated by AccessControl roles: PROTOCOL_ROLE for every setter and risk parameter, GOVERNANCE_ROLE for the four fee parameters, and EMERGENCY_ROLE for pause / unpause only. See Access Control.

Liquidity Provision

deposit

deposit(uint256 amount)

Summary
Deposits the reserve asset (pmUSD) into the pool and mints RToken 1:1 for the caller, minus any deposit fee.

Same-block transfers are blocked

After depositing the main asset and receiving newly minted RToken, you cannot transfer those tokens to another address within the same transaction/block. Avoid attempting an RToken transfer in the same transaction as your deposit.

Parameters

Name Type Description
amount uint256 Deposit amount in pmUSD (18 decimals).

Emits

  • Deposit(address indexed user, uint256 amount, uint256 rTokenMinted)

Reverts

  • InvalidAmount() — if amount == 0
  • EnforcedPause() — if the pool is paused (the StabilityPool is exempt)
  • MinDepositAmountRequired() — if the amount net of the deposit fee is below MinDepositAmount
  • SupplyCapReached() — if LendingPoolSupplyCap would be exceeded

Requirements

  • Deposit fee (LendingPoolDepositFee) goes to FeeCollector before minting RToken.
  • Withdrawals may be paused but deposits remain open unless explicitly disabled.
  • The minimum and the supply cap are checked on the post-fee amount.

The StabilityPool takes a different path

deposit is not marked whenNotPaused; it checks the pause inline and exempts the StabilityPool, so the pool can still re-deposit liquidation proceeds while the protocol is paused. The StabilityPool also pays no deposit fee, skips the minimum and supply-cap checks, and does not have its depositBlock stamped — so the same-block guard never applies to it.

TS
const tx = await lendingPool.deposit(parseUnits("1000", 18));
await tx.wait();
Source code
function deposit(uint256 amount) external nonReentrant onlyValidAmount(amount) notBlacklisted(msg.sender) {
    // allow stability pool to bypass the contract pause for liquidations
    if (msg.sender != stabilityPool && paused()) revert EnforcedPause();
    ReserveLibrary.updateReserveState(reserve, rateData);

    // Calculate the deposit fee. bypass if stability pool is calling
    uint256 depositFeeAmount = (feeCollector == address(0) || msg.sender == stabilityPool) ? 0 : amount.percentMul(parameters.depositFee);
    uint256 depositAmount = amount - depositFeeAmount;
    _validateDeposit(depositAmount);

    uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, depositAmount, msg.sender);

    _rebalanceLiquidity();

    if(depositFeeAmount > 0) _collectFee(depositFeeAmount, keccak256("LENDING_FEE"));

    if (msg.sender != stabilityPool) depositBlock[msg.sender] = block.number;

    emit Deposit(msg.sender, depositAmount, mintedAmount);
}

function _validateDeposit(uint256 amount) internal view {
    // if the sender is the stability pool, we do not need to check the supply cap
    if (msg.sender == stabilityPool) return;
    if (amount < parameters.minDepositAmount) revert MinDepositAmountRequired();
    if (IRToken(reserve.reserveRTokenAddress).totalSupply() + amount > parameters.supplyCap) revert SupplyCapReached();
}

withdraw

withdraw(uint256 amount)

Summary
Burns the caller’s RToken balance to redeem reserve assets (pmUSD).
Updates reserve state, ensures liquidity, and rebalances after the withdrawal.

Guarded Method - Callable by any user with RToken balance. - Restricted by nonReentrant and blacklist checks. - The pause and the WithdrawalStatus flag are both checked inline, and both exempt the StabilityPool — it can withdraw to cover a liquidation shortfall while the protocol is paused.

Full Withdrawal Tip

Pass uint256.max as the amount parameter to withdraw your entire RToken balance.
This ensures that no residual "dust" remains due to rounding or index scaling.

Parameters

Name Type Description
amount uint256 Amount of reserve assets to withdraw. Use uint256.max to withdraw full balance.

Emits - Withdraw(address indexed user, uint256 amountWithdrawn)

Reverts

  • InvalidAmount() — if amount == 0.
  • EnforcedPause() — if the pool is paused (non-StabilityPool callers).
  • WithdrawalsArePaused() — if WithdrawalStatus is set (non-StabilityPool callers).
  • InsufficientBalance() — if caller’s RToken balance < requested amount.
  • Bubbled from VaultProxy.ensureLiquidity — if the buffer plus the vault cannot cover the withdrawal.

Requirements

  • Caller must hold at least amount RToken.
  • Pool liquidity must be sufficient after rebalancing.
  • Reserve state updated via ReserveLibrary.updateReserveState before execution.
Example (TypeScript / ethers)
// Withdraw specific amount (1000 pmUSD)
const tx = await lendingPool.withdraw(parseUnits("1000", 18));
await tx.wait();

// Withdraw full balance without leaving dust
const txFull = await lendingPool.withdraw(constants.MaxUint256);
await txFull.wait();
Source code
function withdraw(uint256 amount) external nonReentrant onlyValidAmount(amount) notBlacklisted(msg.sender) {
    // allow stability pool to bypass the contract pause for liquidations
    if (msg.sender != stabilityPool && paused()) revert EnforcedPause();
    if (msg.sender != stabilityPool && parameters.withdrawalsPaused) revert WithdrawalsArePaused();

    ReserveLibrary.updateReserveState(reserve, rateData);

    uint256 scaledAmount = (amount == type(uint256).max) 
        ? IERC20(reserve.reserveRTokenAddress).balanceOf(msg.sender) 
        : amount;

    if (scaledAmount > IERC20(reserve.reserveRTokenAddress).balanceOf(msg.sender)) revert InsufficientBalance();

    _ensureLiquidity(scaledAmount);

    (uint256 amountWithdrawn,,) = ReserveLibrary.withdraw(
        reserve,
        rateData,
        scaledAmount,
        msg.sender
    );

    _rebalanceLiquidity();

    emit Withdraw(msg.sender, amountWithdrawn);
}

Collateral management

openVaultPosition

openVaultPosition()

Summary
Initializes a user’s vault for collateral management.
Must be called once before using depositAsset or borrow.
Collects a one-time fee if configured.

Guarded Method

  • Callable by any user not blacklisted.
  • No nonReentrant guard since it is a simple state change.

Parameters - None

Emits - VaultOpeningFeePaid(address indexed user, uint256 fee)

Reverts - VaultPositionAlreadyOpened() — if vault already initialized.

Requirements

  • One-time action per user.
  • If vaultOpeningFee > 0, fee is transferred to FeeCollector.
Example (TypeScript / ethers)
const tx = await lendingPool.openVaultPosition();
await tx.wait();
Source code
function openVaultPosition() external notBlacklisted(msg.sender) {
    if (vaultOpened[msg.sender]) revert VaultPositionAlreadyOpened();
    vaultOpened[msg.sender] = true;
    if (parameters.vaultOpeningFee > 0) _collectFee(parameters.vaultOpeningFee, keccak256("LENDING_FEE"));
    emit VaultOpeningFeePaid(msg.sender, parameters.vaultOpeningFee);
}

depositAsset

depositAsset(address adapter, bytes calldata data)

Summary
Supplies collateral into the pool through a registered adapter.
Updates reserve state, verifies that the vault is open, and delegates the deposit logic to the adapter.

Guarded Method

  • Callable by any user with an open vault.
  • Protected by nonReentrant, whenNotPaused, onlySupportedAdapter, and blacklist checks.

Parameters

Name Type Description
adapter address Registered IAssetAdapter to route deposit
data bytes Adapter-specific calldata

Emits - May emit adapter-specific events via IAssetAdapter.deposit.

Reverts

  • CannotDepositWhenUnderLiquidation() — if the position is flagged under liquidation.
  • VaultPositionNotOpened() — if openVaultPosition() has not been called.
  • UnsupportedAdapter() — if adapter not registered.

Requirements

  • User must have paid the VaultOpeningFee.
  • Collateral type must be whitelisted.
  • Reserve state updated via ReserveLibrary.updateReserveState.
Example (TypeScript / ethers)
// Approve adapter to spend the asset
await assetContract.approve(adapterAddr, amount);

const tx = await lendingPool.depositAsset(adapterAddr, adapterData);
await tx.wait();
Source code
function depositAsset(address adapter, bytes calldata data) external nonReentrant whenNotPaused onlySupportedAdapter(adapter) notBlacklisted(msg.sender) {
    CollateralPosition memory position = getPosition(adapter, msg.sender, data);
    if (position.isUnderLiquidation) revert CannotDepositWhenUnderLiquidation();
    if (!vaultOpened[msg.sender]) revert VaultPositionNotOpened();
    ReserveLibrary.updateReserveState(reserve, rateData);
    IAssetAdapter(adapter).deposit(msg.sender, data);
}

withdrawAsset

withdrawAsset(address adapter, bytes calldata data)

Summary
Withdraws collateral from the pool via a registered adapter.
Ensures that health factor remains above liquidation threshold after withdrawal.

Guarded Method
- Callable by any user with sufficient collateral.
- Protected by nonReentrant, whenNotPaused, onlySupportedAdapter, and blacklist checks.

Parameters

Name Type Description
adapter address Registered IAssetAdapter for the collateral
data bytes Adapter-specific calldata

Emits - May emit adapter-specific events via IAssetAdapter.withdraw.

Reverts - CannotWithdrawUnderLiquidation() — if position is flagged under liquidation.
- WithdrawalWouldLeaveUserUnderCollateralized() — if withdrawal reduces health factor below HEALTH_FACTOR_LIQUIDATION_THRESHOLD.

Requirements - Health factor after withdrawal must remain ≥ threshold.
- Reserve state updated via ReserveLibrary.updateReserveState.
- Collateral must be whitelisted.

Example (TypeScript / ethers)
const tx = await lendingPool.withdrawAsset(adapterAddr, adapterData);
await tx.wait();
Source code
function withdrawAsset(address adapter, bytes calldata data) external nonReentrant whenNotPaused onlySupportedAdapter(adapter) notBlacklisted(msg.sender) {
    CollateralPosition memory position = getPosition(adapter, msg.sender, data);
    if (position.isUnderLiquidation) revert CannotWithdrawUnderLiquidation();

    ReserveLibrary.updateReserveState(reserve, rateData);

    uint256 positionDebt = getPositionScaledDebt(adapter, msg.sender, data);
    if (positionDebt > 0) {
        uint256 depositedValue = IAssetAdapter(adapter).getAssetValue(msg.sender, data);
        uint256 withdrawalValue = IAssetAdapter(adapter).getWithdrawValue(msg.sender, data);
        uint256 newCollateralValue = depositedValue - withdrawalValue;
        uint256 newHealthFactor;
        if (positionDebt < 1) newHealthFactor = WadRayMath.RAY;
        else {
            uint256 collateralThreshold = newCollateralValue.percentMul(parameters.borrowThreshold);
            newHealthFactor = (collateralThreshold * 1e18) / positionDebt;
        }
        if (newHealthFactor < HEALTH_FACTOR_LIQUIDATION_THRESHOLD) {
            revert WithdrawalWouldLeaveUserUnderCollateralized();
        }
    }

    IAssetAdapter(adapter).withdraw(msg.sender, data);
}

Borrow and repayments

borrow

borrow(address adapter, bytes data, uint256 amount)

Summary
Borrows the reserve asset (pmUSD) against the caller’s collateral managed by adapter.
Internally validates collateral, ensures pool liquidity (vault pull if needed), mints scaled DebtToken, transfers the borrowed amount to the user, updates indices/rates, and rebalances liquidity.
If the position had insurance, it is cleared after a successful borrow.

Parameters

Name Type Description
adapter address Registered IAssetAdapter for the collateral position
data bytes Adapter-specific calldata identifying the position
amount uint256 Amount of reserve asset to borrow (18 decimals)

Returns
- None

Emits
- Borrow(address indexed user, uint256 amount)

Reverts

  • CannotBorrowUnderLiquidation() — if the position is flagged under liquidation.
  • Any error from _validateBorrow — insufficient collateral vs. BorrowThreshold / caps.
  • Liquidity errors from _ensureLiquidity — if the pool cannot source funds (after vault pull).
  • Adapter/role/amount guards listed above.

Side Effects

  • Clears insurance state on the position after a successful borrow:
  • position.isInsured = false
  • position.rawInsuredBalance = 0

Requirements

  • Adapter must be registered and support the asset.
  • Post-borrow health factor must remain ≥ liquidation threshold (enforced by _validateBorrow).
  • Sufficient pool liquidity must be obtainable (_ensureLiquidity).
  • Indices updated before state changes to preserve accrual correctness.

Notes

  • Insurance is intentionally cleared on borrow. To remain insured after borrowing again, the user must re-purchase insurance according to the protocol rules.
  • Debt accounting uses scaled amounts against reserve.usageIndex.
Example (TypeScript / ethers)
import { parseUnits } from "ethers";

// Adapter + data identify the collateral position (e.g., NFT vault, ERC20 vault)
const adapter = "0xAdapter...";
const data = adapterSpecificCalldata; // bytes encoded off-chain per adapter ABI
const amount = parseUnits("5000", 18); // 5,000 pmUSD

const tx = await lendingPool.borrow(adapter, data, amount);
await tx.wait();
Source code (public wrapper and internal implementation)
function borrow(address adapter, bytes calldata data, uint256 amount)
    external
    nonReentrant
    whenNotPaused
    onlyValidAmount(amount)
    onlySupportedAdapter(adapter)
    notBlacklisted(msg.sender)
{
    _borrow(adapter, data, amount);
    CollateralPosition storage position = positions[msg.sender][IAssetAdapter(adapter).getPositionKey(data)];
    if (position.isInsured) {
        position.isInsured = false;
        position.rawInsuredBalance = 0;
    }
}

function _borrow(address adapter, bytes calldata data, uint256 amount) internal {
    CollateralPosition storage position = positions[msg.sender][IAssetAdapter(adapter).getPositionKey(data)];
    if (position.isUnderLiquidation) revert CannotBorrowUnderLiquidation();

    ReserveLibrary.updateReserveState(reserve, rateData);
    _validateBorrow(adapter, data, amount);
    _ensureLiquidity(amount);

    (, uint256 underlyingAmount, uint256 userIncrease, ) =
        IDebtToken(reserve.reserveDebtTokenAddress).mint(
            msg.sender,
            msg.sender,
            amount,
            reserve.usageIndex,
            abi.encode(adapter, data)
        );

    position.positionIndex = reserve.usageIndex;
    IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
    position.rawDebtBalance += underlyingAmount + userIncrease;

    ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);
    _rebalanceLiquidity();

    emit Borrow(msg.sender, amount);
}

borrowWithInsurance

borrowWithInsurance(address adapter, bytes data, uint256 amount)

Summary
Executes a borrow and simultaneously insures the resulting debt.
The insurance fee is calculated and collected upfront before the state changes.
Post-borrow, the user’s position is marked insured for both the newly borrowed amount and any previously uninsured balance.

Premium excludes interest accrued since the last mint or burn

The fee is quoted before _borrow capitalises pending interest, so that interest is not charged on this call and is not added to rawInsuredBalance. It is charged on the next insured borrow; if there is none, it is covered by the grace period for free. See Insurance.

Differences from borrow
- Requires the user to have approved and hold sufficient reserve asset to cover the insuranceFeeAmount.
- Calls _collectFee(..., INSURANCE_FEE) immediately after the borrow.
- Sets position.isInsured = true and increments rawInsuredBalance by uninsuredBalance + amount.
- Emits InsuredAsset(...) in addition to the normal Borrow event.

Extra Reverts
- InsufficientAllowanceForFees() — if ERC20 allowance < insuranceFeeAmount.
- InsufficientBalance() — if ERC20 balance < insuranceFeeAmount.

Notes
- Borrowing again without this function clears existing insurance.
- To stay insured across multiple borrows, always use borrowWithInsurance.

Example (TypeScript / ethers)
const adapter = "0xAdapter...";
const data = adapterSpecificCalldata;
const amount = parseUnits("3000", 18);

// Approve insurance fee beforehand
await reserveAsset.approve(lendingPool.target, expectedFee);

const tx = await lendingPool.borrowWithInsurance(adapter, data, amount);
await tx.wait();
Source code
function borrowWithInsurance(address adapter, bytes calldata data, uint256 amount)
    external
    nonReentrant
    whenNotPaused
    onlyValidAmount(amount)
    onlySupportedAdapter(adapter)
    notBlacklisted(msg.sender)
{
    CollateralPosition storage position =
        positions[msg.sender][IAssetAdapter(adapter).getPositionKey(data)];

    uint256 uninsuredBalance = getUninsuredBalance(adapter, msg.sender, data);
    uint256 insuranceFeeAmount = calculateInsuranceFee(adapter, msg.sender, data, amount);

    if (IERC20(reserve.reserveAssetAddress).allowance(msg.sender, address(this)) < insuranceFeeAmount)
        revert InsufficientAllowanceForFees();
    if (IERC20(reserve.reserveAssetAddress).balanceOf(msg.sender) < insuranceFeeAmount)
        revert InsufficientBalance();

    _borrow(adapter, data, amount);

    _collectFee(insuranceFeeAmount, keccak256("INSURANCE_FEE"));
    position.isInsured = true;
    position.rawInsuredBalance += uninsuredBalance + amount;

    emit InsuredAsset(IAssetAdapter(adapter).getAssetToken(), msg.sender, data, uninsuredBalance);
}

getUninsuredBalance

getUninsuredBalance(address adapter, address user, bytes data) → uint256

Summary
Returns the portion of a user’s debt that is not covered by insurance.
Computed as rawDebtBalance - rawInsuredBalance for the given collateral position.

Parameters

Name Type Description
adapter address Registered IAssetAdapter for the collateral
user address Address of the borrower
data bytes Adapter-specific calldata identifying the position

Returns

Type Description
uint256 Uninsured portion of the debt
Example (TypeScript / ethers)
const uninsured = await lendingPool.getUninsuredBalance(adapterAddr, userAddr, adapterData);
console.log("Uninsured balance:", uninsured.toString());
Source code
function getUninsuredBalance(address adapter, address user, bytes calldata data)
    public
    view
    returns (uint256)
{
    CollateralPosition memory position = getPosition(adapter, user, data);
    return position.rawDebtBalance - position.rawInsuredBalance;
}

calculateInsuranceFee

calculateInsuranceFee(address adapter, address user, bytes data, uint256 amount) → uint256

Summary
Computes the insurance fee owed for a user’s position if they borrow with insurance.
The fee covers both the new borrow amount and any previously uninsured balance.

Parameters

Name Type Description
adapter address Registered IAssetAdapter for the collateral
user address Address of the borrower
data bytes Adapter-specific calldata identifying the position
amount uint256 Amount of reserve asset intended to be borrowed

Returns

Type Description
uint256 Insurance fee in pmUSD

Formula

insuranceFee = (uninsuredBalance + amount) * insuranceFeeRate

  • uninsuredBalance = from getUninsuredBalance
  • insuranceFeeRate = parameters.insuranceFee (basis points)

Notes

  • If the user borrows without insurance initially, then later decides to insure, they must pay on their entire uninsured raw debt, not just the new amount.
  • uninsuredBalance uses raw (recorded) debt, so interest accrued since the last mint or burn is not included in the fee.
  • Pure view function: no state changes, no events.
Example (TypeScript / ethers)
const fee = await lendingPool.calculateInsuranceFee(adapterAddr, userAddr, adapterData, parseUnits("2000", 18));
console.log("Insurance fee:", fee.toString());
Source code
function calculateInsuranceFee(address adapter, address user, bytes calldata data, uint256 amount)
    public
    view
    returns (uint256)
{
    uint256 uninsuredBalance = getUninsuredBalance(adapter, user, data);
    return (uninsuredBalance + amount).percentMul(parameters.insuranceFee);
}

repay

repay(address adapter, bytes data, uint256 amount)

Summary
Repays the caller’s own debt for a specific collateral position and updates indices, debt balances, insurance status, rates, and liquidity.

Parameters

Name Type Description
adapter address Registered IAssetAdapter for the collateral
data bytes Adapter-specific calldata identifying the position
amount uint256 Repayment amount (pass type(uint256).max to repay full scaled debt)

Emits
Repay(address payer, address onBehalfOf, uint256 actualRepayAmount)

Reverts (selected)

  • InvalidAmount() — amount == 0
  • CannotRepayUnderLiquidationWithoutInsurance() — under liquidation and not insured
  • GracePeriodExpired() — liquidation grace window elapsed
  • ResidualCannotBeRepaid() — remaining scaled debt < MIN_BORROW_AMOUNT but still > 0
  • ERC20 allowance/balance failures on transfer
  • Adapter validation failures

Notes

  • To repay the entire outstanding scaled debt and avoid dust, pass type(uint256).max as amount.
  • After successful repayment, withdrawAsset can be called for an ERC-721 adapter to retrieve the NFT.
  • For ERC-20 adapters, you can continue repaying until your position approaches the borrow threshold; ensure collateralization remains sufficient.
  • If a small residual remains below MIN_BORROW_AMOUNT, the system reverts; repay in full to avoid this.
Example (TypeScript / ethers)
// Repay a fixed amount
await reserveAsset.approve(lendingPool.target, repayAmt);
await (await lendingPool.repay(adapter, data, repayAmt)).wait();

// Repay in full (no dust)
await reserveAsset.approve(lendingPool.target, maxExpected);
await (await lendingPool.repay(adapter, data, ethers.constants.MaxUint256)).wait();
Source code (public + internal)
function repay(address adapter, bytes calldata data,  uint256 amount)
    external
    nonReentrant
    whenNotPaused
    onlyValidAmount(amount)
    onlySupportedAdapter(adapter)
    notBlacklisted(msg.sender)
{
    _repay(adapter, data, amount, msg.sender);
}

function _repay(address adapter, bytes calldata data, uint256 amount, address onBehalfOf) internal {
    if (amount == 0) revert InvalidAmount();
    if (onBehalfOf == address(0)) revert AddressCannotBeZero();

    IAssetAdapter(adapter).validate(onBehalfOf, data);

    // Update reserve state before repayment
    ReserveLibrary.updateReserveState(reserve, rateData);

    CollateralPosition storage position = positions[onBehalfOf][IAssetAdapter(adapter).getPositionKey(data)];

    // If the user is under liquidation and position is not insured, they cannot repay.
    if (position.isUnderLiquidation && !position.isInsured) revert CannotRepayUnderLiquidationWithoutInsurance();

    // If the position is under liquidation and the grace period has expired, the user cannot repay.
    if (position.isUnderLiquidation && block.timestamp > position.liquidationStartTime + parameters.liquidationGracePeriod) revert GracePeriodExpired();

    // Calculate the position's debt including any accrued interest
    uint256 positionScaledDebt = getPositionScaledDebt(adapter, onBehalfOf, data);
    // If amount is greater than positionDebt, cap it at positionDebt
    uint256 actualRepayAmount = (amount == type(uint256).max) ? positionScaledDebt : amount;

    // Burn DebtTokens from the user whose debt is being repaid (onBehalfOf)
    (, , uint256 normalizedAmountBurned, uint256 balanceIncrease) =
        IDebtToken(reserve.reserveDebtTokenAddress).burn(
            onBehalfOf,
            actualRepayAmount,
            this.getNormalizedDebt(),
            abi.encode(adapter, data)
        );

    // Update the position index of the user
    position.positionIndex = reserve.usageIndex;

    // Transfer reserve assets from the caller (msg.sender) to the reserve
    IERC20(reserve.reserveAssetAddress).safeTransferFrom(msg.sender, reserve.reserveRTokenAddress, actualRepayAmount);

    // Update position's debt balance
    if (balanceIncrease > 0) {
        position.rawDebtBalance += balanceIncrease;
    }
    position.rawDebtBalance = position.rawDebtBalance - normalizedAmountBurned;

    // Update the insurance
    if (position.rawInsuredBalance > position.rawDebtBalance) {
        position.rawInsuredBalance = position.rawDebtBalance;
    }
    if (position.rawDebtBalance == 0) {
        position.rawInsuredBalance = 0;
        position.isInsured = false;
    }

    // Update liquidity and interest rates
    ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, actualRepayAmount, 0);

    // If a residual remains below MIN_BORROW_AMOUNT, revert
    if (position.rawDebtBalance > 0 && getPositionScaledDebt(adapter, onBehalfOf, data) < MIN_BORROW_AMOUNT) {
        revert ResidualCannotBeRepaid();
    }

    _rebalanceLiquidity();

    emit Repay(msg.sender, onBehalfOf, actualRepayAmount);
}

repayOnBehalf

repayOnBehalf(address adapter, bytes data, uint256 amount, address onBehalfOf)

Summary
Third-party repayment of a borrower’s debt for a specific collateral position. Forwards to the internal _repay flow after performing feature-flag and minimum-amount checks.

Minimum Repayment Requirement

When repaying on behalf of another user, at least 1% of the borrower's scaled debt must be repaid in a single transaction.
Attempting to repay less than 1% will revert with RepaymentBelowMinimumThreshold().
To repay the full debt (and avoid dust), pass type(uint256).max as the amount parameter.

Parameters

Name Type Description
adapter address Registered IAssetAdapter for the collateral position
data bytes Adapter-specific calldata identifying the position
amount uint256 Repayment amount (use type(uint256).max to repay full scaled debt; see Notes)
onBehalfOf address Borrower whose debt is being repaid

Emits
Repay(address payer, address onBehalfOf, uint256 actualRepayAmount) (emitted inside _repay)

Reverts (selected)

  • PaybackDebtDisabled() — feature flag disabled
  • AddressCannotBeZero() — onBehalfOf is zero
  • RepaymentBelowMinimumThreshold() — when msg.sender != onBehalfOf and amount < 1% of scaled debt
  • Plus all _repay errors (e.g., liquidation/grace-period rules, allowance/balance, residual below MIN_BORROW_AMOUNT)

Notes

  • To repay the entire outstanding scaled debt and avoid dust, pass type(uint256).max as amount.
  • When the payer is not the borrower, the repayment must be ≥ 1% of the borrower’s scaled debt (anti-griefing).
  • If a small residual remains below MIN_BORROW_AMOUNT, the call will revert; repay in full to avoid this.
  • Ensure the payer has approved and holds enough reserve asset for the transfer.
Example (TypeScript / ethers)
// Third-party repay >= 1% of scaled debt
const scaled = await lendingPool.getPositionScaledDebt(adapter, borrower, data);
const onePercent = scaled.mul(1).div(100);
await reserveAsset.approve(lendingPool.target, onePercent);
await (await lendingPool.repayOnBehalf(adapter, data, onePercent, borrower)).wait();

// Full payoff (no dust)
await reserveAsset.approve(lendingPool.target, maxExpected);
await (await lendingPool.repayOnBehalf(
  adapter, data, ethers.constants.MaxUint256, borrower
)).wait();
Source code
function repayOnBehalf(address adapter, bytes calldata data, uint256 amount, address onBehalfOf)
    external
    nonReentrant
    whenNotPaused
    onlyValidAmount(amount)
    onlySupportedAdapter(adapter)
    notBlacklisted(msg.sender)
    notBlacklisted(onBehalfOf)
{
    if (!parameters.canPaybackDebt) revert PaybackDebtDisabled();
    if (onBehalfOf == address(0)) revert AddressCannotBeZero();

    uint256 positionScaledDebt = getPositionScaledDebt(adapter, onBehalfOf, data);
    if (positionScaledDebt != 0 && msg.sender != onBehalfOf) {
        uint256 minRepaymentAmount = positionScaledDebt.percentMul(100); // 1%
        if (amount < minRepaymentAmount) revert RepaymentBelowMinimumThreshold();
    }

    _repay(adapter, data, amount, onBehalfOf);
}

Liquidation Management

initiateLiquidation

initiateLiquidation(address adapter, address user, bytes data)

Summary
Starts liquidation for a position whose health factor is below the liquidation threshold.
Validates the position, accrues indices, checks HF, then flags the position under liquidation and records the start time.

Note

The contract implementing this function is a proxy and resides in LiquidationProxy.sol.

Access control — the LendingPool entry point

initiateLiquidation is nonReentrant whenNotPaused onlySupportedAdapter(adapter) notBlacklisted(msg.sender), then delegatecalls LiquidationProxy. Anyone may call it. It is blocked while the pool is paused, and the adapter must still be registered.

Parameters

Name Type Description
adapter address Registered IAssetAdapter for the collateral
user address Position owner
data bytes Adapter-specific calldata identifying the position

Emits
- LiquidationInitiated(address indexed sender, address indexed user, address adapter, bytes data)

Reverts (selected)

  • UserAlreadyUnderLiquidation() — position already flagged
  • HealthFactorTooHigh() — health factor ≥ HEALTH_FACTOR_LIQUIDATION_THRESHOLD
  • Adapter validation failures

Notes

  • Health factor is computed via calculateHealthFactorProxy (delegates to LendingPool’s calculateHealthFactor).
  • Index accrual occurs before checks via ReserveLibrary.updateReserveState.
Example (TypeScript / ethers)
const adapterAddr = "0xAdapter...";
const borrower = "0xBorrower...";
const adapterData = ethers.AbiCoder.defaultAbiCoder().encode(
  ["uint256"],
  [tokenId] // e.g. for an ERC721 collateral adapter
);

const tx = await lendingPool.initiateLiquidation(adapterAddr, borrower, adapterData);
await tx.wait();
Source code
function initiateLiquidation(address adapter, address user, bytes calldata data) external onlyProxy {
    bytes32 positionKey = IAssetAdapter(adapter).getPositionKey(data);
    CollateralPosition storage position = positions[user][positionKey];
    if (position.isUnderLiquidation) revert UserAlreadyUnderLiquidation();

    IAssetAdapter(adapter).validate(user, data);

    // update state
    ReserveLibrary.updateReserveState(reserve, rateData);

    uint256 healthFactor = calculateHealthFactorProxy(adapter, user, data);

    if (healthFactor >= HEALTH_FACTOR_LIQUIDATION_THRESHOLD) revert HealthFactorTooHigh();

    position.isUnderLiquidation = true;
    position.liquidationStartTime = block.timestamp;

    emit LiquidationInitiated(msg.sender, user, adapter, data);
}

closeLiquidation

closeLiquidation(address adapter, bytes data)

Summary
Closes an active liquidation for the caller’s position.
Validates the position, enforces grace-period and insurance rules, accrues indices, confirms debt is fully repaid, then clears the liquidation flags and insurance state.

Note

The contract implementing this function is a proxy and resides in LiquidationProxy.sol.

Access control — the LendingPool entry point

closeLiquidation is nonReentrant whenNotPaused onlySupportedAdapter(adapter) notBlacklisted(msg.sender), then delegatecalls LiquidationProxy. The proxy resolves the position from msg.sender, so only the borrower can close their own liquidation. Because it is whenNotPaused, pausing the pool removes the borrower's ability to cure while the StabilityPool can still finalise — see Design Decisions.

Parameters

| Name    | Type      | Description                                        |
|---------|-----------|----------------------------------------------------|
| adapter | `address` | Registered `IAssetAdapter` for the collateral      |
| data    | `bytes`   | Adapter-specific calldata identifying the position |

**Emits**  
- `LiquidationClosed(address indexed user, address adapter, bytes data)`

**Reverts (selected)**

- `NotUnderLiquidation()` — position isn’t flagged under liquidation  
- `GracePeriodExpired()` — grace period elapsed and debt is still > 0  
- `NotInsured()` — attempting to close with nonzero debt and no insurance  
- `DebtNotZero()` — after accrual, outstanding debt is not fully repaid  
- Adapter validation failures

**Notes**

- If insured, the borrower must fully repay within `liquidationGracePeriod` to close.  
- The proxy permits closing after the grace period only when debt is already zero.  
- On success, liquidation flags are cleared and insurance is reset.

??? example "Example (TypeScript / ethers)"
    const adapterAddr = "0xAdapter...";
    const data = ethers.AbiCoder.defaultAbiCoder().encode(["uint256"], [tokenId]);

    const tx = await lendingPool.closeLiquidation(adapterAddr, data);
    await tx.wait();
    ```

??? quote "Source code"
    ```solidity
    function closeLiquidation(address adapter, bytes calldata data) external onlyProxy {
        address userAddress = msg.sender;
        bytes32 positionKey = IAssetAdapter(adapter).getPositionKey(data);
        CollateralPosition storage position = positions[userAddress][positionKey];

        if (!position.isUnderLiquidation) revert NotUnderLiquidation();

        IAssetAdapter(adapter).validate(msg.sender, data);

        uint256 positionDebt = getPositionScaledDebtProxy(adapter, userAddress, data);
        if (block.timestamp > position.liquidationStartTime + parameters.liquidationGracePeriod && positionDebt > 0) {
            revert GracePeriodExpired();
        }

        // Closing is only allowed with insurance if debt is nonzero
        if (!position.isInsured && positionDebt > 0) revert NotInsured();

        // Accrue indices; after accrual, debt must be zero
        ReserveLibrary.updateReserveState(reserve, rateData);
        if (positionDebt > 0) revert DebtNotZero();

        position.isUnderLiquidation = false;
        position.liquidationStartTime = 0;
        position.rawInsuredBalance = 0;
        position.isInsured = false;

        emit LiquidationClosed(userAddress, adapter, data);
    }
    ```

finalizeLiquidation

finalizeLiquidation(address adapter, address user, bytes data)

Summary
Completes the liquidation process by transferring collateral to the Stability Pool and burning the user’s debt.
Enforces that the grace period has expired (for insured positions) and ensures there is still outstanding debt.

Restricted Access

This method is only callable by the Stability Pool through the proxy when a position must be liquidated.
If the debt is already zero, the liquidation cannot be finalized here. In that case, the borrower (owner) should close the liquidation instead.

Note

The contract implementing this function is a proxy and resides in LiquidationProxy.sol.

Access control — the LendingPool entry point

finalizeLiquidation is nonReentrant onlyStabilityPool onlySupportedAdapter(adapter), then delegatecalls LiquidationProxy. Only the configured stabilityPool address may call it, and it is deliberately not whenNotPaused — clearing an unhealthy position must stay possible during a pause. On return the pool calls _rebalanceLiquidity().

Parameters

| Name    | Type      | Description                                        |
|---------|-----------|----------------------------------------------------|
| adapter | `address` | Registered `IAssetAdapter` for the collateral      |
| user    | `address` | The borrower whose position is being liquidated    |
| data    | `bytes`   | Adapter-specific calldata identifying the position |

**Emits**  
- `LiquidationFinalized(address stabilityPool, address user, address adapter, bytes data, uint256 debtCleared, uint256 collateralValue)`

**Reverts (selected)**

- `NotUnderLiquidation()` — position isn’t marked for liquidation  
- `GracePeriodNotExpired()` — insured position still within grace period  
- Adapter validation failures  
- Stability Pool checks debt > 0 before execution

**Notes**

- Debt must exist for liquidation to be finalized.  
- After execution, the user’s collateral is transferred to the Stability Pool and the debt is fully cleared.  
- Insurance flags and balances are reset to zero.

??? example "Example (TypeScript / ethers)"
    ```ts
    const adapterAddr = "0xAdapter...";
    const userAddr = "0xBorrower...";
    const data = ethers.AbiCoder.defaultAbiCoder().encode(["uint256"], [tokenId]);
    const tx = await lendingPool.finalizeLiquidation(adapterAddr, userAddr, data);
    await tx.wait();
    ```

??? quote "Source code"
    ```solidity
    function finalizeLiquidation(address adapter, address user, bytes calldata data) external onlyProxy {
        bytes32 positionKey = IAssetAdapter(adapter).getPositionKey(data);
        CollateralPosition storage position = positions[user][positionKey];
        if (!position.isUnderLiquidation) revert NotUnderLiquidation();

        IAssetAdapter(adapter).validate(user, data);

        // update state
        ReserveLibrary.updateReserveState(reserve, rateData);

        if (position.isInsured && block.timestamp <= position.liquidationStartTime + parameters.liquidationGracePeriod) {
            revert GracePeriodNotExpired();
        }

        uint256 positionDebt = getPositionScaledDebtProxy(adapter, user, data);

        position.isUnderLiquidation = false;
        position.liquidationStartTime = 0;
        position.isInsured = false;

        IAssetAdapter(adapter).transferAsset(user, data, stabilityPool);

        IDebtToken(reserve.reserveDebtTokenAddress).burn(
            user,
            positionDebt,
            reserve.usageIndex,
            abi.encode(adapter, data)
        );

        position.positionIndex = reserve.usageIndex;
        position.rawDebtBalance = 0;
        position.rawInsuredBalance = 0;

        ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, positionDebt, 0);

        emit LiquidationFinalized(
            stabilityPool,
            user,
            adapter,
            data,
            positionDebt,
            IAssetAdapter(adapter).getAssetValue(user, data)
        );
    }
    ```

Positions

calculateHealthFactor

calculateHealthFactor(address adapter, address user, bytes data) → uint256

Summary
Returns the position’s health factor (RAY). Uses current collateral value and scaled debt with the protocol’s liquidationThreshold.

Parameters

Name Type Description
adapter address Registered IAssetAdapter
user address Position owner
data bytes Adapter-specific identifier

Returns

uint256 — Health factor (RAY, 1e27). If scaled debt is negligible, returns WadRayMath.RAY.

Notes

  • HF ≥ 1e18 implies safe; HF < 1e18 implies liquidatable per thresholding logic (exact threshold governed by parameters).
  • Collateral value from IAssetAdapter.getAssetValue(user, data).
Example (TypeScript / ethers)
const hf = await lendingPool.calculateHealthFactor(adapter, user, data);
console.log("HF:", hf.toString());

getUserCollateralValue

getUserCollateralValue(address user) → uint256

Summary
Sums the user’s total collateral value across all registered adapters.

Parameters

Name Type Description
user address User address

Returns

uint256 — Total collateral value.

Notes

  • Iterates adapters[] and calls IAssetAdapter(adapter).getTotalAssetValue(user) per adapter.
Example (TypeScript / ethers)
const total = await lendingPool.getUserCollateralValue(user);
console.log("Total collateral value:", total.toString());

getPositionDebt

getPositionDebt(address adapter, address user, bytes data) → uint256

Summary
Returns raw debt for a position without interest accrual (stored balance).

Parameters

Name Type Description
adapter address Registered IAssetAdapter
user address Position owner
data bytes Position identifier

Returns

uint256 — Raw debt balance.

Notes

  • For interest-inclusive debt, use getPositionScaledDebt.
Example (TypeScript / ethers)
const raw = await lendingPool.getPositionDebt(adapter, user, data);
console.log("Raw debt:", raw.toString());

getPositionScaledDebt

getPositionScaledDebt(address adapter, address user, bytes data) → uint256

Summary
Returns scaled debt (includes interest via index) for a specific position.

Parameters

Name Type Description
adapter address Registered IAssetAdapter
user address Position owner
data bytes Position identifier

Returns

uint256 — Scaled debt (interest-aware).

Notes

  • Internally derives from _positionScaledDebt(position).
Example (TypeScript / ethers)
const scaled = await lendingPool.getPositionScaledDebt(adapter, user, data);
console.log("Scaled debt:", scaled.toString());

getPositionsScaledDebt

getPositionsScaledDebt(address user) → uint256

Summary
Aggregates the user’s total scaled debt across all positions on all adapters.

Parameters

Name Type Description
user address User address

Returns

uint256 — Total scaled debt.

Notes

  • Iterates adapters[], fetches getPositionKeys(user), sums _positionScaledDebt for each key.

Gas scales with deposited NFT count

ERC721AssetAdapter does not cap how many NFTs a user can deposit, so the gas cost of this call grows with that number. DebtToken.balanceOf uses this aggregate, and DebtToken.mint calls it on every borrow. At around 1,000 deposited NFTs, the aggregate debt read and new borrows exceed the per-transaction gas cap. Only that user is affected: repayment, liquidation and withdrawing debt-free NFTs work per position and keep working.

Example (TypeScript / ethers)
const totalScaled = await lendingPool.getPositionsScaledDebt(user);
console.log("Total scaled debt:", totalScaled.toString());

getPosition

getPosition(address adapter, address user, bytes data) → CollateralPosition

Summary
Returns the stored CollateralPosition struct for the specified position.

Parameters

Name Type Description
adapter address Registered IAssetAdapter
user address Position owner
data bytes Position identifier

Returns

CollateralPosition — Current stored fields for the position.

Notes

  • Keyed by positions[user][IAssetAdapter(adapter).getPositionKey(data)].
Example (TypeScript / ethers)
const pos = await lendingPool.getPosition(adapter, user, data);
console.log("Position:", pos);

getPositionData

getPositionData(address adapter, address user, bytes data) → (uint256 positionIndex, uint256 rawDebtBalance)

Summary
Returns a compact view of two key fields of a position.

Parameters

Name Type Description
adapter address Registered IAssetAdapter
user address Position owner
data bytes Position identifier

Returns

  • uint256 positionIndex
  • uint256 rawDebtBalance

Notes

  • For a fuller struct, use getPosition or getPositionView.
Example (TypeScript / ethers)
const [positionIndex, rawDebt] = await lendingPool.getPositionData(adapter, user, data);
console.log({ positionIndex: positionIndex.toString(), rawDebt: rawDebt.toString() });

getPositionView

getPositionView(address adapter, address user, bytes data) → CollateralPositionView

Summary
Returns a synthesized view struct for a position, including flags, balances, indices, scaled debt, health factor, adapter and asset addresses.

Parameters

Name Type Description
adapter address Registered IAssetAdapter
user address Position owner
data bytes Position identifier

Returns
CollateralPositionView with:

  • isUnderLiquidation, liquidationStartTime, rawDebtBalance, isInsured
  • scaledDebtBalance (via getPositionScaledDebt)
  • healthFactor (via calculateHealthFactor)
  • positionIndex, adapter, asset

Notes

  • Useful for UIs needing a single call to render the full state.
Example (TypeScript / ethers)
const view = await lendingPool.getPositionView(adapter, user, data);
console.log("Position view:", view);

Indexes

getNormalizedIncome

getNormalizedIncome() → uint256

Summary
Returns the reserve’s normalized income (liquidity index).

Returns

uint256 — Liquidity index.

Notes

  • Read-only wrapper over ReserveLibrary.getLiquidityIndex(reserve, rateData).
Example (TypeScript / ethers)
const income = await lendingPool.getNormalizedIncome();
console.log("Liquidity index:", income.toString());

getNormalizedDebt

getNormalizedDebt() → uint256

Summary
Returns the reserve’s normalized debt (usage index).

Returns

uint256 — Usage index.

Notes

  • Read-only wrapper over ReserveLibrary.getUsageIndex(reserve, rateData).
Example (TypeScript / ethers)
const usage = await lendingPool.getNormalizedDebt();
console.log("Usage index:", usage.toString());

getReserveState

getReserveState() → ReserveData

Summary
Returns the reserve’s data that includes the indexes and usages.

Returns
ReserveData with:

  • reserveRTokenAddress, reserveAssetAddress, reserveDebtTokenAddress
  • totalLiquidity, totalUsage,
  • liquidityIndex, usageIndex
  • lastUpdateTimestamp, pendingProtocolFeeAmount

Important: Correct Source for Total Usage (Total Debt)

To have an up-to-date value for total usage (i.e., the total debt in the system including accrued interest), do not use reserve.totalUsage.
Instead, use DebtToken.totalSupply.
The reserve.totalUsage field only reflects the total supply of the debt token in circulation at the last update and does not include accrued interest since then.

Example (TypeScript / ethers)
const state = await lendingPool.getReserveState();

Protocol Administration

setParameter

setParameter(OwnerParameter param, uint256 newValue)

Summary
The single entry point for all 17 mutable parameters. Which role may call it depends on which bucket the parameter falls in — a parameter in neither bucket reverts.

Guarded Method
PROTOCOL_ROLE for the 13 risk parameters, GOVERNANCE_ROLE for the 4 fee parameters.

Parameters

Name Type Description
param OwnerParameter Enum selecting the parameter to write
newValue uint256 New value, in the parameter's own unit

Behaviour

  1. ReserveLibrary.updateReserveState accrues interest first, so the change never applies retroactively.
  2. The parameter is classified as protocol or governance; the matching role is checked.
  3. The parameter's own validation runs (see the table below).
  4. updateInterestRatesAndLiquidity(reserve, rateData, 0, 0) recomputes rates, so the value takes effect immediately.
  5. ParameterChanged is emitted.

Per-parameter validation

Parameter Bucket Reverts on
LendingPoolBorrowCap Protocol CapZero() if 0; CapExceedSupply() if > supplyCap
LendingPoolSupplyCap Protocol CapZero() if 0; SupplyCapBelowBorrowCap() if < borrowCap
BorrowThreshold Protocol BorrowThresholdTooHigh() if >= liquidationThreshold
LiquidationThreshold Protocol LiquidationThresholdTooLow() if <= borrowThreshold
LiquidationGracePeriod Protocol InvalidGracePeriod() outside [1 days, 7 days]
MinDepositAmount Protocol InvalidAmount() if 0
WithdrawalStatus, CanPaybackDebt Protocol InvalidBoolean() if > 1
LiquidityBufferRatio, MaxRate, OptimalUtilizationRate, PrimeRateChangeBPS, MinVaultRebalanceAmount Protocol no validation
InsuranceFee, ProtocolFeeRate, LendingPoolDepositFee, VaultOpeningFee Governance no validation

Emits

  • ParameterChanged(OwnerParameter parameter, uint256 oldValue, uint256 newValue)

Reverts

  • AccessDenied() — caller lacks the role that owns this parameter
  • InvalidParameter() — the enum value is in neither bucket

oldValue is reported as 0 for the two boolean parameters

WithdrawalStatus and CanPaybackDebt write straight to their boolean field and never populate the local oldValue, so ParameterChanged always carries oldValue = 0 for those two. Read the previous state from parameters() rather than from the event.

Fee and rate parameters are unbounded in the contract

Nothing stops a basis-point fee being set above 100_00, or LiquidityBufferRatio above 100%. The bound is a trust assumption on the role holders, not a contract-enforced invariant. See Design Decisions for why the risk bucket has no timelock.

TS
// OwnerParameter.LiquidationThreshold
const tx = await lendingPool.setParameter(3, 8000); // 80.00%
await tx.wait();

updateState

updateState()

Summary
Accrues the liquidity and usage indices and recomputes rates without any user action. Permissionless — anyone may poke the pool to bring its indices current.

Behaviour

  1. ReserveLibrary.updateReserveState(reserve, rateData)
  2. ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, 0)

Reverts

  • EnforcedPause() — the function is whenNotPaused

The StabilityPool calls this before every deposit

StabilityPool.deposit calls updateState() first so that the index used to scale the deposit is current.

Source code
function updateState() external whenNotPaused{
    ReserveLibrary.updateReserveState(reserve, rateData);
    ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, 0);
}

registerAdapter / unregisterAdapter

registerAdapter(address adapter) · unregisterAdapter(address adapter)

Summary
Add or remove a collateral adapter from the supported set. Every collateral-facing entry point is gated by onlySupportedAdapter.

Guarded Method
PROTOCOL_ROLE only.

Emits

  • AdapterRegistered(address adapter)
  • AdapterRemoved(address adapter)

Reverts

  • AdapterAlreadySupported() — registering an adapter already in the set
  • AdapterNotSupported() — unregistering one that is not

Unregistering does not check for outstanding positions

unregisterAdapter performs no position check. Removing an adapter that still has live positions freezes them — debt keeps accruing through the usage index while the borrower cannot repay, withdraw, or be liquidated. Positions must be unwound, or the adapter re-registered, before removal is finalised. See Design Decisions.

Source code
function registerAdapter(address adapter) external onlyRole(PROTOCOL_ROLE) {
    if (supportedAdapters[adapter]) revert AdapterAlreadySupported();
    supportedAdapters[adapter] = true;
    adapters.push(adapter);
    emit AdapterRegistered(adapter);
}

function unregisterAdapter(address adapter) external onlyRole(PROTOCOL_ROLE) {
    if (!supportedAdapters[adapter]) revert AdapterNotSupported();
    supportedAdapters[adapter] = false;
    // Remove from adapters array
    for (uint256 i = 0; i < adapters.length; i++) {
        if (adapters[i] == adapter) {
            adapters[i] = adapters[adapters.length - 1];
            adapters.pop();
            break;
        }
    }
    emit AdapterRemoved(adapter);
}

setPrimeRate

setPrimeRate(uint256 newPrimeRate)

Summary
Moves the prime rate. This is not a setParameter value — it has its own entry point and its own caller.

Guarded Method
onlyPrimeRateOracle — callable only by the address held in primeRateOracle, which is the CRE receiver RAACPrimeRateReceiver.

Parameters

Name Type Description
newPrimeRate uint256 New prime rate, in RAY

Behaviour

Delegates to ReserveLibrary.setPrimeRate, which:

  1. accrues interest at the old rate before the change lands;
  2. requires newPrimeRate > 0 and <= 1 RAY (100%);
  3. rejects a move larger than PrimeRateChangeBPS of one RAY in either direction;
  4. re-derives baseRate (25% of prime) and optimalRate (50% of prime).

Emits

  • PrimeRateUpdated(uint256 oldPrimeRate, uint256 newPrimeRate)

Reverts

  • Unauthorized() — caller is not primeRateOracle
  • PrimeRateMustBePositive() — newPrimeRate is 0
  • PrimeRateChangeExceedsLimit() — newPrimeRate exceeds 1 RAY, or the move exceeds PrimeRateChangeBPS of one RAY in either direction

maxRate is not re-derived on-chain

maxRate is a separate PROTOCOL_ROLE parameter and is updated by a second, distinct transaction (setParameter(MaxRate, …)). Between the two the rate curve is internally inconsistent above OptimalUtilizationRate. The ordering is an operational guarantee, not a contract-enforced one.


claimProtocolFees

claimProtocolFees()

Summary
Realises the accrued protocol fee to the FeeCollector. The fee is held scaled in reserve.scaledPendingProtocolFee and accrues with the liquidity index until claimed.

Guarded Method
PROTOCOL_ROLE only.

Behaviour

  1. Accrues interest.
  2. _ensureLiquidity() for the full unscaled pending amount — pulling from the vault if the buffer is short.
  3. LendingPoolFeeLibrary.claimProtocolFees transfers to feeCollector and zeroes the pending fee.
  4. Rates and liquidity are updated with the claimed amount, then liquidity is rebalanced.

Unclaimed fees sit inside totalLiquidity

Because the pending fee counts as liquidity, utilisation is marginally understated while fees are unclaimed. See Design Decisions.

Fees are paid before borrower interest is collected

The pending fee accrues from the debt index, not from interest borrowers have actually paid. A claim is funded from the buffer or the vault, which is depositor cash, so the protocol's share is realised ahead of the receivable. If that debt later defaults, suppliers absorb the shortfall, including the fee already paid out.

Source code
function claimProtocolFees() external onlyRole(PROTOCOL_ROLE) {
    ReserveLibrary.updateReserveState(reserve, rateData);
    uint256 liquidityIndex = reserve.liquidityIndex;
    _ensureLiquidity(reserve.scaledPendingProtocolFee.rayMul(liquidityIndex));
    (, uint256 claimedAmount) = LendingPoolFeeLibrary.claimProtocolFees(reserve, feeCollector, liquidityIndex);
    ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, claimedAmount);
    _rebalanceLiquidity();
}

claimCollectorRewards

claimCollectorRewards() → (address tokenAddress, uint256 claimableAmount)

Summary
Transfers harvested vault yield out of the pool to the FeeCollector.

Guarded Method
onlyFeeCollector — callable only by the address held in feeCollector.

Returns

  • tokenAddress — always reserve.reserveAssetAddress (pmUSD)
  • claimableAmount — the amount transferred

Emits

  • VaultRewardsClaimed(uint256 amount)

Reverts

  • Unauthorized() — caller is not feeCollector

This is the one balance the pool holds at rest

_harvestYield() books proceeds with vaultRewards.unclaimedRewards += yieldHarvested without forwarding them. The pmUSD sits in the pool until this function moves it out.


setVault

setVault(address newVault)

Summary
Repoints the yield venue for idle liquidity. Migrating fully exits the old vault before switching.

Guarded Method
PROTOCOL_ROLE only.

Behaviour

  1. If a different, non-zero vault is already set, VaultProxy.withdrawAll() is delegate-called — redeeming every share and harvesting residual yield.
  2. vault is overwritten. address(0) is permitted, which detaches the pool from any vault.
  3. If the new vault is non-zero, vault.asset() must equal the reserve asset.

Emits

  • VaultUpdated(address indexed oldVault, address indexed newVault)

Reverts

  • InvalidVaultAsset() — the new vault's asset() is not the reserve asset

Only asset() is validated

LendingPool types the venue as IBaseVault and checks nothing else. The design assumes a vault that runs no strategy, so share price is monotonic and there is no loss path. Pointing the pool at a vault that can realise losses is outside the model. See Design Decisions.

Source code
function setVault(address newVault) external onlyRole(PROTOCOL_ROLE) {
    // If we change the vault, we need to claim all rewards from the old vault
    if (address(vault) != address(newVault) && address(vault) != address(0)) {
        (bool success, bytes memory returndata) = vaultProxy.delegatecall(
            abi.encodeWithSignature("withdrawAll()")
        );
        _handleProxyError(success, returndata);
    }
    // Allow address(0) to be set to remove the vault
    address oldVault = address(vault);
    vault = IBaseVault(newVault);

    // Verify vault asset matches our reserve asset
    if (newVault != address(0) && vault.asset() != reserve.reserveAssetAddress) revert InvalidVaultAsset();

    emit VaultUpdated(oldVault, newVault);
}

rescueToken

rescueToken(address tokenAddress, address recipient, uint256 amount)

Summary
Recovers an ERC-20 sent to the pool by mistake.

Guarded Method
PROTOCOL_ROLE only.

Parameters

Name Type Description
tokenAddress address Token to rescue — must not be the vault share
recipient address Destination
amount uint256 Amount to transfer

Reverts

  • "Cannot rescue vault token" — a string require, not a custom error, when tokenAddress == address(vault)

The vault share is excluded, but the reserve asset is not

Because harvested yield rests in the pool, the pool's pmUSD balance between a harvest and a claim equals vaultRewards.unclaimedRewards. rescueToken can move that balance and does not decrement the counter, after which claimCollectorRewards() would attempt to transfer pmUSD the pool no longer holds.

This is the residual half of a finding whose vault-share half was fixed (v4 · 2026-02, L-04). Treat rescueToken as a PROTOCOL_ROLE-trusted recovery path that must not be pointed at the reserve asset while vault rewards are unclaimed.


Configuration

Single-assignment setters. All are role-gated, emit an event, and take effect immediately. Pause and unpause sit behind EMERGENCY_ROLE; everything else behind PROTOCOL_ROLE.

Function Access Effect Reverts
pause() EMERGENCY_ROLE Halts every whenNotPaused entry point — collateral, borrow, repay, liquidation, updateState. deposit checks the pause inline and exempts the StabilityPool. —
unpause() EMERGENCY_ROLE Lifts the pause —
setPrimeRateOracle(address newOracle) PROTOCOL_ROLE Changes the only address allowed to call setPrimeRate. Zero is not rejected. Emits PrimeRateOracleUpdated(address indexed newOracle). —
setStabilityPool(address newStabilityPool) PROTOCOL_ROLE Changes the privileged liquidation counterparty — the only caller of finalizeLiquidation, and the address exempted from deposit fees, caps and the same-block guard. Emits StabilityPoolUpdated(address indexed oldStabilityPool, address indexed newStabilityPool). AddressCannotBeZero()
setLiquidationProxy(address _liquidationProxy) PROTOCOL_ROLE Swaps the delegatecall target for the liquidation module. Emits LiquidationProxyUpdated(address indexed oldLiquidationProxy, address indexed newLiquidationProxy). AddressCannotBeZero()
setFeeCollector(address newFeeCollector) PROTOCOL_ROLE Changes the destination for deposit, insurance and protocol fees. Emits FeeCollectorUpdated(address indexed oldFeeCollector, address indexed newFeeCollector). AddressCannotBeZero()

setPrimeRateOracle accepts the zero address

Unlike the three setters beside it, setPrimeRateOracle has no zero-address check. Setting it to zero makes setPrimeRate uncallable and freezes the prime rate at its current value until the oracle is set again.


Additional Views

Read-only helpers not covered above. None has state effect.

Function Returns Description
getAdapters() address[] Every registered collateral adapter
supportedAdapter(address adapter) bool Whether adapter is in the supported set
getUserDeposits(address user) uint256 The user's raw (scaled) RToken balance
getUserDepositsInStabilityPool(address user) uint256 The user's deposit in the StabilityPool, read by staticcall. Returns 0 if no StabilityPool is set
getEligibleUserDeposits(address user) uint256 getUserDeposits + getUserDepositsInStabilityPool
isUserDepositInSameBlock(address user) bool Whether depositBlock[user] == block.number — the same-block guard
getAssetValue(address adapter, address user, bytes data) uint256 Oracle value of a position, straight from the adapter
getPendingProtocolFee() uint256 Unscaled protocol fee awaiting claimProtocolFees()
getPrimeRate() uint256 Current prime rate, in RAY
getReserveRateData() ReserveRateData primeRate, baseRate, optimalRate, maxRate, optimalUtilizationRate, protocolFeeRate, and the three current rates

getAssetValue is deliberately unguarded

getAssetValue takes an arbitrary adapter without onlySupportedAdapter, and the ERC-721 adapter's own getters do not check that the tokenId is actually deposited. Every path that acts on a value validates the adapter first; the looser view exists so integrators can price a position that is not yet open.

Integrators must not treat a non-zero return as proof that a position exists. See Design Decisions.

getUserDepositsInStabilityPool does not check the call succeeded

The staticcall return value is discarded and abi.decode is applied to whatever comes back. It is guarded only by the address(0) early return, so a stabilityPool set to a non-contract address would revert on decode.