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()— ifamount == 0EnforcedPause()— if the pool is paused (the StabilityPool is exempt)MinDepositAmountRequired()— if the amount net of the deposit fee is belowMinDepositAmountSupplyCapReached()— ifLendingPoolSupplyCapwould be exceeded
Requirements
- Deposit fee (
LendingPoolDepositFee) goes toFeeCollectorbefore 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.
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()— ifamount == 0.EnforcedPause()— if the pool is paused (non-StabilityPool callers).WithdrawalsArePaused()— ifWithdrawalStatusis 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
amountRToken. - Pool liquidity must be sufficient after rebalancing.
- Reserve state updated via
ReserveLibrary.updateReserveStatebefore execution.
Example (TypeScript / ethers)
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
nonReentrantguard 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 toFeeCollector.
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()— ifopenVaultPosition()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)
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)
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 = falseposition.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)
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)
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
uninsuredBalance= fromgetUninsuredBalanceinsuranceFeeRate=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.
uninsuredBalanceuses 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)
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 == 0CannotRepayUnderLiquidationWithoutInsurance()— under liquidation and not insuredGracePeriodExpired()— liquidation grace window elapsedResidualCannotBeRepaid()— remaining scaled debt <MIN_BORROW_AMOUNTbut 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).maxasamount. - After successful repayment,
withdrawAssetcan 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 disabledAddressCannotBeZero()—onBehalfOfis zeroRepaymentBelowMinimumThreshold()— whenmsg.sender != onBehalfOfandamount < 1%of scaled debt- Plus all
_repayerrors (e.g., liquidation/grace-period rules, allowance/balance, residual belowMIN_BORROW_AMOUNT)
Notes
- To repay the entire outstanding scaled debt and avoid dust, pass
type(uint256).maxasamount. - 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 flaggedHealthFactorTooHigh()— health factor ≥HEALTH_FACTOR_LIQUIDATION_THRESHOLD- Adapter validation failures
Notes
- Health factor is computed via
calculateHealthFactorProxy(delegates to LendingPool’scalculateHealthFactor). - 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 ≥
1e18implies safe; HF <1e18implies liquidatable per thresholding logic (exact threshold governed by parameters). - Collateral value from
IAssetAdapter.getAssetValue(user, data).
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 callsIAssetAdapter(adapter).getTotalAssetValue(user)per adapter.
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.
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).
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[], fetchesgetPositionKeys(user), sums_positionScaledDebtfor 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.
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)].
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 positionIndexuint256 rawDebtBalance
Notes
- For a fuller struct, use
getPositionorgetPositionView.
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,isInsuredscaledDebtBalance(viagetPositionScaledDebt)healthFactor(viacalculateHealthFactor)positionIndex,adapter,asset
Notes
- Useful for UIs needing a single call to render the full state.
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).
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).
getReserveState¶
getReserveState() → ReserveData
Summary
Returns the reserve’s data that includes the indexes and usages.
Returns
ReserveData with:
reserveRTokenAddress,reserveAssetAddress,reserveDebtTokenAddresstotalLiquidity,totalUsage,liquidityIndex,usageIndexlastUpdateTimestamp,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.
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
ReserveLibrary.updateReserveStateaccrues interest first, so the change never applies retroactively.- The parameter is classified as protocol or governance; the matching role is checked.
- The parameter's own validation runs (see the table below).
updateInterestRatesAndLiquidity(reserve, rateData, 0, 0)recomputes rates, so the value takes effect immediately.ParameterChangedis 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 parameterInvalidParameter()— 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.
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
ReserveLibrary.updateReserveState(reserve, rateData)ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, 0)
Reverts
EnforcedPause()— the function iswhenNotPaused
The StabilityPool calls this before every deposit
StabilityPool.deposit calls updateState() first so that the index used to scale the deposit is current.
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 setAdapterNotSupported()— 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:
- accrues interest at the old rate before the change lands;
- requires
newPrimeRate > 0and<= 1 RAY(100%); - rejects a move larger than
PrimeRateChangeBPSof one RAY in either direction; - re-derives
baseRate(25% of prime) andoptimalRate(50% of prime).
Emits
PrimeRateUpdated(uint256 oldPrimeRate, uint256 newPrimeRate)
Reverts
Unauthorized()— caller is notprimeRateOraclePrimeRateMustBePositive()—newPrimeRateis0PrimeRateChangeExceedsLimit()—newPrimeRateexceeds1 RAY, or the move exceedsPrimeRateChangeBPSof 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
- Accrues interest.
_ensureLiquidity()for the full unscaled pending amount — pulling from the vault if the buffer is short.LendingPoolFeeLibrary.claimProtocolFeestransfers tofeeCollectorand zeroes the pending fee.- 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— alwaysreserve.reserveAssetAddress(pmUSD)claimableAmount— the amount transferred
Emits
VaultRewardsClaimed(uint256 amount)
Reverts
Unauthorized()— caller is notfeeCollector
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
- If a different, non-zero vault is already set,
VaultProxy.withdrawAll()is delegate-called — redeeming every share and harvesting residual yield. vaultis overwritten.address(0)is permitted, which detaches the pool from any vault.- 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'sasset()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 stringrequire, not a custom error, whentokenAddress == 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.