Market Creation

Complete guide to creating Lotus markets with MarketParams construction, IRM and liquidation parameter encoding, and calling createMarket with examples.

Create a Lotus market end-to-end: construct MarketParams, encode IRM and liquidation module parameters, and call createMarket.

circle-info

Prerequisites: The IRM, each LLTV value, and the liquidation module must be enabled via the admin enablelists (see Reference → Admin). You need collateral token addresses, oracle addresses, and LLTV values for each tranche.

Market creation is permissionless. Anyone can call createMarket as long as all components are on the enablelists.

1

Construct MarketParams

Build the MarketParams struct. All arrays must be the same length, which defines the number of tranches. Tranches are ordered by index: tranche 0 is the most senior (lowest risk), and the last tranche is the most junior (highest risk).

MarketParams memory params = MarketParams({
    loanToken: 0xA0b8...USDC,                         // ERC-20 that lenders supply
    irm: 0x1234...AdaptiveLinearKinkIrm,               // Must be enabled
    liquidationModule: 0x5678...OracleIncentiveModule,  // Must be enabled
    collateralTokens: new address[](3),
    oracles: new address[](3),
    lltvs: new uint128[](3)
});

// Tranche 0 (senior): WETH collateral, 80% LLTV
params.collateralTokens[0] = 0xC02a...WETH;
params.oracles[0]          = 0xABCD...ETH_USDC_Oracle;
params.lltvs[0]            = 0.80e18;

// Tranche 1 (mid): wstETH collateral, 85% LLTV
params.collateralTokens[1] = 0x7f39...wstETH;
params.oracles[1]          = 0xBCDE...wstETH_USDC_Oracle;
params.lltvs[1]            = 0.85e18;

// Tranche 2 (junior): cbBTC collateral, 90% LLTV
params.collateralTokens[2] = 0x2260...cbBTC;
params.oracles[2]          = 0xCDEF...cbBTC_USDC_Oracle;
params.lltvs[2]            = 0.90e18;

Each field explained:

Field
Description

loanToken

The ERC-20 that lenders supply and borrowers receive

irm

The IRM contract (must be on the enablelist)

liquidationModule

The liquidation module contract (must be on the enablelist)

collateralTokens[]

One per tranche: the ERC-20 borrowers post as collateral

oracles[]

One per tranche: must implement IOracle.price()

lltvs[]

One per tranche: the liquidation loan-to-value threshold (WAD); each must be on the enablelist

Validation rules enforced by createMarket:

  • All three arrays must have the same length (at least 1).

  • Each LLTV must be enabled via enableLltv.

  • The IRM must be enabled via enableIrm.

  • The liquidation module must be enabled via enableLiquidationModule.

  • The market must not already exist (duplicate MarketParams hashes are rejected).

2

Encode IRM Parameters

The irmParams bytes are forwarded to the IRM's initialize function. The encoding depends on which IRM you use.

AdaptiveLinearKinkIrm

The adaptive IRM automatically adjusts rates toward a target utilization. It decodes irmParams as abi.encode(address operator, AdaptiveConfig[] configs).

import {IAdaptiveLinearKinkIrm} from "lotus/irms/interfaces/IAdaptiveLinearKinkIrm.sol";

AdaptiveConfig[] memory configs = new AdaptiveConfig[](3);

configs[0] = AdaptiveConfig({
    targetUtilization: 0.90e18,     // 90% target
    initialRateAtTarget: 634195839, // ~2% APR in WAD per second
    minRateAtTarget: 317097919,     // ~1% APR floor
    maxRateAtTarget: 3170979198,    // ~10% APR ceiling
    adjustmentSpeed: 0.5e18,        // Adaptation speed
    curveParameter: 4e18,           // Curve steepness (1e18–10e18)
    gracePeriod: 7 days,            // No adaptation during grace period
    minUpdateInterval: 1 hours,     // Minimum time between updates
    maxRateChange: 0.10e18          // Max 10% change per update
});
// ... repeat for configs[1], configs[2]

bytes memory irmParams = abi.encode(operatorAddress, configs);

The helper function getEncodedAdaptiveIrmParams(operator, configs) on the IRM contract produces the same encoding.

Validation rules per config:

  • targetUtilization must be in [0.01e18, 0.99e18]

  • initialRateAtTarget must be in [minRateAtTarget, maxRateAtTarget]

  • maxRateAtTarget must be ≤ 56,816,320,054 (MAX_BORROW_RATE_PER_SECOND, ~500% APY)

  • curveParameter must be in [1e18, 10e18]

  • adjustmentSpeed must be ≤ 1e18

  • gracePeriod must be ≤ 7 days

  • minUpdateInterval must be ≤ 24 hours

  • maxRateChange must be in [0.01e18, 0.5e18]

FixedRateIrm

The fixed IRM sets a constant borrow rate per tranche. It decodes irmParams as abi.encode(address operator, uint256[] fixedRates).

uint256[] memory fixedRates = new uint256[](3);
fixedRates[0] = 634195839;   // ~2% APR in WAD per second
fixedRates[1] = 1268391679;  // ~4% APR
fixedRates[2] = 1902587519;  // ~6% APR

bytes memory irmParams = abi.encode(operatorAddress, fixedRates);

The helper function getEncodedIrmParams(operator, fixedRates) on the IRM contract produces the same encoding. Each rate must be ≤ MAX_BORROW_RATE_PER_SECOND (56,816,320,054).

ManagedLinearKinkIrm

The managed IRM uses a piecewise-linear curve with a configurable kink. It decodes irmParams as abi.encode(address operator, TrancheRateConfig[] rateConfig).

import {IBaseLinearKinkIrm} from "lotus/irms/interfaces/IBaseLinearKinkIrm.sol";

TrancheRateConfig[] memory configs = new TrancheRateConfig[](3);

configs[0] = TrancheRateConfig({
    targetUtilization: 0.90e18,   // Kink at 90%
    baseBorrowRate: 317097919,    // ~1% APR at 0% utilization
    gentleSlope: 352331021,       // Gradual increase below kink
    targetBorrowRate: 634195839,  // ~2% APR at kink
    steepSlope: 3523310216        // Steep increase above kink
});
// ... repeat for configs[1], configs[2]

bytes memory irmParams = abi.encode(operatorAddress, configs);

The helper function getEncodedIrmParams(operator, rateConfig) on the IRM contract produces the same encoding.

Validation rules per config:

  • baseBorrowRate must be ≤ targetBorrowRate

  • gentleSlope must be ≤ steepSlope

  • targetBorrowRate must be ≤ MAX_BORROW_RATE_PER_SECOND

  • steepSlope must be ≤ MAX_SLOPE_PER_SECOND (56,816,320,054)

Rate formula: Below the kink, rate = baseBorrowRate + gentleSlope.mulWad(utilization). Above the kink, rate = targetBorrowRate + steepSlope.mulWad(utilization - targetUtilization). Here mulWad(x) means * x / 1e18.

circle-exclamation

Converting APR to WAD per Second

All IRM rates are in WAD per second. To convert from an annual percentage rate:

ratePerSecond = APR_WAD / SECONDS_PER_YEAR

Where SECONDS_PER_YEAR = 31,536,000. For example, 2% APR = 0.02e18 / 31_536_000 ≈ 634,195,839.

3

Encode Liquidation Parameters

The liquidationParams bytes are forwarded to the liquidation module's initializeMarket function.

OracleIncentiveLiquidationModule

This module prices liquidations using the tranche oracle and applies a configurable incentive factor. It decodes liquidationParams as abi.encode(address operator, uint256[] factors).

uint256[] memory factors = new uint256[](3);
factors[0] = 1.05e18;  // 5% incentive for tranche 0 (senior)
factors[1] = 1.08e18;  // 8% incentive for tranche 1
factors[2] = 1.10e18;  // 10% incentive for tranche 2 (junior)

bytes memory liquidationParams = abi.encode(operatorAddress, factors);

The helper function getEncodedLiquidationParams(operator, factors) on the module contract produces the same encoding.

Each factor must be in [MIN_LIQUIDATION_INCENTIVE, MAX_LIQUIDATION_INCENTIVE] = [1e18, 1.15e18]. A factor of 1.05e18 means the liquidator receives 5% more collateral value than the debt they repay.

4

Call createMarket

lotus.createMarket(params, irmParams, liquidationParams);

The function:

  1. Validates all enablelist requirements and array lengths.

  2. Creates empty tranches for each entry.

  3. Calls IIrm(irm).initialize(marketId, irmParams) to configure the IRM.

  4. Calls ILiquidationModule(liquidationModule).initializeMarket(marketId, liquidationParams) to configure the liquidation module.

5

Verify

Compute the market ID and verify the market was created:

import {MarketParamsLib} from "lotus/libraries/MarketParamsLib.sol";

Id marketId = MarketParamsLib.id(params);
MarketParams memory stored = lotus.getMarketParams(marketId);
assert(stored.loanToken == params.loanToken);

uint256 numTranches = lotus.getNumMarketTranches(marketId);
assert(numTranches == 3);

TypeScript Example

import { ethers } from "ethers";

const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const lotus = new ethers.Contract(LOTUS_ADDRESS, lotusAbi, signer);

// 1. Build MarketParams
const marketParams = {
  loanToken: USDC_ADDRESS,
  irm: ADAPTIVE_IRM_ADDRESS,
  liquidationModule: ORACLE_INCENTIVE_MODULE_ADDRESS,
  collateralTokens: [WETH_ADDRESS, WSTETH_ADDRESS, CBBTC_ADDRESS],
  oracles: [ETH_ORACLE, WSTETH_ORACLE, CBBTC_ORACLE],
  lltvs: [
    ethers.parseUnits("0.80", 18),
    ethers.parseUnits("0.85", 18),
    ethers.parseUnits("0.90", 18),
  ],
};

// 2. Encode IRM params (FixedRateIrm example)
const fixedRates = [634195839n, 1268391679n, 1902587519n];
const irmParams = ethers.AbiCoder.defaultAbiCoder().encode(
  ["address", "uint256[]"],
  [OPERATOR_ADDRESS, fixedRates]
);

// 3. Encode liquidation params
const factors = [
  ethers.parseUnits("1.05", 18),
  ethers.parseUnits("1.08", 18),
  ethers.parseUnits("1.10", 18),
];
const liquidationParams = ethers.AbiCoder.defaultAbiCoder().encode(
  ["address", "uint256[]"],
  [OPERATOR_ADDRESS, factors]
);

// 4. Create market
const tx = await lotus.createMarket(marketParams, irmParams, liquidationParams);
await tx.wait();

See Also

Last updated