> For the complete documentation index, see [llms.txt](https://docs.covenant.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.covenant.finance/resources/integration-guide.md).

# Integration guide

This page is the reference for building on Covenant: protocol integrations (vaults, aggregators, strategies, bots, analytics) and frontends (your own UI over Covenant markets). It is written to be self-contained so that a developer, or an AI agent working for one, can integrate from this page plus the verified on-chain source.

Sources of truth, in order:

1. The deployed contracts. Source code is verified on the Monad explorer at the addresses in [Deployments](/deployments/monad.md).
2. This page (interfaces, units, conventions, gates, error selectors).
3. The mechanism pages: [How tranching works](/protocol-mechanism/how-does-covenant-work.md), [The Latent Swap AMM](/protocol-mechanism/latent-swap-amm.md), [Perpetual Debt](/protocol-mechanism/perpetual-debt.md).
4. The [Audits](/resources/audits.md).

The whole documentation set is also available as plain text for LLM consumption at `https://docs.covenant.finance/llms.txt` (index) and `https://docs.covenant.finance/llms-full.txt` (full content).

There is no SDK and no npm package. Integrations talk to the contracts directly with any EVM client (viem, ethers, web3.py, Foundry `cast`).

**Disclaimer.** The Covenant protocol is provided "as is" and "as available", without warranties of any kind. This page is a technical reference only: it describes the deployed contracts to the best of the team's knowledge at the time of writing, but the contracts are the sole authority on their own behaviour and may change through governance or new deployments. Nothing here is an offer, a recommendation, or an assurance of safety, liquidity, or performance. Anyone integrating does so at their own risk and is responsible for their own testing, security review, and compliance. See the [Terms of Service](/resources/terms-of-service.md) and [Risks](/resources/risks.md).

## 1. At a glance

```json
{
  "network": { "name": "Monad", "chainId": 143, "nativeCurrency": "MON",
               "publicRpc": "https://rpc.monad.xyz", "explorer": "https://monadvision.com" },
  "contracts": {
    "Covenant":        "0x11A7Ab0A9D7bD531DBcF0f0630BF7167F8F198f6",
    "CovenantCurator": "0xAB0f8aB1e67cc02A9D58fc27055292289B159094",
    "DataProvider":    "0x3818a6d5018AA9eb69b6bCe09e38a7C24BBe8C22",
    "LatentSwapLEX": {
      "80/20 Wide (x5 wide)":         "0x9a369DF53EC62725c93f41B891e701BD88aFf2c8",
      "80/20 Concentrated (x5 slim)": "0x820517d0dfd1F084254571F722c5357Cb3483318",
      "50/50 Wide (x2 wide)":         "0x6A5Aa3df89bA1C1f8B471c72159458D61a9447c2",
      "50/50 Concentrated (x2 slim)": "0xE484728342853B35d0EbE71cD99A9053ae922b15"
    }
  },
  "blocks": { "covenantDeployed": 35851140, "firstMarketCreated": 35870050 },
  "quoteUnits": {
    "USD": "0x0000000000000000000000000000000000000348",
    "MON": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
  },
  "assetType": { "BASE": 0, "DEBT": 1, "LEVERAGE": 2 },
  "units": { "prices": "WAD per 1e18 raw token units (divide amount*price by 1e36)",
             "ltv": "basis points (10000 = 100%)", "duration": "seconds",
             "sqrtPrices": "Q64.96 (Uniswap v3 style)", "fees": "basis points" }
}
```

Only Monad mainnet is live.

## 2. Terminology map

The documentation uses product names. The contracts use generic names. Both appear in this page; they map as follows.

| Documentation term                               | Contract term                                         | Notes                                                                                      |
| ------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Covenant Market                                  | `marketId` (`bytes20`) with `MarketParams`            | One market per (base, quote, curator, lex) tuple                                           |
| Base Asset                                       | `baseToken`, `AssetType.BASE` (0)                     | The ERC-20 collateral deposited into the market                                            |
| Quote unit (USD, MON)                            | `quoteToken`                                          | A pricing unit, not necessarily an ERC-20. Never call `balanceOf` on it                    |
| Yield Coin (senior tranche)                      | `zToken`, `AssetType.DEBT` (1)                        | ERC-20 minted by the market. Symbol pattern `USD.b.<base>` or `MON.b.<base>`               |
| Leverage Coin (junior tranche)                   | `aToken`, `AssetType.LEVERAGE` (2)                    | ERC-20 minted by the market. Symbol pattern `<base>x5.USD`, `<base>x2.USD`, `<base>x5.MON` |
| Latent Swap AMM                                  | `LatentSwapLEX` (a "Liquid Exchange Model", LEX)      | One deployment per market type; holds pricing state for every market using it              |
| Market type (x5 wide, x5 slim, x2 wide, x2 slim) | The `lex` address in `MarketParams`                   | The LEX address identifies the market type                                                 |
| Oracle / curator                                 | `CovenantCurator`                                     | Oracle router. `curator` address in `MarketParams`                                         |
| Neutral LTV                                      | `targetLTV`                                           | Basis points                                                                               |
| Duration `D`                                     | `lexParams.debtDuration`                              | Seconds. All live market types use 7,889,400 s (3 months)                                  |
| Yield Coin NAV (notional) per token              | `lexState.lastDebtNotionalPrice`                      | WAD, quote units per whole Yield Coin                                                      |
| Yield Coin price relative to NAV                 | `debtPriceDiscount`                                   | WAD. `1e18` = par, below = discount, above = premium                                       |
| Neutral rate                                     | `lexParams.initLnRateBias`, `lexState.lastLnRateBias` | WAD log-rate per duration (see section 6.3)                                                |

Be careful with the letters: **a**Token is the **Leverage** Coin and **z**Token is the **Yield** Coin (debt). The `AssetType` enum is `BASE = 0, DEBT = 1, LEVERAGE = 2`.

## 3. Contract architecture

Five contract roles make up a deployment.

**Covenant (core).** The single entry point for every user action and the only contract that holds collateral. It stores the market registry (`marketId` to `MarketParams`), the collateral balance per market (`baseSupply`), the pause flag, and accumulated protocol fees. It pulls base tokens from the caller on mint and base-in swaps, pushes base tokens on redeem and base-out swaps, and delegates all pricing and accounting to the market's LEX. It is non-upgradeable. Owner powers are limited to enabling LEX and curator implementations for new markets, setting protocol fees (capped on-chain at 30% of yield and 5% of TVL per year; currently 0 on all markets), collecting fees, and pausing or unpausing markets.

**LatentSwapLEX (one per market type).** Implements the Latent Swap AMM and the Perpetual Debt accrual for every market that references it. On market creation it deploys the two synth tokens. It computes how many tokens a mint, redeem, or swap produces, enforces LTV gates and mint/redeem caps, accrues the Yield Coin notional, and reads the oracle through the curator. Only Covenant core can call its state-changing functions. The constructor parameters that define a market type (price band, LTV gates, duration, swap fee) are immutable and readable via `getLexParams()`.

**SynthToken (two per market).** Standard ERC-20 (OpenZeppelin) with 6 or 18 decimals matching the quote unit. Minted and burned only by the LEX. Extra getters: `getMarketId()`, `getSynthType()` (returns the `AssetType`), `getBaseAsset()`, `getCovenantCore()`. Because the LEX burns synth tokens directly from the sender, **no ERC-20 approval is needed to redeem or swap out of a Yield Coin or Leverage Coin**.

**CovenantCurator.** An oracle router (adapted from Euler's price oracle router) that resolves a `(base, quote)` pair to a price. Pairs can resolve through a configured adapter (Chainlink, Pyth, fixed rate, vault-specific adapters) or, for ERC-4626 vaults, through the vault's own `convertToAssets`. Interface: `getQuote(uint256 inAmount, address base, address quote) returns (uint256 outAmount)`, plus `getQuotes` (bid/ask), `previewGetQuote`, `getUpdateFee` and `updatePriceFeeds` for pull oracles.

**DataProvider (periphery, read-only).** Aggregates everything an integrator needs about a market into one `eth_call`: token metadata, market params and state, LEX params, config and state, current and target LTV, Yield Coin price relative to NAV, and spot prices of all three assets after accruing to the current block and current oracle price. Use it for reads. It is not required for writes.

Market creation is permissionless (`createMarket` has no access control) but requires a LEX and a curator that the core owner has enabled, and the curator must resolve the base/quote pair. In practice new markets are created in coordination with the Covenant team because the curator configuration is owner-controlled.

## 4. Market identity

```solidity
struct MarketParams { address baseToken; address quoteToken; address curator; address lex; }

// marketId = low 160 bits of keccak256(abi.encodePacked(baseToken, quoteToken, curator, lex))
MarketId = bytes20(uint160(uint256(keccak256(abi.encodePacked(p.baseToken, p.quoteToken, p.curator, p.lex)))));
```

In viem:

```ts
import { encodePacked, keccak256, type Address, type Hex } from "viem";

export function marketId(p: { baseToken: Address; quoteToken: Address; curator: Address; lex: Address }): Hex {
  const hash = keccak256(encodePacked(["address", "address", "address", "address"],
    [p.baseToken, p.quoteToken, p.curator, p.lex]));
  return `0x${hash.slice(-40)}`; // last 20 bytes
}
```

Every write and preview takes both `marketId` and `marketParams`; the core recomputes the id from the params and reverts with `E_IncorrectMarketParams` if they disagree. Read the params with `Covenant.getIdToMarketParams(marketId)` and cache them; they never change for the life of a market.

The app's market URLs use the same id: `https://app.covenant.finance/market/<marketId>`. The `?action=mint|swap|redeem` query parameter deep-links to a tab.

**Quote units.** `quoteToken` is a pricing unit. USD markets use the ISO 4217 convention (`0x…0348`, decimal 840, 6 decimals). MON markets use the EIP-7528 native-asset address (`0xEeee…EEeE`, 18 decimals). The LEX exposes `assetSymbol(addr)`, `assetDecimals(addr)` and `assetName(addr)` for these units since they are not ERC-20s.

**Synth decimals.** Yield Coins and Leverage Coins have the quote unit's decimals: 6 for USD markets, 18 for MON markets. The base token keeps its own decimals.

**Market types.** The four LEX addresses in section 1 correspond one-to-one to the four [Market types](/markets/market-types.md). On-chain parameters confirm the calibration:

| LEX           | Short name | Target LTV | Duration (s) | Swap fee (bps) |
| ------------- | ---------- | ---------- | ------------ | -------------- |
| `0x9a36…f2c8` | x5 wide    | 8000       | 7889400      | 5              |
| `0x8205…3318` | x5 slim    | 8000       | 7889400      | 5              |
| `0x6A5A…47c2` | x2 wide    | 5000       | 7889400      | 30             |
| `0xE484…2b15` | x2 slim    | 5000       | 7889400      | 30             |

## 5. Discovering markets

Covenant core has no enumeration function. Use one of the following.

1. **The** [**Live markets**](/markets/markets.md) **page.** Human-maintained list of the markets the Covenant app shows, with Yield Coin and Leverage Coin addresses. Market ids are in the app links.
2. **`CreateMarket` events** on Covenant core from block 35,851,140. Each event carries the `marketId`, the `MarketParams`, and both synth token addresses. The public RPC limits `eth_getLogs` to a 100-block range per request, so scan in chunks or use a provider without that limit. The `marketId` topic is left-aligned in the 32-byte indexed slot: take its first 20 bytes.

Not every created market is listed in the app; some are test or retired markets. Treat `statusFlag == 1` (section 6.1) as the minimum liveness check and the Live markets page as the curated set.

```ts
const createMarketEvent = parseAbiItem(
  "event CreateMarket(bytes20 indexed marketId, (address baseToken, address quoteToken, address curator, address lex) marketParams, (address aToken, address zToken) synthTokens, bytes initData, bytes lexData)"
);
// scan [35851140, latest] in 100-block windows on rpc.monad.xyz
```

## 6. Reading market state

### 6.1 DataProvider

```solidity
function getMarketDetails(address covenant, bytes20 marketId) view returns (MarketDetails);
function getMarketsDetails(address covenant, bytes20[] marketIds) view returns (MarketDetails[]);
```

Human-readable ABI (viem `parseAbi` compatible):

```
function getMarketsDetails(address covenant, bytes20[] marketIds) view returns ((bytes20 marketId, (address tokenAddress, uint8 decimals, uint256 totalSupply, string name, string symbol) baseToken, (address tokenAddress, uint8 decimals, uint256 totalSupply, string name, string symbol) quoteToken, (address tokenAddress, uint8 decimals, uint256 totalSupply, string name, string symbol) aToken, (address tokenAddress, uint8 decimals, uint256 totalSupply, string name, string symbol) zToken, (address baseToken, address quoteToken, address curator, address lex) marketParams, (uint256 baseSupply, uint128 protocolFeeGrowth, address authorizedPauseAddress, uint8 statusFlag) marketState, (uint256 lastDebtNotionalPrice, uint256 lastBaseTokenPrice, uint256 lastETWAPBaseSupply, uint160 lastSqrtPriceX96, uint96 lastUpdateTimestamp, int64 lastLnRateBias) lexState, (uint32 protocolFee, address aToken, address zToken, uint8 noCapLimit, int8 scaleDecimals, bool adaptive) lexConfig, (address covenantCore, int64 initLnRateBias, uint160 edgeSqrtPriceX96_B, uint160 edgeSqrtPriceX96_A, uint160 limHighSqrtPriceX96, uint160 limMaxSqrtPriceX96, uint32 debtDuration, uint8 swapFee, uint256 targetXvsL) lexParams, uint32 currentLTV, uint32 targetLTV, uint128 debtPriceDiscount, (uint256 baseTokenPrice, uint256 aTokenPrice, uint256 zTokenPrice) tokenPrices)[] marketsDetails)
```

`getMarketDetails` has the same return type for a single market. Pass the Covenant core address as the first argument.

Field reference:

| Field                                         | Unit                   | Meaning                                                                                                                                                      |
| --------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `baseToken`, `quoteToken`, `aToken`, `zToken` |                        | Address, decimals, total supply, name, symbol. `quoteToken.totalSupply` is 0 (not an ERC-20)                                                                 |
| `marketState.baseSupply`                      | raw base units         | Collateral held by the market. This is the market's TVL in base tokens                                                                                       |
| `marketState.protocolFeeGrowth`               | raw base units         | Protocol fees accrued and not yet collected                                                                                                                  |
| `marketState.statusFlag`                      | enum                   | `0` uninitialized, `1` live, `2` locked (mid-transaction reentrancy flag), `3` paused                                                                        |
| `lexState.lastDebtNotionalPrice`              | WAD                    | Yield Coin NAV (notional) in quote units per whole Yield Coin, accrued to now                                                                                |
| `lexState.lastBaseTokenPrice`                 | WAD                    | Oracle price of one whole base token in quote units                                                                                                          |
| `lexState.lastSqrtPriceX96`                   | Q64.96                 | Current AMM position (sqrt price). `2^96` is Neutral LTV                                                                                                     |
| `lexState.lastLnRateBias`                     | WAD (signed)           | Current log-rate bias per duration (see 6.3)                                                                                                                 |
| `lexState.lastETWAPBaseSupply`                | raw base units         | Exponential time-weighted average of `baseSupply`, used for mint/redeem caps                                                                                 |
| `lexConfig.protocolFee`                       | packed bps             | High 16 bits: fee on yield; low 16 bits: fee on TVL per year. Currently 0                                                                                    |
| `lexConfig.noCapLimit`                        | power of two           | Markets with `baseSupply < 2^noCapLimit` raw units are exempt from mint/redeem caps                                                                          |
| `lexConfig.scaleDecimals`                     | int                    | Internal: oracle quote decimals minus synth decimals (12 on USD markets, 0 on MON markets)                                                                   |
| `lexParams.edgeSqrtPriceX96_A/_B`             | Q64.96                 | AMM price band edges (`Pa`, `Pb` on the AMM page)                                                                                                            |
| `lexParams.limHighSqrtPriceX96`               | Q64.96                 | High LTV gate position                                                                                                                                       |
| `lexParams.limMaxSqrtPriceX96`                | Q64.96                 | Max LTV gate position                                                                                                                                        |
| `lexParams.debtDuration`                      | seconds                | Duration `D`                                                                                                                                                 |
| `lexParams.swapFee`                           | bps                    | Fee deducted from the output of every mint, redeem and swap (except a redemption of the entire market). Accrues to the market, i.e. to Leverage Coin holders |
| `currentLTV`, `targetLTV`                     | bps                    | Current LTV and Neutral LTV                                                                                                                                  |
| `debtPriceDiscount`                           | WAD                    | Yield Coin market price relative to NAV: `1e18` = par                                                                                                        |
| `tokenPrices.baseTokenPrice`                  | WAD per 1e18 raw units | Spot value in quote units (see 6.2)                                                                                                                          |
| `tokenPrices.aTokenPrice`                     | WAD per 1e18 raw units | Leverage Coin spot price                                                                                                                                     |
| `tokenPrices.zTokenPrice`                     | WAD per 1e18 raw units | Yield Coin spot price                                                                                                                                        |

The DataProvider accrues interest and re-reads the oracle at the current block, so `lexState` returned here can differ from the stored state returned by `LatentSwapLEX.getLexState(marketId)` (which is only updated when a transaction touches the market).

**Decoding note.** With many markets in one `getMarketsDetails` call, some client libraries have trouble decoding the large nested tuple array. If that happens, batch individual `getMarketDetails` calls through Multicall3 (`0xcA11bde05977b3631167028862bE2a173976CA11`) instead.

### 6.2 Price convention

All three `tokenPrices` (and the `tokenPrices` emitted in events) are WAD values per `1e18` raw token units, which folds token decimals into the number. The practical rules:

```
valueInQuote(amountRaw, price)   = amountRaw * price / 1e36          // whole quote units (USD or MON)
priceOfOneWholeToken(price, dec) = price * 10^dec / 1e36             // quote units per whole token
```

Example from the bbqUSDC x5 wide market (values at the time of writing; they change every block):

| Value                                     | Raw                               | Decoded                              |
| ----------------------------------------- | --------------------------------- | ------------------------------------ |
| `baseTokenPrice` (base has 18 decimals)   | `1025760814154353206`             | 1.0258 USD per bbqUSDC               |
| `zTokenPrice` (Yield Coin, 6 decimals)    | `1017726617493378139754767257237` | 1.0177 USD per Yield Coin            |
| `aTokenPrice` (Leverage Coin, 6 decimals) | `1088834449148855210054448746957` | 1.0888 USD per Leverage Coin         |
| `lastDebtNotionalPrice`                   | `1014303109803104091`             | Yield Coin NAV 1.0143 USD            |
| `debtPriceDiscount`                       | `1003483027714949277`             | Yield Coin at 1.0035 × NAV (premium) |
| `currentLTV` / `targetLTV`                | `8183` / `8000`                   | 81.83% / 80%                         |
| `baseSupply`                              | `105945086447740920982`           | 105.95 bbqUSDC collateral            |

For USD markets, USD is the quote unit, so values are already in dollars. For MON markets, convert with a MON/USD price of your choosing (the curator can quote MON in USD: `getQuote(1e18, MON, USD)` returns USD per MON with 18 decimals).

### 6.3 Derived metrics

```
ltv                 = currentLTV / 10000
leverage            = 1 / (1 − ltv)                                  // Leverage Coin structural leverage
yieldCoinNAV        = zToken.totalSupply * lastDebtNotionalPrice / 10^(18 + zDecimals)
marketTVL_quote     = baseSupply * baseTokenPrice / 1e36
premiumOrDiscount   = debtPriceDiscount / 1e18 − 1                   // >0 premium, <0 discount

// Yield Coin funding rate, continuously compounded per duration, then annualized
ratePerDuration     = lastLnRateBias / 1e18 − ln(debtPriceDiscount / 1e18)
yieldCoinAPY        = exp(ratePerDuration * 31_536_000 / debtDuration) − 1
```

`ratePerDuration` is the instantaneous rate at which the Yield Coin notional accrues; it is positive when the Yield Coin trades below NAV plus bias and can go negative at a large premium. The Covenant app and the DefiLlama yields listing both report `yieldCoinAPY` as the Yield Coin APY. There is no on-chain APY for the Leverage Coin because its return depends on the base asset's own yield and price path; the app computes `leverage × baseAPY + (1 − leverage) × yieldCoinAPY` with an off-chain `baseAPY`.

## 7. Writing: mint, redeem, swap

### 7.1 Common rules

* **Approvals.** Approve the **base token** to the Covenant core address (`0x11A7…98f6`) before a mint or a base-in swap. Yield Coins and Leverage Coins need no approval: the LEX burns them from `msg.sender` directly. Never approve the LEX or a synth token.
* **Always preview first.** `previewMint`, `previewRedeem` and `previewSwap` are `view` functions that run the same validation as the write (gates, caps, limits) and return the expected amounts plus `protocolFees` and `oracleUpdateFee`. Derive `min…` / `amountLimit` from the preview with bigint math, e.g. `out * 9900n / 10000n` for 1% slippage. The Covenant app uses 1%.
* **`data` and `msgValue`.** Reserved for pull oracles (Pyth-style) where a price update must be attached to the transaction. All live markets use push or vault oracles: pass `data = "0x"` and `msgValue = 0`, and send no ETH. If a preview ever returns a nonzero `oracleUpdateFee`, pass the update bytes in `data`, set `msgValue` to the fee, and send exactly that amount as `value`. For Pyth-routed pairs the curator expects `data = abi.encode(bytes[] priceUpdates)` as returned by Pyth Hermes.
* **Exact `value`.** Outside `multicall`, `msg.value` must equal the `msgValue` field or the call reverts with `E_IncorrectPayment`.
* **`to`** is the recipient of the output tokens and must be nonzero.
* **Amounts are raw** (token decimals), never WAD.
* **Reentrancy.** Each market is locked during a transaction (`statusFlag = 2`). Previews and DataProvider reads revert with `E_MarketLocked` if called re-entrantly from inside a market action.
* **Protocol fee.** Currently 0 on all markets, so `protocolFees` previews as 0. When nonzero it is charged in base tokens and reduces `baseSupply`.

### 7.2 Mint (pair-mint)

Deposits base tokens and mints Yield Coins and Leverage Coins in the market's current proportion, with no price impact. The market's swap fee is deducted from the minted amounts.

```
function mint((bytes20 marketId, (address baseToken, address quoteToken, address curator, address lex) marketParams,
               uint256 baseAmountIn, address to, uint256 minATokenAmountOut, uint256 minZTokenAmountOut,
               bytes data, uint256 msgValue) mintParams) payable returns (uint256 aTokenAmountOut, uint256 zTokenAmountOut)

function previewMint(...same struct...) view returns (uint256 aTokenAmountOut, uint256 zTokenAmountOut,
               uint128 protocolFees, uint128 oracleUpdateFee, (uint256 baseTokenPrice, uint256 aTokenPrice, uint256 zTokenPrice) tokenPrices)
```

Subject to the mint cap and the Max LTV gate (section 8). Blocked when the market is undercollateralized.

### 7.3 Redeem (pair or single-tranche)

Burns `aTokenAmountIn` Leverage Coins and/or `zTokenAmountIn` Yield Coins from the caller and returns base tokens. Both amounts are exact-in and may be set independently; at least one must be nonzero. Redeeming both in the market's current proportion is a pair redemption with no price impact. Redeeming one side only is priced through the AMM. In both cases the swap fee is deducted from the base output, except for a redemption of the entire market or a redemption in an undercollateralized market.

```
function redeem((bytes20 marketId, (address,address,address,address) marketParams, uint256 aTokenAmountIn,
                 uint256 zTokenAmountIn, address to, uint256 minAmountOut, bytes data, uint256 msgValue) redeemParams)
                 payable returns (uint256 baseAmountOut)

function previewRedeem(...same struct...) view returns (uint256 amountOut, uint128 protocolFees, uint128 oracleUpdateFee, (uint256,uint256,uint256) tokenPrices)
```

Subject to the redeem cap and the High LTV gate when the redemption raises LTV (burning Leverage Coins). Burning Yield Coins is always allowed, including when the market is undercollateralized (the only action allowed in that state besides selling Yield Coins for base).

### 7.4 Swap

One function covers every pair among the three assets. Swaps to or from the base token mint or burn synth tokens under the hood; swaps between the two synths rebalance the AMM.

```
function swap((bytes20 marketId, (address,address,address,address) marketParams, uint8 assetIn, uint8 assetOut,
               address to, uint256 amountSpecified, uint256 amountLimit, bool isExactIn, bytes data, uint256 msgValue) swapParams)
               payable returns (uint256 amount)

function previewSwap(...same struct...) view returns (uint256 amountCalculated, uint128 protocolFees, uint128 oracleUpdateFee, (uint256,uint256,uint256) tokenPrices)
```

| `assetIn`    | `assetOut`   | Meaning                                  | Exact-in | Exact-out |
| ------------ | ------------ | ---------------------------------------- | -------- | --------- |
| BASE (0)     | DEBT (1)     | Buy Yield Coin with base ("lend")        | yes      | no        |
| BASE (0)     | LEVERAGE (2) | Buy Leverage Coin with base ("lever up") | yes      | no        |
| DEBT (1)     | BASE (0)     | Sell Yield Coin for base                 | yes      | no        |
| LEVERAGE (2) | BASE (0)     | Sell Leverage Coin for base              | yes      | no        |
| DEBT (1)     | LEVERAGE (2) | Rotate Yield Coin into Leverage Coin     | yes      | yes       |
| LEVERAGE (2) | DEBT (1)     | Rotate Leverage Coin into Yield Coin     | yes      | yes       |

* `isExactIn = true`: `amountSpecified` is the input amount; the call reverts with `E_CrossedLimit` if the output is below `amountLimit`. Set `amountLimit = 0` in previews.
* `isExactIn = false` (synth-to-synth only): `amountSpecified` is the desired output; the call reverts if the required input exceeds `amountLimit`. Set `amountLimit` to `type(uint256).max` in previews.
* Swaps with base as input or output and `isExactIn = false` revert with `E_LEX_OperationNotAllowed`.
* The swap fee is applied exactly once to every swap, in every direction.
* Base-in swaps are subject to the mint cap; base-out swaps to the redeem cap. Any swap that raises LTV is subject to the High LTV gate.

### 7.5 Multicall and updateState

`multicall(bytes[] data)` batches several core calls in one transaction (for example two swaps, or a mint followed by a swap). Inside a multicall the per-call `msg.value == msgValue` check is skipped; instead the batch reverts unless the native value sent equals the sum of the `msgValue` fields. Nested multicalls are rejected.

`updateState(marketId, marketParams, data, msgValue)` accrues the market (interest, caps' time-weighted average, oracle read) without trading. It is never required: every mint, redeem and swap accrues first. It is useful for keeper-style flows that want fresh stored state.

### 7.6 Minimal example (viem)

```ts
import { createPublicClient, createWalletClient, http, parseAbi } from "viem";

const COVENANT = "0x11A7Ab0A9D7bD531DBcF0f0630BF7167F8F198f6";
const covenantAbi = parseAbi([
  "function getIdToMarketParams(bytes20 marketId) view returns ((address baseToken, address quoteToken, address curator, address lex))",
  "function getMarketState(bytes20 marketId) view returns ((uint256 baseSupply, uint128 protocolFeeGrowth, address authorizedPauseAddress, uint8 statusFlag))",
  "function previewSwap((bytes20 marketId, (address baseToken, address quoteToken, address curator, address lex) marketParams, uint8 assetIn, uint8 assetOut, address to, uint256 amountSpecified, uint256 amountLimit, bool isExactIn, bytes data, uint256 msgValue) p) view returns (uint256 amountCalculated, uint128 protocolFees, uint128 oracleUpdateFee, (uint256 baseTokenPrice, uint256 aTokenPrice, uint256 zTokenPrice) tokenPrices)",
  "function swap((bytes20 marketId, (address baseToken, address quoteToken, address curator, address lex) marketParams, uint8 assetIn, uint8 assetOut, address to, uint256 amountSpecified, uint256 amountLimit, bool isExactIn, bytes data, uint256 msgValue) p) payable returns (uint256 amount)",
]);
const erc20Abi = parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]);

// Buy Yield Coins with 1 bbqUSDC in the bbqUSDC x5 wide market
const marketId = "0x2BbB31F07Be3bf497adA87eb9446e0Eb5F937C89";
const marketParams = await pub.readContract({ address: COVENANT, abi: covenantAbi, functionName: "getIdToMarketParams", args: [marketId] });
const state = await pub.readContract({ address: COVENANT, abi: covenantAbi, functionName: "getMarketState", args: [marketId] });
if (state.statusFlag !== 1) throw new Error("market not live");

const params = { marketId, marketParams, assetIn: 0, assetOut: 1, to: account.address,
                 amountSpecified: 10n ** 18n, amountLimit: 0n, isExactIn: true, data: "0x", msgValue: 0n };
const [quote, , oracleFee] = await pub.readContract({ address: COVENANT, abi: covenantAbi, functionName: "previewSwap", args: [params] });
if (oracleFee !== 0n) throw new Error("pull-oracle market: attach update data");

await wallet.writeContract({ address: marketParams.baseToken, abi: erc20Abi, functionName: "approve", args: [COVENANT, params.amountSpecified] });
await wallet.writeContract({ address: COVENANT, abi: covenantAbi, functionName: "swap",
  args: [{ ...params, amountLimit: quote * 9900n / 10000n }], value: 0n });
```

## 8. Gates, caps and reverts

A preview that succeeds now may still fail at execution if the market moved; a preview that reverts tells you which rule you hit. Reverts are custom errors; decode the 4-byte selector.

**Market status.** Writes revert with `E_MarketPaused` (`0xed9c479e`) on a paused market and `E_MarketNonExistent` (`0xe2f65643`) on an unknown id. Previews still succeed on a paused market, so check `statusFlag == 1` before relying on a preview.

**LTV gates** (per market type; see each [Market type](/markets/market-types.md) page for the levels):

* Above the **High LTV gate**, actions that push LTV higher are blocked: selling or redeeming Leverage Coins, and buying Yield Coins with base or with Leverage Coins. Error `E_LEX_ActionNotAllowedGivenLTVlimit` (`0x2ab66638`).
* Above the **Max LTV gate**, pair mints are also blocked and the workout rate applies. Only Yield Coin exits (sell or redeem) and actions that lower LTV remain open.
* When the market is **undercollateralized** (collateral value below Yield Coin notional), Leverage Coin actions revert with `E_LEX_ActionNotAllowedUnderCollateralized` (`0xabc1fa28`). Yield Coin holders can still exit.

**Mint cap.** Deposits in a single transaction cannot exceed the market's current `baseSupply` (so a market can at most double per call) and the sum must stay within twice the time-weighted average supply, which tracks `baseSupply` with a half-life of about 30 minutes. Error `E_LEX_MintCapExceeded` (`0x6c8de6e1`). Applies to `mint` and to base-in swaps.

**Redeem cap.** Base withdrawn in a single transaction cannot take `baseSupply` below 75% of its time-weighted average. A full redemption of the entire market is exempt. Error `E_LEX_RedeemCapExceeded` (`0xef9b092a`). Applies to `redeem` and base-out swaps.

**Size threshold.** Both caps are skipped while `baseSupply < 2^noCapLimit` raw base units. `noCapLimit` is set per market (`lexConfig.noCapLimit`) and differs across live markets (values of 41, 70, 80 and 85 are in use), so read it rather than assume it. On current markets the threshold sits well above current supply (for example `noCapLimit = 80` with an 18-decimal base token is about 1.2 million whole tokens), so the caps only engage once a market is large.

**Synth supply cap.** Minting that would push a synth token's supply past an internal ceiling reverts with `E_LEX_MarketSizeLimitExceeded`.

Large positions should be split across transactions and, where the cap binds, across blocks spaced by tens of minutes.

### Error selectors

Covenant core:

| Selector     | Error                              | Typical cause                                                              |
| ------------ | ---------------------------------- | -------------------------------------------------------------------------- |
| `0xaf4935be` | `E_ZeroAmount`                     | Zero input amount, or a deposit that delivered nothing                     |
| `0x459f5f6e` | `E_ZeroAddress`                    | `to` is zero                                                               |
| `0xb8775684` | `E_CrossedLimit`                   | Output below `amountLimit` / `min…`, or input above `amountLimit`          |
| `0x9950c184` | `E_InsufficientAmount`             | Requested more base than the market holds, or a swap that produced nothing |
| `0x76ba676e` | `E_IncorrectMarketAsset`           | `assetIn`/`assetOut` not in 0..2                                           |
| `0x213a0fd0` | `E_EqualSwapAssets`                | `assetIn == assetOut`                                                      |
| `0x8a7ede1f` | `E_MarketLocked`                   | Reentrant call during a market action                                      |
| `0xed9c479e` | `E_MarketPaused`                   | Market paused by the authorized pause address                              |
| `0xe2f65643` | `E_MarketNonExistent`              | Unknown `marketId`                                                         |
| `0xb4ee3f59` | `E_LEXimplementationNotAuthorized` | `createMarket` with a LEX not enabled                                      |
| `0x808c99be` | `E_CuratorNotAuthorized`           | `createMarket` with a curator not enabled                                  |
| `0x601494a7` | `E_MarketAlreadyExists`            | `createMarket` for an existing tuple                                       |
| `0x7f6cd0c7` | `E_IncorrectMarketParams`          | `marketId` does not match `marketParams`                                   |
| `0x08e2ce17` | `E_Unauthorized`                   | Restricted function                                                        |
| `0xa705df45` | `E_IncorrectPayment`               | `msg.value` differs from `msgValue` (or batch total in multicall)          |
| `0xc886fec7` | `E_ProtocolFeeTooHigh`             | Owner fee setting above the cap                                            |

LatentSwapLEX:

| Selector     | Error                                       | Typical cause                                                                     |
| ------------ | ------------------------------------------- | --------------------------------------------------------------------------------- |
| `0x2ab66638` | `E_LEX_ActionNotAllowedGivenLTVlimit`       | Action would cross the High LTV gate                                              |
| `0xabc1fa28` | `E_LEX_ActionNotAllowedUnderCollateralized` | Leverage Coin action while undercollateralized                                    |
| `0x6c8de6e1` | `E_LEX_MintCapExceeded`                     | Deposit too large for the mint cap                                                |
| `0xef9b092a` | `E_LEX_RedeemCapExceeded`                   | Withdrawal too large for the redeem cap                                           |
| `0x82a547cd` | `E_LEX_OperationNotAllowed`                 | Unsupported combination (e.g. exact-out with base), or price would leave the band |
| `0x6cf03401` | `E_LEX_InsufficientTokens`                  | Burning more synth tokens than exist                                              |
| `0xe869f1da` | `E_LEX_InsufficientAmount`                  | Exact-out amount that rounds to zero input                                        |
| `0xb38d3cff` | `E_LEX_ZeroLiquidity`                       | Market has no collateral                                                          |
| `0x417969ec` | `E_LEX_OraclePriceTooLowForMarket`          | Oracle price incompatible with market parameters                                  |
| `0x94f85219` | `E_LEX_Overdeposit`                         | Native value sent without oracle update data                                      |
| `0xda47482e` | `E_LEX_MarketDoesNotExist`                  | LEX has no state for this id                                                      |

## 9. Events

Emitted by Covenant core. Every trade event carries the post-trade `tokenPrices` (same convention as section 6.2), so a full price history can be rebuilt from logs alone.

```
event CreateMarket(bytes20 indexed marketId, (address baseToken, address quoteToken, address curator, address lex) marketParams, (address aToken, address zToken) synthTokens, bytes initData, bytes lexData)
event Mint(bytes20 indexed marketId, uint256 baseAmountIn, address indexed sender, address indexed receiver, uint256 aTokenAmountOut, uint256 zTokenAmountOut, uint128 protocolFees, (uint256 baseTokenPrice, uint256 aTokenPrice, uint256 zTokenPrice) tokenPrices)
event Redeem(bytes20 indexed marketId, uint256 aTokenAmountIn, uint256 zTokenAmountIn, address indexed sender, address indexed receiver, uint256 amountOut, uint128 protocolFees, (uint256 baseTokenPrice, uint256 aTokenPrice, uint256 zTokenPrice) tokenPrices)
event Swap(bytes20 indexed marketId, uint8 assetIn, uint8 assetOut, uint256 amountIn, uint256 amountOut, address indexed sender, address indexed receiver, uint128 protocolFees, (uint256 baseTokenPrice, uint256 aTokenPrice, uint256 zTokenPrice) tokenPrices)
event MarketPaused(bytes20 indexed marketId, bool isPaused)
event UpdateMarketProtocolFee(bytes20 indexed marketId, uint32 oldMarketFee, uint32 newMarketFee)
event CollectProtocolFee(bytes20 indexed marketId, address recipient, address asset, uint128 amount)
```

Positions are plain ERC-20 balances of the Yield Coin and Leverage Coin contracts; index their `Transfer` events (token addresses from `CreateMarket`) to track holders.

Between trades the Yield Coin notional keeps accruing and the oracle price keeps moving, so event-derived prices are stale between transactions. For a continuous series, sample `DataProvider.getMarketDetails` at the block height you need (archive node required for historical blocks).

## 10. Oracles and curators

Each market prices its base token through the curator in `MarketParams`:

```
function getQuote(uint256 inAmount, address base, address quote) view returns (uint256 outAmount)
function getQuotes(uint256 inAmount, address base, address quote) view returns (uint256 bid, uint256 ask)
function previewGetQuote(uint256 inAmount, address base, address quote) view returns (uint256)
function getUpdateFee(address base, address quote, bytes updateData) view returns (uint128)
function updatePriceFeeds(address base, address quote, bytes updateData) payable
```

`getQuote(10**baseDecimals, baseToken, quoteToken)` returns the value of one whole base token in the curator's quote decimals, which are 18 for both USD and MON on the live curator. The LEX separately treats the USD unit as 6 decimals for synth tokens; `lexConfig.scaleDecimals` bridges the two. For ERC-4626 vault collateral the curator can be configured to use the vault's `convertToAssets`, which ignores withdrawal fees, liquidity limits and cooldowns; the reported price may therefore not be realizable through the vault's own redeem path. Oracle risk is discussed in [Risks](/resources/risks.md).

To propose a new market, the base/quote pair must be resolvable by an enabled curator. Reach out to the Covenant team with the asset, its oracle source, and the intended market type.

## 11. Building a frontend

A UI over Covenant needs nothing beyond an RPC and the ABIs above. The Covenant app follows this pattern:

1. **Chain setup.** Monad (chain id 143), native currency MON with 18 decimals. Use your own RPC provider; the public RPC is rate-limited and caps `eth_getLogs` at 100 blocks.
2. **Market list.** Maintain a curated list of market ids (start from [Live markets](/markets/markets.md)) rather than showing every `CreateMarket`.
3. **Market data.** One `DataProvider.getMarketDetails` call per market (batched through Multicall3) gives everything shown on a market page: prices, LTV, leverage, Yield Coin APY (formula in 6.3), TVL, supplies, token metadata. Poll per block or on a short interval.
4. **User positions.** `balanceOf(user)` on each market's base token, Yield Coin and Leverage Coin, batched through Multicall3. Value them with `tokenPrices` (6.2).
5. **Quotes.** Debounce user input and call the matching `preview*` function with zero limits, `data = "0x"`, `msgValue = 0`. Show `amountCalculated` (or the two mint outputs), the implied price, and the post-trade `tokenPrices` for price impact. Post-trade Yield Coin APY can be previewed by scaling `debtPriceDiscount` by `newZTokenPrice / currentZTokenPrice`.
6. **Transactions.** For mint and base-in swaps: ERC-20 `approve` to Covenant core (exact amount or an allowance check), then the write. For redeem and synth-in swaps: just the write. Simulate before sending, pass limits derived from the preview with a slippage tolerance (the app uses 1%), and send `value` equal to `msgValue` (0 unless a pull oracle is involved).
7. **Errors.** Map custom-error selectors (section 8) to messages. Surface gate and cap errors as actionable hints ("market above High LTV: Leverage Coin sales are paused", "reduce size: redeem cap").
8. **Status.** Hide or disable actions when `statusFlag != 1`, when `currentLTV >= 10000` (undercollateralized: only Yield Coin exits), and when the current LTV sits above the market type's gates.
9. **Charts.** Build price and volume series from `Mint`/`Redeem`/`Swap` events (section 9), or sample `DataProvider.getMarketDetails` at historical blocks.
10. **Deep links.** `https://app.covenant.finance/market/<marketId>?action=mint|swap|redeem` opens the official app on the same market, useful as a fallback.

## 12. Reference integrations

* **DefiLlama TVL adapter** ([`projects/covenant`](https://github.com/DefiLlama/DefiLlama-Adapters/tree/main/projects/covenant) in the public DefiLlama-Adapters repository). Discovers markets from `CreateMarket` logs, calls `DataProvider.getMarketsDetails`, filters `statusFlag != 0`, and sums the base-token balances held by Covenant core. A compact, complete example of read-only integration.
* **Covenant app** ([app.covenant.finance](https://app.covenant.finance)). Follows section 11 exactly: DataProvider for reads, `preview*` for quotes, 1% slippage, approvals to core only.

## 13. Integration checklist

1. Confirm chain id 143 and the core address `0x11A7Ab0A9D7bD531DBcF0f0630BF7167F8F198f6`.
2. Pick markets from the Live markets page or `CreateMarket` logs; cache `getIdToMarketParams` per market.
3. Read state with `DataProvider.getMarketDetails`; check `statusFlag == 1`.
4. Decode prices with the `1e36` rule; LTV in basis points; durations in seconds.
5. Remember `aToken` = Leverage Coin, `zToken` = Yield Coin; `AssetType` `BASE=0, DEBT=1, LEVERAGE=2`.
6. Approve only the base token, only to Covenant core.
7. Preview, then write with limits derived from the preview; `data = "0x"`, `msgValue = 0`, `value = 0` on current markets.
8. Exact-out only for synth-to-synth swaps.
9. Handle gate and cap reverts by selector; split large flows across transactions.
10. Build history from `Mint`/`Redeem`/`Swap` events or from historical `DataProvider` reads.

For questions about new markets, oracle sources, or listing support, contact the Covenant team through the channels on [covenant.finance](https://covenant.finance).
