# Umbrae developer documentation

> The markdown copy of https://www.umbrae.io/dev, generated from the HTML. Agents: start at the Agents and Agent quickstart sections. Machine-readable API: https://www.umbrae.io/openapi.json

# Quickstart

Building an agent that provides liquidity? Start with the agent quickstart. Reading market data? See public market data. Integrating the contracts directly? Read on.

For integrators

The [DLMM V2 ABI index](https://www.umbrae.io/docs/dev/abi/v2/index.json) contains copies of the application's canonical interface bundle. Contract sections below include historical deployments. Verify the target factory and current proxy implementation before signing; a published ABI alone does not establish deployment compatibility.

### 1 · Pick an RPC

Public RPC works for development. For production, use a dedicated provider (Alchemy / Infura / QuickNode) so you aren't rate-limited.
```
# Base mainnet (chain ID 8453)
RPC=https://mainnet.base.org
```

### 2 · Read a contract

Try the U1 token name with `cast` (Foundry):
```
cast call 0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3E \
  'name()(string)' --rpc-url $RPC
# → "Umbrae One"
```

The same call with `viem` in TypeScript:
```
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';

const client = createPublicClient({ chain: base, transport: http() });

const name = await client.readContract({
  address: '0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3E',
  abi: [{ name: 'name', type: 'function', stateMutability: 'view',
          inputs: [], outputs: [{ type: 'string' }] }],
  functionName: 'name',
});
// "Umbrae One"
```

### 3 · Get the active DLMM bin

Reads the live U1/WETH DLMM pair (V2 stack, 100 bps, at `0xd288…5ff2`) for the active bin ID:
```
cast call 0xd288889ef25ca7483277f39c262098f6ba615ff2 \
  'getActiveId()(uint24)' --rpc-url $RPC
```

### 4 · Find the contract you need

  * Address Book — every contract address with explorer link.
  * ABIs — downloadable JSON for every deployed contract.
  * Deploy blocks — useful for log-scan ranges.

### 5 · Pick your integration

  * DLMM Router — swap quotes and multi-hop on the concentrated-liquidity DEX.
  * DAMM Pair — the U1/WETH constant-product pool (simpler, single-pair).
  * Custody keeper — auto-rebalance a DLMM position; eligibility-gated.
  * Aggregator adapter pattern — surface our pools through 1inch / OpenOcean / Matcha.

# Agents

AI agents can open their own Umbrae account and provide liquidity on Umbrae DLMM pools on Base and Robinhood Chain, entirely over HTTP. Umbrae builds each transaction; the agent signs it with its own wallet.

Start hereAgent quickstartSign up, key, approve, build, sign, send, record, claim, close. [Machine-readableOpenAPI 3.1Every agent operation, with schemas, scopes, errors and examples.](https://www.umbrae.io/openapi.json) [OverviewAgents on UmbraeWhat agent accounts can do and how they are marked.](https://www.umbrae.io/agents)

### What an agent can do

  * Read every pool, its bins and prices (no key needed).
  * Preview a deposit, a withdrawal or a fee claim with the contract's own view functions.
  * Open a position (Spot, Curve or Bid-Ask), add to it, withdraw part of it, claim its fees and close it.
  * List its positions with their bins, amounts and unclaimed fees.

### How it works

  1. **Sign up as an agent.** Sign the `agent-auth` challenge with the agent's wallet. The message declares `Account type: AI agent (cannot be changed)`; the account is created as an AI agent and can never become a human account.
  2. **Create an API key** for that wallet, with `read` and `write` scopes.
  3. **Ask for the approvals still needed** , sign and send each one.
  4. **Build** the position transaction, **sign** it with the same wallet, **send** it, wait until it is confirmed, then **record** it on the account.

### Rules

Topic| Rule  
---|---  
Marking| Agent accounts are shown as AI agents everywhere on Umbrae.  
Rewards| Agents earn LP fees from their positions like any provider. They get no XP, quests, achievements or prize-board rewards.  
Keys| An API key acts for the one wallet it was created for. Up to 10 active keys per account; each lasts 1 to 365 days (90 by default).  
Custody| Umbrae never holds the agent's key. Position NFTs are minted to the agent's own wallet.  
Robinhood Chain| In preview: pools are public to read; writes are limited to wallets on the preview list, and other wallets get `403 CHAIN_NOT_ALLOWED`.  
Rate limits| 60 requests a minute per API key across the API, and per-route limits per IP (sign-in 10 a minute; build, quotes and approvals 120; send 120; status 300). Honor `Retry-After` on 429.  
  
# Agent quickstart

One complete TypeScript program: an agent signs up, gets a key, opens a liquidity position, claims its fees and closes it. It needs only `viem`.

Use a dedicated wallet

The wallet that signs up as an agent becomes an AI-agent account for good. A wallet that already has a human account is refused (`409 WALLET_IS_HUMAN_ACCOUNT`). Verify every transaction's target against the published addresses before signing.

### The API

Base URL `https://www.umbrae.io/api/v1/{chain}`, where `{chain}` is `base` or `robinhood`. Send `Authorization: Bearer <session token or umbr_ key>`.

Step| Call| Scope  
---|---|---  
Sign up as an agent| `POST /api/auth/challenge` with `{"purpose":"agent-auth"}`, sign the message, `POST /api/auth/wallet-auth`| none  
Create an API key| `POST /api/account/api-keys` (with the session token)| session  
Find and read a pool| `GET /pools`, `POST /pools/search`, `GET /pools/{address}`, `GET /bins?pool=`| public  
Preview| `POST /quote/add-liquidity`, `/quote/withdrawal`, `/quote/claim-fees`| read  
Approvals still needed| `POST /approvals`| read  
Build| `POST /build` (create, add_liquidity, remove_liquidity, decrease_by_percent, close, claim_fees)| write  
Send and wait| `POST /send`, then `POST /status` until `confirmed`| write / read  
Record| `POST /sync` with the build's `actionToken`| write  
Your positions| `POST /positions`| read  
  
### Wide positions

One transaction holds at most 71 bins. A `close` of a wider position returns `meta.chunkedClose: true`: build and send `close` again until it returns `false`. A `claim_fees` on a wide position returns `meta.claimComplete: false` until the last page: build the next page with `claimStartIndex = meta.claimStartIndex + meta.claimBinCount`.

### Errors

Every error is JSON `{"error": "…", "code"?: "…", "retryable"?: true}` in plain English. `400` names the bad field; `401` means sign in again; `403` is a wallet mismatch, a missing scope, or the Robinhood preview list; `429` means wait for `Retry-After`; a `5xx` with `retryable: true` can be retried.

### The program

Download it as [agent-quickstart.ts](https://www.umbrae.io/docs/agents/agent-quickstart.ts). Run it with `AGENT_PRIVATE_KEY=0x… POOL=0x… npx tsx agent-quickstart.ts`.
```
/**
 * Umbrae agent quickstart: a headless AI agent signs up, gets an API key, and
 * provides liquidity on an Umbrae DLMM pool, start to finish, over plain HTTP.
 *
 *   sign up as an agent → create an API key → read the pool → quote →
 *   approvals → build → sign → send → wait → record → claim fees → close
 *
 * Umbrae builds every transaction; this script signs each one with its own
 * wallet. Umbrae never sees the private key.
 *
 * Run (Node 20+):
 *   npm i viem
 *   AGENT_PRIVATE_KEY=0x… POOL=0x… npx tsx agent-quickstart.ts
 *
 * Environment:
 *   AGENT_PRIVATE_KEY  the agent's wallet key (required). Use a dedicated wallet:
 *                      it becomes an AI-agent account and can never be a human one.
 *   POOL               the DLMM pool address to provide liquidity in (required)
 *   CHAIN              'base' (default) or 'robinhood'
 *   RPC_URL            an RPC for the chain, used only to fill nonce and gas
 *                      before signing (default: the chain's public RPC)
 *   UMBRAE_URL         default https://www.umbrae.io
 *   DEPOSIT            amount of the pool's tokenY to deposit, as a decimal
 *                      string (default "0.0005"); the deposit is one-sided, so
 *                      only tokenY is needed
 *   BIN_RANGE          bins on each side of the active bin (default 10)
 */

import { createWalletClient, defineChain, http, parseUnits, type Hex, type PrivateKeyAccount } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const UMBRAE = process.env.UMBRAE_URL ?? 'https://www.umbrae.io';
const CHAIN = (process.env.CHAIN ?? 'base') as 'base' | 'robinhood';
const PUBLIC_RPC = { base: 'https://mainnet.base.org', robinhood: 'https://rpc.mainnet.chain.robinhood.com' } as const;
const RPC_URL = process.env.RPC_URL ?? PUBLIC_RPC[CHAIN];
const POOL = process.env.POOL as Hex | undefined;
const DEPOSIT = process.env.DEPOSIT ?? '0.0005';
const BIN_RANGE = Number(process.env.BIN_RANGE ?? '10');
const API = `${UMBRAE}/api/v1/${CHAIN}`;

interface UnsignedTransaction { to: Hex; data: Hex; chainId: number; value?: string }
interface Pool {
  address: Hex; binStep: number; currentPrice: number; tokenXPriceUsd: number; tokenYPriceUsd: number;
  tokenX: { address: Hex; symbol: string; decimals: number };
  tokenY: { address: Hex; symbol: string; decimals: number };
}
interface BuildResponse {
  transaction: UnsignedTransaction;
  actionToken: string;
  meta: { chunkedClose?: boolean; claimComplete?: boolean; claimStartIndex?: number; claimBinCount?: number };
}

/** POST or GET JSON; throws with the server's plain-English error on any non-2xx. */
async function call<T>(url: string, init: { method?: string; token?: string; body?: unknown } = {}): Promise<T> {
  const res = await fetch(url, {
    method: init.method ?? (init.body === undefined ? 'GET' : 'POST'),
    headers: {
      'content-type': 'application/json',
      ...(init.token ? { authorization: `Bearer ${init.token}` } : {}),
    },
    body: init.body === undefined ? undefined : JSON.stringify(init.body),
  });
  const json = (await res.json().catch(() => ({}))) as T & { error?: string; code?: string };
  if (!res.ok) throw new Error(`${res.status} ${url}: ${json.error ?? 'request failed'}${json.code ? ` (${json.code})` : ''}`);
  return json;
}

/** Step 1: sign up (or sign back in) as an AI agent. Returns a session token. */
async function signUpAsAgent(account: PrivateKeyAccount): Promise<string> {
  const challenge = await call<{ nonce: string; message: string }>(`${UMBRAE}/api/auth/challenge`, {
    body: { purpose: 'agent-auth', walletAddress: account.address },
  });
  // The message declares "Account type: AI agent (cannot be changed)". Signing it is the declaration.
  const timestamp = Number(/^Timestamp: (\d+)$/m.exec(challenge.message)?.[1]);
  const signature = await account.signMessage({ message: challenge.message });
  const session = await call<{ sessionToken: string; accountType?: string }>(`${UMBRAE}/api/auth/wallet-auth`, {
    body: { purpose: 'agent-auth', walletAddress: account.address, signature, message: challenge.message, timestamp, nonce: challenge.nonce },
  });
  console.log(`Signed in as ${session.accountType ?? 'ai_agent'} ${account.address}`);
  return session.sessionToken;
}

/** Step 2: create an API key for this wallet. Shown once; store it like a password. */
async function createApiKey(sessionToken: string): Promise<string> {
  const created = await call<{ key: string; keyPrefix: string; walletAddress: string }>(`${UMBRAE}/api/account/api-keys`, {
    token: sessionToken,
    body: { name: 'agent-quickstart', scopes: ['read', 'write'], ttlDays: 7 },
  });
  console.log(`API key ${created.keyPrefix}… acts for ${created.walletAddress}`);
  return created.key;
}

/** Sign an Umbrae-built transaction with the agent's wallet, send it, and wait until it is confirmed. */
async function signSendAndWait(account: PrivateKeyAccount, key: string, tx: UnsignedTransaction): Promise<Hex> {
  const chain = defineChain({
    id: tx.chainId,
    name: CHAIN,
    nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
    rpcUrls: { default: { http: [RPC_URL] } },
  });
  const wallet = createWalletClient({ account, chain, transport: http(RPC_URL) });
  const request = await wallet.prepareTransactionRequest({ to: tx.to, data: tx.data, value: BigInt(tx.value ?? '0') });
  const signedTransaction = await wallet.signTransaction(request);
  const { signature: hash } = await call<{ signature: Hex }>(`${API}/send`, {
    token: key,
    body: { signedTransaction, walletAddress: account.address },
  });
  for (let attempt = 0; attempt < 60; attempt++) {
    const status = await call<{ confirmationStatus: 'pending' | 'confirmed' | 'failed' }>(`${API}/status`, { token: key, body: { txHash: hash } });
    if (status.confirmationStatus === 'confirmed') return hash;
    if (status.confirmationStatus === 'failed') throw new Error(`Transaction ${hash} reverted`);
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }
  throw new Error(`Transaction ${hash} still pending after 3 minutes`);
}

/** Send every approval still needed for an action (each one confirmed before the next). */
async function approve(account: PrivateKeyAccount, key: string, body: Record<string, unknown>): Promise<void> {
  const { steps } = await call<{ steps: Array<{ kind: string; transaction: UnsignedTransaction }> }>(`${API}/approvals`, { token: key, body });
  for (const step of steps) {
    console.log(`Approval: ${step.kind}`);
    await signSendAndWait(account, key, step.transaction);
  }
}

/** Build → sign → send → wait → record. Returns the build meta and the recorded token ID. */
async function run(account: PrivateKeyAccount, key: string, body: Record<string, unknown>): Promise<{ meta: BuildResponse['meta']; tokenId?: string }> {
  const built = await call<BuildResponse>(`${API}/build`, { token: key, body });
  const txHash = await signSendAndWait(account, key, built.transaction);
  const synced = await call<{ tokenId?: string }>(`${API}/sync`, {
    token: key,
    body: { txHash, walletAddress: account.address.toLowerCase(), actionToken: built.actionToken },
  });
  console.log(`${String(body.actionType)} confirmed: ${txHash}`);
  return { meta: built.meta, tokenId: synced.tokenId };
}

async function main(): Promise<void> {
  if (!process.env.AGENT_PRIVATE_KEY || !POOL) throw new Error('Set AGENT_PRIVATE_KEY and POOL');
  const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as Hex);

  const sessionToken = await signUpAsAgent(account);
  const key = await createApiKey(sessionToken);

  // Step 3: read the pool (public).
  const pool = await call<Pool>(`${API}/pools/${POOL}`);
  console.log(`Pool ${pool.tokenX.symbol}-${pool.tokenY.symbol}, bin step ${pool.binStep} bps, price ${pool.currentPrice}`);

  // The fields every build of this pool repeats.
  const common = {
    walletAddress: account.address.toLowerCase(),
    pairAddress: pool.address,
    tokenX: pool.tokenX.address, tokenY: pool.tokenY.address,
    tokenXSymbol: pool.tokenX.symbol, tokenYSymbol: pool.tokenY.symbol,
    tokenXDecimals: pool.tokenX.decimals, tokenYDecimals: pool.tokenY.decimals,
    binStep: pool.binStep,
    currentPrice: pool.currentPrice, tokenXPriceUsd: pool.tokenXPriceUsd, tokenYPriceUsd: pool.tokenYPriceUsd,
  };

  // Step 4: preview the deposit (amounts here are raw integers).
  const rawDeposit = parseUnits(DEPOSIT, pool.tokenY.decimals).toString();
  const quote = await call<{ quote: { usedY: string; binIds: number[] } | null }>(`${API}/quote/add-liquidity`, {
    token: key,
    body: { ...common, amountX: '0', amountY: rawDeposit, strategy: 0, binRange: BIN_RANGE },
  });
  console.log(`Quote: ${quote.quote?.usedY ?? '0'} ${pool.tokenY.symbol} across ${quote.quote?.binIds.length ?? 0} bins`);

  // Steps 5–9: approvals, then create a one-sided Spot position below the price.
  const create = { ...common, actionType: 'create', amountX: '0', amountY: DEPOSIT, strategy: 0, binRange: BIN_RANGE, slippageBps: 100 };
  await approve(account, key, create);
  const { tokenId } = await run(account, key, create);
  if (!tokenId) throw new Error('Create confirmed but no position token ID was recorded');
  console.log(`Position #${tokenId} is earning fees`);

  // Step 10: claim fees (wide positions claim in pages until claimComplete).
  const fees = await call<{ amountX: string; amountY: string }>(`${API}/quote/claim-fees`, { token: key, body: { tokenId } });
  console.log(`Claimable: ${fees.amountX} ${pool.tokenX.symbol}, ${fees.amountY} ${pool.tokenY.symbol}`);
  // Claim only when there is something to claim: an empty claim pays gas and records nothing.
  if (Number(fees.amountX) > 0 || Number(fees.amountY) > 0) {
    await approve(account, key, { walletAddress: common.walletAddress, actionType: 'claim_fees', pairAddress: pool.address });
    let claimStartIndex = 0;
    for (;;) {
      const { meta } = await run(account, key, { ...common, actionType: 'claim_fees', tokenId, amountX: '0', amountY: '0', claimStartIndex });
      if (meta.claimComplete !== false) break;
      claimStartIndex = (meta.claimStartIndex ?? 0) + (meta.claimBinCount ?? 0);
    }
  }

  // Step 11: close (wide positions close in several rounds while chunkedClose is true).
  for (;;) {
    const { meta } = await run(account, key, { ...common, actionType: 'close', tokenId, amountX: '0', amountY: '0' });
    if (!meta.chunkedClose) break;
  }
  console.log(`Position #${tokenId} closed; tokens are back in ${account.address}`);
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : error);
  process.exitCode = 1;
});
```

# Network & RPC

Base mainnet endpoints, chain ID, and the explorer you'll use most.

Chain

Base mainnet

Optimistic rollup, OP Stack

Chain ID

8453

Block time

~2 s

Explorer

[basescan.org](https://basescan.org)

### RPC endpoints

Endpoint| Use  
---|---  
`https://mainnet.base.org`| Public RPC. Rate-limited -- fine for development, not for production.  
Alchemy / Infura / QuickNode| Production. Most providers offer Base in their default plan.  
  
### WETH on Base

The canonical wrapped-ETH contract is at `0x4200000000000000000000000000000000000006`. Read each pair's token addresses to determine its assets and direction; WETH is not always the Y side.

# Authentication & API keys

How a wallet signs in, how sessions and API keys work, and what each scope allows. Pure on-chain integration needs none of this.

On-chain reads = no auth

Every Umbrae contract is public. Reading state, scanning logs and submitting wallet-signed transactions directly never needs an Umbrae session. Sessions and keys are for Umbrae's HTTP API.

### Sign in with a wallet

  1. `POST /api/auth/challenge` with `{"purpose": "wallet-auth" | "agent-auth", "walletAddress": "0x…"}`. Returns `{nonce, expiresAt, message}`.
  2. Sign `message` exactly as returned (EIP-191 personal message). Smart-contract wallets sign the same way (ERC-1271 / ERC-6492).
  3. `POST /api/auth/wallet-auth` with `{purpose, walletAddress, signature, message, timestamp, nonce}`, where `timestamp` is the number on the message's `Timestamp:` line. Returns `{sessionToken, expiresAt, account, accountType}`.

Each nonce works once and expires after a few minutes; the timestamp must be within 5 minutes. The `agent-auth` message carries the line `Account type: AI agent (cannot be changed)`: the first sign-in creates the account as an AI agent, permanently. A wallet that already has a human account cannot sign in as an agent (`409 WALLET_IS_HUMAN_ACCOUNT`).

### Sessions

  * Send the token as `Authorization: Bearer <sessionToken>`. Browsers also receive it as an HttpOnly cookie.
  * Sessions last 7 days. Tokens are stored only as hashes. An expired session answers `401`; sign in again.
  * `POST /api/auth/logout` ends the session. It does not end an API key: a key sent to it gets `400` with `code: "API_KEY_NOT_A_SESSION"` and keeps working (revoke keys instead).
  * Unlinking a wallet from the account ends that wallet's sessions on the account.

### API keys

  * Create one with a session (not with another key): `POST /api/account/api-keys` with `{"name": "…", "scopes": ["read","write"], "ttlDays": 90, "walletAddress"?: "0x…"}`. The response includes `key` (`umbr_…`) once; store it like a password.
  * A key acts for the one wallet it was created for (the signed-in wallet, or another wallet linked to the account). Changing the account's primary wallet does not move a key.
  * Scopes: `read` covers quotes, positions, approvals and transaction status; `write` covers build, send and record. A read-only key on a write operation gets `403` with `code: "WRITE_SCOPE_REQUIRED"`: the key is valid, it needs the `write` scope.
  * Up to 10 active keys; each lasts 1 to 365 days. List with `GET /api/account/api-keys`; revoke with `DELETE /api/account/api-keys/{id}` (needs a session, not a key).
  * A key trades and manages positions; it never manages the account. Linking, unlinking or choosing wallets, creating or revoking keys, profile, email, terms, account settings and access all need a wallet sign-in: a key gets `403` with `code: "INTERACTIVE_SESSION_REQUIRED"`.

### Public HTTP API

The [OpenAPI specification](https://www.umbrae.io/openapi.json) describes the agent API and the anonymous market-data operations. Read [the integration guide](https://www.umbrae.io/docs/integration.md) for units, retries and indexing.

# Public market data

Read Umbrae market data on Base without an API key, session or wallet connection.

[Machine-readableOpenAPIFour public operations on four adapter servers, plus the agent API.](https://www.umbrae.io/openapi.json) [Plain textIntegration guideIndexing, units, freshness and retries.](https://www.umbrae.io/docs/integration.md) [For agentsDiscovery indexLinks agents to the API, the quickstart and the contracts.](https://www.umbrae.io/llms.txt)

### First request
```
curl --fail --silent --show-error \
  https://www.umbrae.io/api/public/geckoterminal/dlmm/latest-block
```

Operation| Input| Result  
---|---|---  
`GET /latest-block`| None| Last indexed block and timestamp  
`GET /asset`| Token address as `id`| Token metadata  
`GET /pair`| Pool address as `id`| Pool metadata  
`GET /events`| `fromBlock` and `toBlock`| Events in an inclusive block range  
  
Choose a DAMM or DLMM base URL from the OpenAPI server list. GeckoTerminal and DEX Screener URLs are aliases for the same venue data. DLMM adapters expose curated pools and swap events; DAMM also exposes joins and exits.

### Build a reliable reader

Preserve token-unit strings, deduplicate events by transaction hash and event index, and advance your cursor only after a successful response. Honor `Retry-After` on 429 and back off on upstream 502 responses. Indexed data can lag the chain; it is not an executable quote.

### Providing liquidity

The same specification also describes the agent API: build, sign, send and record position transactions with an API key. To integrate the contracts directly instead, use the [DLMM V2 ABI bundle](https://www.umbrae.io/docs/dev/abi/v2/index.json), verify the current deployment, quote and simulate from the actual caller, and sign with the caller's wallet. API keys act only for the wallet they were created for.

Documentation prepared by GPT-6 via Codex, 2026-09-08.

# Address Book

DLMM V2 addresses are generated from the application configuration. Legacy deployments are identified separately. Other deployment notes are dated records; verify live state before signing.

Source of truth

Addresses here are mirrored from `docs/CONTRACTS.md` in the Umbrae monorepo. If anything on this page disagrees with that file, that file wins. Every address links to its Basescan / Etherscan `#code` page where you can read the verified Solidity source.

### Token + migration

Name| Address| State  
---|---|---  
**U1Token**  
`Umbrae One` · `U1`| [`0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3E`](https://basescan.org/address/0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3E#code)| Sealed supply 5,000,000 × 1018. 18 decimals. No mint, no burn, no owner.  
**U1Migration**| [`0xb374f3bC4085dE6675AF44Fa81472A93402ad467`](https://basescan.org/address/0xb374f3bC4085dE6675AF44Fa81472A93402ad467#code)| Owner = TimelockController. Window OPEN until unix `1780388389` (≈ June 2026). 72h delay between entitlement set and `claim()`.  
**U1ReserveLP**| [`0x9bb9961059f90041672AdE075450DDA398310fBd`](https://basescan.org/address/0x9bb9961059f90041672AdE075450DDA398310fBd#code)| Holds 850,000 U1. Only outbound op: `depositIntoLP` to the DAMM pool.  
  
### Rewards

Name| Address| State  
---|---|---  
**U1TokenLockVault**| [`0xf771F202e8B49612e83f18B68D6b268765A40F72`](https://basescan.org/address/0xf771F202e8B49612e83f18B68D6b268765A40F72#code)| 5% protocol-fee payout vault for U1 lockers. Reward token = WETH. `MIN_LOCK_DURATION = 7 days`. Owner = U1/DAMM Timelock.  
  
### Current DLMM V2 trading stack — Base mainnet

Remediated deployment of 2026-08-24. Use these addresses with the [V2 ABI bundle](https://www.umbrae.io/docs/dev/abi/v2/index.json). [Download the deployment registry](https://www.umbrae.io/docs/dev/contracts.json). Implementation and extension addresses are source references; call the appropriate proxy or pair.

Configuration name| Address  
---|---  
`timelock`| [`0xFfdF1cc01BE7150a81E537Dca97364C7022c82EB`](https://basescan.org/address/0xFfdF1cc01BE7150a81E537Dca97364C7022c82EB#code)  
`factory`| [`0x9DBB9289d0D75508b5D8EE01FfE4eb7c412F733b`](https://basescan.org/address/0x9DBB9289d0D75508b5D8EE01FfE4eb7c412F733b#code)  
`router`| [`0x5A7673d413f510f4b5c191d96837694C5942bF38`](https://basescan.org/address/0x5A7673d413f510f4b5c191d96837694C5942bF38#code)  
`beacon`| [`0xf74ca4e58DA6f4F8D1dc1D9fF087b1bEd1d9a573`](https://basescan.org/address/0xf74ca4e58DA6f4F8D1dc1D9fF087b1bEd1d9a573#code)  
`positionNftManager`| [`0x362A4AD56E93B71b0dBB28f9Ba1e578E5b077134`](https://basescan.org/address/0x362A4AD56E93B71b0dBB28f9Ba1e578E5b077134#code)  
`pairViewer`| [`0xEaBC5b163bF79CdCE31027E722361802FAbBe8fD`](https://basescan.org/address/0xEaBC5b163bF79CdCE31027E722361802FAbBe8fD#code)  
`positionNftManagerViewer`| [`0x9c37eD70cf46a732A9Ac60663C1811dAa866822C`](https://basescan.org/address/0x9c37eD70cf46a732A9Ac60663C1811dAa866822C#code)  
`pairExtension`| [`0x17BDe1AFdb7218347f422E1e0c26296F73799340`](https://basescan.org/address/0x17BDe1AFdb7218347f422E1e0c26296F73799340#code)  
`pairImplementation`| [`0x2d7C648f6D33A89550c72218003d0c683F25AF5a`](https://basescan.org/address/0x2d7C648f6D33A89550c72218003d0c683F25AF5a#code)  
`pausedImplementation`| [`0x7f5464D476318D36177De9bFE80422Bd58E502Fe`](https://basescan.org/address/0x7f5464D476318D36177De9bFE80422Bd58E502Fe#code)  
  
Discover pools through the current factory's `allPairsLength()` and `allPairs(i)`. Read the pair's tokens and bin step instead of reusing an old pair list.

### Legacy DLMM — withdrawal-only

**Paused for trading. Do not integrate, deposit, or grant new approvals to this retired deployment.** These addresses are retained only to identify existing positions for withdrawal. The current application uses the V2 deployment above. Use the legacy interface only when withdrawing a verified legacy position.

Name| Address| State  
---|---|---  
**DLMM Router** (proxy)| [`0x4965DD6877ca9DE77caca2f57996651e7AF23c93`](https://basescan.org/address/0x4965DD6877ca9DE77caca2f57996651e7AF23c93#code)| Retired deployment; legacy withdrawals only.  
**DLMM Factory**| [`0x17Da44dcbdffD8c715be7A368E19F252C2940Fee`](https://basescan.org/address/0x17Da44dcbdffD8c715be7A368E19F252C2940Fee#code)| Retired deployment; legacy withdrawals only.  
**DLMM Beacon**| [`0xa1BF1667bE1Dab835E224CDA666728f3e5194496`](https://basescan.org/address/0xa1BF1667bE1Dab835E224CDA666728f3e5194496#code)| Retired deployment; legacy withdrawals only.  
**Position NFT Manager** (proxy)| [`0x1a49ab1d93C269D22E98a8786823526813b04bD6`](https://basescan.org/address/0x1a49ab1d93C269D22E98a8786823526813b04bD6#code)| Retired deployment; legacy withdrawals only.  
**Pair Viewer** (proxy)| [`0xbA3A5400A95b055b1412e18ad1978EdF32Fc3F05`](https://basescan.org/address/0xbA3A5400A95b055b1412e18ad1978EdF32Fc3F05#code)| Retired deployment; legacy withdrawals only.  
**Position NFT Viewer** (proxy)| [`0x13302a890EaA061f6015d8ab2c08FaB28b1C5538`](https://basescan.org/address/0x13302a890EaA061f6015d8ab2c08FaB28b1C5538#code)| Retired deployment; legacy withdrawals only.  
  
### Retired custody keeper stack (v8) — do not integrate

**Historical addresses only.** v8 was retired after a fee-attribution defect. These contracts are not the current V2 trading stack. Consult the canonical contract registry for keeper migration status.

Keeper **v8** deployed 2026-06-23 (deploy block `47679950`, owned by 48h timelock). v8 is a keeper-only upgrade: `CustodyRebalanceKeeper` \+ linked `KeeperMath` changed; peripherals (OrderManager / OrderExecutor / EligibilityManager / UmbraeSwapRouter / PositionNFTManager) reused from v7 unchanged.

Contract| Address  
---|---  
**CustodyRebalanceKeeper** (v8)| [`0xa98D5e73D890A0A5C1db478a456F8d29B1581fE8`](https://basescan.org/address/0xa98D5e73D890A0A5C1db478a456F8d29B1581fE8)  
**OrderManager**| [`0xA6861fD7a51f479dF84A300721222d350fFeC8c9`](https://basescan.org/address/0xA6861fD7a51f479dF84A300721222d350fFeC8c9)  
**OrderExecutor**| [`0x8d28ce16cBf19F987A72B531b75756AA5f15C00e`](https://basescan.org/address/0x8d28ce16cBf19F987A72B531b75756AA5f15C00e)  
**EligibilityManager**| [`0x6AE6faf6F82cf2ccE8e40bF473c2000D1eBD4102`](https://basescan.org/address/0x6AE6faf6F82cf2ccE8e40bF473c2000D1eBD4102)  
**UmbraeSwapRouter**| [`0xA8F5f4DbdadD1de15fb9A77E0e53950a70186199`](https://basescan.org/address/0xA8F5f4DbdadD1de15fb9A77E0e53950a70186199)  
**KeeperMath** (linked library, v8)| [`0x5B31C45d80EBD1b589AC5DE0f94755Ced3383275`](https://basescan.org/address/0x5B31C45d80EBD1b589AC5DE0f94755Ced3383275)  
  
### DAMM V4 trading stack

Name| Address  
---|---  
**DAMMPair** (U1/WETH, `DAMM U1-WETH LP`)| [`0x296964C34a571fCf85d3F74FB815ee871F5A08d4`](https://basescan.org/address/0x296964C34a571fCf85d3F74FB815ee871F5A08d4#code)  
**DAMMFactory** (proxy, ERC-1967)| [`0xD14322b444415d78DBBF646BB369Ec325a1aCD5c`](https://basescan.org/address/0xD14322b444415d78DBBF646BB369Ec325a1aCD5c#code)  
**DAMMRouter**| [`0x71225FC9E1c522dC33E8f7CFA3E8dBCf2F3dF205`](https://basescan.org/address/0x71225FC9E1c522dC33E8f7CFA3E8dBCf2F3dF205#code)  
**DAMMPairBeacon**| [`0xB6363F6091d185B431Ad51F7Cc0eF49797fa8569`](https://basescan.org/address/0xB6363F6091d185B431Ad51F7Cc0eF49797fa8569#code)  
**DAMMPair impl**| [`0x1d8752A8533668FBC49E4721790592f3111A3D0A`](https://basescan.org/address/0x1d8752A8533668FBC49E4721790592f3111A3D0A#code)  
**DAMMPairPaused** (emergency stub)| [`0xEcEc2DCBE4a61De7A7f8530B510a801b0EA76B22`](https://basescan.org/address/0xEcEc2DCBE4a61De7A7f8530B510a801b0EA76B22#code)  
**DAMMFactory impl**| [`0x5491E26dBFF761C10478e43d0D5c289CC8816630`](https://basescan.org/address/0x5491E26dBFF761C10478e43d0D5c289CC8816630#code)  
  
### Governance

Role| Address| Delay  
---|---|---  
**U1 / DAMM Timelock**| [`0xC915dA2847A0640339D0071a0f92cC1562f8Eee4`](https://basescan.org/address/0xC915dA2847A0640339D0071a0f92cC1562f8Eee4#code)| 48h  
**Legacy DLMM Timelock (withdrawal-only stack)**| [`0x8c1Ab74D8FDE9d5B860896da2A1057d59513E2CF`](https://basescan.org/address/0x8c1Ab74D8FDE9d5B860896da2A1057d59513E2CF#code)| 10 min  
  
### External canonical

Name| Address| Notes  
---|---|---  
**WETH** (Base canonical)| [`0x4200000000000000000000000000000000000006`](https://basescan.org/address/0x4200000000000000000000000000000000000006#code)| Wrapped ETH on Base. The Y side of every Umbrae pair.  
  
### Ethereum mainnet

Name| Address| State  
---|---|---  
**IgnisNFTLockVault**| [`0x7c0A392fA177E5019FFd47f3daD5982BB1110D3C`](https://etherscan.io/address/0x7c0A392fA177E5019FFd47f3daD5982BB1110D3C#code)| Locks an Ignis Elite NFT for 7/14/30 days. Eligibility for keeper access + platform rewards.  
  
# ABIs

For DLMM V2, use the [current application ABI bundle](https://www.umbrae.io/docs/dev/abi/v2/index.json). The table below includes legacy DLMM and keeper interfaces; verify deployment compatibility before use. Bind a proxy's implementation ABI at the proxy address.

Naming

`live-impl` means the address you talk to is a proxy -- the ABI is the implementation behind it. `live` means the address is the contract itself. `pre-deploy` means built and audited but not on-chain yet (skipped from this index).

### Current DLMM V2 interfaces

[Download canonical V2 interfaces and hashes](https://www.umbrae.io/docs/dev/abi/v2/index.json), paired with the [generated deployment registry](https://www.umbrae.io/docs/dev/contracts.json). Use the proxy address from the Address Book, not an implementation address.

### Historical interfaces

Legacy DLMM and keeper ABI downloads describe retired deployments. They are not compatible references for a new V2 integration. Use them only for a verified legacy withdrawal.

Bind at the proxy, not the implementation

For any proxy contract: bind the ABI at the proxy address you find in the Address Book, not at the implementation address. Calls to the implementation directly do not have state and may revert.

# Deploy blocks & explorer

Useful for log-scan ranges and historical reads. All references are on Basescan (chain ID 8453).

### Custody keeper stack v8 (active)

Contract| Deploy block| Notes  
---|---|---  
CustodyRebalanceKeeper| `47679950`| v8 deploy 2026-06-23. Earliest block to scan for `Rebalanced`, `FeesRealized`, `RetainedWithdrawn` events.  
KeeperMath (linked lib)| `47679950`| v8 redeploy alongside the keeper.  
  
### DLMM pairs

Pair| Notes  
---|---  
U1 / WETH| Added 2026-05-15 (verified via `factory.allPairs(13)`).  
  
### Block explorer

  * **Basescan** — [basescan.org](https://basescan.org). Every contract on this site links to its `#code` page for verified source.
  * **Etherscan** — [etherscan.io](https://etherscan.io), used only for the Ethereum-side NFT Lock Vault.

# U1 token

A standard ERC-20 with a sealed supply. No mint, no burn, no owner.

Address

[0x14a4e80d…CCEd3E](https://basescan.org/address/0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3E#code)

Name / Symbol

Umbrae One · U1

Decimals

18

Total supply

5,000,000

Sealed (no mint or burn function exists)

### Reads
```
name()        → "Umbrae One"
symbol()      → "U1"
decimals()    → 18
totalSupply() → 5000000 * 1e18
balanceOf(address) → uint256
allowance(owner, spender) → uint256
```

### Writes
```
approve(spender, amount) → bool
transfer(to, amount) → bool
transferFrom(from, to, amount) → bool
```

### Why "sealed"

The contract has **no** `mint`, `burn`, `pause`, or owner-only functions. Supply at the address above is the exhaustive supply of U1 that will ever exist. Verify on-chain:
```
cast call 0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3E 'totalSupply()(uint256)' \
  --rpc-url https://mainnet.base.org
# → 5000000000000000000000000
```

# U1 lock vault (rewards)

Lock U1 to earn the protocol-fee payout in WETH. Pull-based rewards, monotonic lock extension, withdraw always available after the lock window.

Address

[0xf771F202…A40F72](https://basescan.org/address/0xf771F202e8B49612e83f18B68D6b268765A40F72#code)

Lock token

U1

Reward token

WETH

Min lock

7 days

### Lifecycle

  1. **Approve U1** to the vault, then `lock(amount, duration)`.
  2. Rewards accumulate as protocol fees are deposited; query your share with `pendingReward(holder)`.
  3. **Claim** WETH any time via `claimReward()` -- works while still locked.
  4. **Extend** the lock by calling `extendLock(newUnlockTime)` -- monotonic, new unlock time must be ≥ current.
  5. **Withdraw** the U1 after `unlockTime` via `withdraw()`.

### Surface
```
lock(uint256 amount, uint256 duration) external
extendLock(uint256 newUnlockTime) external
claimReward() external returns (uint256)
withdraw() external
pendingReward(address holder) view returns (uint256)
lockOf(address holder) view returns (uint256 amount, uint64 unlockTime)
totalLocked() view returns (uint256)
totalRewardsAccumulated() view returns (uint256)
notifyReward(uint256 amount) external onlyOwner
```

### Operational notes

  * **Never transfer the reward token directly to the vault** -- use `notifyReward` (owner-only) so the per-holder accounting updates.
  * Per the audit gate, the operator waits until `totalLocked >= 1_000e18` before the first `notifyReward`.
  * The vault is pausable but **unlock always works** , even when paused.

# Migration (sunsetting)

Sunsetting

The U1 migration window is open until unix `1780388389` (≈ June 2026). Entitled-but-unclaimed entries stay claimable forever -- the window controls new entitlement creation, not claims. Once the window closes the contract goes read-only.

Solana → Base U1 migration. A backend operator sets per-address entitlements after off-chain validation; a 72h security delay then opens the claim.

### Reads
```
migrationWindowEnd() view returns (uint256)         // 1780388389
isWindowOpen() view returns (bool)
entitlementOf(address user) view returns (uint256)
claimableAt(address user) view returns (uint256)    // unix; claims OK when block.timestamp >= this
isClaimable(address user) view returns (bool)
totalEntitled() view returns (uint256)
totalClaimed() view returns (uint256)
unclaimedTotal() view returns (uint256)
```

### Write (user-facing)
```
claim() external                                    // claims caller's full entitlement
```

### UI states

State| Detection  
---|---  
No entitlement| `entitlementOf(user) == 0`  
Entitled, not yet matured| `entitlement > 0 && now < claimableAt` -- show countdown  
Claimable now| `isClaimable(user) == true`  
  
### Custom errors
```
NoEntitlement()
ClaimNotYetAvailable(uint256 claimableAt)
WindowAlreadyClosed()
```

# DLMM V2 overview

Use the remediated deployment in the Address Book. Positions hold LP shares across discrete price bins and are represented by ERC-721 NFTs.

V2 uses declared-input swaps, a unified price domain, per-bin fee backing, up to 101 bins per position and fee-claim pages of at most 40 bins. Legacy balance-delta swap examples and legacy position interfaces do not describe this deployment.

Read current pool parameters, fees and permissions from the deployed contracts. The canonical deployment record is `docs/CONTRACTS.md`; integration behavior is documented in `docs/reference/DLMM_V2_INTEGRATION_2026-08-20.md`.

# DLMM V2 Router

Use `router` from the [current deployment registry](https://www.umbrae.io/docs/dev/contracts.json) with [UmbraeLBRouterUpgradeable.json](https://www.umbrae.io/docs/dev/abi/v2/UmbraeLBRouterUpgradeable.json).

Read the exact argument types for `swapExactTokensForTokens` or `swapTokensForExactTokens` from that ABI. Obtain a fresh quote from the V2 Pair Viewer, set the caller's slippage and deadline, and simulate the exact calldata from the actual caller before requesting a signature.

Verify each pair belongs to the current factory. Token allowance must target the current router; confirm a successful approval receipt before submitting the swap.

# DLMM V2 Factory

Use `factory` from the [current deployment registry](https://www.umbrae.io/docs/dev/contracts.json) with [UmbraeLBFactoryV2.json](https://www.umbrae.io/docs/dev/abi/v2/UmbraeLBFactoryV2.json).

Enumerate pairs with `allPairsLength()` and `allPairs(i)`. Use the full V2 `getPair` signature, including `baseFactor`, from the ABI. Do not use a legacy three-argument lookup.

Pair creation is permissioned. Check the caller's pool-creation permissions before building a transaction. Read current ownership from the contract: the deployment registry records a pending factory handover, so deployment into the timelock-governed stack alone is not proof that handover completed.

# DLMM V2 Pair

Discover the pair through the current V2 factory and bind [UmbraeLBPairUpgradeable.json](https://www.umbrae.io/docs/dev/abi/v2/UmbraeLBPairUpgradeable.json) at the pair address. Implementation and extension addresses in the registry are source references, not independent trading destinations.

Use `getActiveId`, `getBin`, `getTokenX` and `getTokenY` for current pool state. Bind overloaded swap functions by their full ABI signature. Do not transfer tokens to a pair using a legacy balance-delta swap recipe.

Fees are accounted separately from principal. Read the relevant fee getters; a reserve or token-balance difference is not a fee entitlement.

# DLMM V2 Pair Viewer

Use `pairViewer` from the [current deployment registry](https://www.umbrae.io/docs/dev/contracts.json) with [UmbraeLBPairViewer.json](https://www.umbrae.io/docs/dev/abi/v2/UmbraeLBPairViewer.json).

The viewer exposes `quoteSwap`, `quoteSwapDetailed`, `quoteSwapIn` and `quoteSwapInDetailed`. Decode the exact return shape from the current ABI. A quote is a read; simulate the transaction with the caller's minimum output before signing.

# DLMM V2 Position NFT Manager

Use `positionNftManager` from the [current deployment registry](https://www.umbrae.io/docs/dev/contracts.json) with [PositionNFTManagerV2.json](https://www.umbrae.io/docs/dev/abi/v2/PositionNFTManagerV2.json).

The lifecycle uses `createPositionWithStrategy`, `increasePositionWithStrategy`, `decreasePosition`, `decreasePositionByPercent` and `closePosition`. Use `claimFeesRange` to page fee claims within the 40-bin limit.

For create or increase, simulate the exact deposit and derive `minTotalLiquidity` from the total LP shares returned by that simulation. Distribution weights are not minted shares. Preserve the user's active-bin and share-slippage limits.

ERC-20 allowances target the manager. Pair `setApprovalForAll(manager, true)` grants the manager operator rights over the wallet's LP shares in that pair, including fee settlement. Explain this grant before signing, and require a successful receipt before the deposit build.

# DLMM V2 Position NFT Viewer

Use `positionNftManagerViewer` from the [current deployment registry](https://www.umbrae.io/docs/dev/contracts.json) with [PositionNFTManagerViewer.json](https://www.umbrae.io/docs/dev/abi/v2/PositionNFTManagerViewer.json).

Decode the viewer's position and fee reads using this ABI. Position identity includes the manager and pair, not just a token ID. Keep historical legacy positions distinct from current V2 positions.

# DLMM Subgraph

The Graph subgraph indexing DLMM `Factory`, `Pair`, `Position NFT Manager`, and `UmbraeLBToken` events for pair / position / volume queries.

### Schema highlights

  * `Pair` -- tokens, bin step, active id, reserves, volume, fees, swap count, price changes (5m, 15m, 1h, 4h, 8h, 12h, 24h, 7d, 30d).
  * `Position` -- NFT positions: owner, pair, bin ranges, liquidity amounts, fees accrued.
  * `Swap` -- individual swap events (amount in/out, active id, fee, timestamp).
  * `PairHourlySnapshot` / `PairDailySnapshot` -- time-series rollups for charts and 24h stats.
  * `Deposit` / `Withdrawal` / `FeeClaim` -- per-user history.
  * `User` -- wallet with aggregated stats (position count, fees claimed, last active).

### Example queries
```
{
  pairs(orderBy: swapCount, orderDirection: desc, first: 20) {
    id tokenXSymbol tokenYSymbol binStep activeId
    reserveX reserveY totalVolumeX totalVolumeY totalFeesX totalFeesY
    swapCount positionCount
  }
}

# 24h volume
{
  pairDailySnapshots(where: { dayStartUnix_gte: 1706745600 }, orderBy: volumeX, orderDirection: desc) {
    pair { id tokenXSymbol tokenYSymbol }
    volumeX volumeY feesX feesY swapCount reserveX reserveY
  }
}

# User positions
{
  user(id: "0xalice...") {
    positions { tokenId pair { id } isOpen strategy }
  }
}
```

### What the Graph CANNOT compute

The subgraph indexes **token amounts**. USD values are computed by your client by combining the subgraph data with a USD price feed:
```
volumeUsd = volumeX * priceXUsd + volumeY * priceYUsd
apy       = (fees24hUsd / tvlUsd) * 365 * 100
```

Use the source-of-truth USD feeds you trust (CoinGecko / Birdeye / our `/api/base/weth-usd`); the subgraph stays USD-free for portability.

### Endpoint

Production gateway URL is provisioned per integrator with a consumer API key (do not share an Umbrae-owned URL -- that puts query load on our quota). Reach out for issuance. Local dev: `http://localhost:8000/subgraphs/name/umbrae/dlmm`.

# DAMM overview & architecture

Umbrae's constant-product DEX. Single live pair (U1/WETH) with dynamic fees, LP locking, and a 5% protocol-fee stream to U1 lockers.

### Mental model

  * **Constant-product** -- classic x*y=k with a base fee + a volatility-driven dynamic adjustment.
  * **LP is an ERC-20** on the pair contract -- no NFT, no per-bin shares. Simpler than DLMM.
  * **Lockable LP** -- the pair has a built-in `lockLP`/`unlockLP` surface; locked LP keeps earning fees.
  * **Protocol fee** = 5% of swap fees, denominated in WETH, claimable to the protocol fee wallet.

### Stack
```
DAMMFactory (ERC-1967 proxy)  ──creates──▶  DAMMPair instances (BeaconProxies)
                                                └─ DAMMPairBeacon (48h-timelocked)
                                                    └─ DAMMPair impl
DAMMRouter (stateless ETH→LP helper)
DAMMPairPaused (emergency stub; beacon swaps to this for incident response)
```

### Token X vs Y

Same convention as DLMM: `tokenX = U1`, `tokenY = WETH` on the live pair because U1's address sorts lower.

### Trust signals

  * All 7 DAMM V4 contracts are source-verified on Basescan.
  * Pair upgrades require 48h beacon timelock + 48h governance timelock (dual gate).
  * LP locked through 2031-05-07 by the LP-treasury Safe.

# DAMM Pair (U1/WETH)

The trading + LP + lock surface. Single contract for swap, add/remove liquidity, claim fees, lock LP.

Address

[0x296964C3…08d4](https://basescan.org/address/0x296964C34a571fCf85d3F74FB815ee871F5A08d4#code)

tokenX / tokenY

U1 / WETH

Base fee

100 bps

\+ dynamic volatility adjustment

Protocol fee

5%

of swap fees, in WETH

### Reads -- state
```
getReserves()         view returns (uint112 reserveX, uint112 reserveY)
getAmountOut(uint256 amountIn, bool xToY)  view returns (uint256 amountOut)
currentFeeBps()       view returns (uint16)
tokenX() / tokenY() / feeToken()  view returns (address)
protocolFeesAccumulated() / lpFeesAccumulated()  view returns (uint256)
pendingLPFees(address holder)  view returns (uint256)
```

### Reads -- LP token (ERC-20) + lock
```
name() / symbol() / decimals() / totalSupply() / balanceOf(addr) / allowance(o, s)
lockedBalance(holder) view returns (uint256)
lockOf(address) view returns (uint256 amount, uint64 unlockTime)
```

### Writes
```
swap(uint256 amountIn, bool xToY, uint256 amountOutMin, address to)
  returns (uint256 amountOut)

addLiquidity(uint256 amountX, uint256 amountY, address to, uint256 minLiquidity)
  returns (uint256 lpTokens)

removeLiquidity(uint256 lpAmount, uint256 minAmountX, uint256 minAmountY, address to)
  returns (uint256 amountX, uint256 amountY)

lockLP(uint256 additionalAmount, uint64 newUnlockTime)
unlockLP()
claimLPFees(address recipient) returns (uint256 amount)
```

### Direction (xToY)

User picks| `xToY`  
---|---  
Sell U1 → get WETH| `true`  
Buy U1 with WETH| `false`  
Buy U1 with native ETH| `false` (wrap ETH→WETH first via `weth.deposit{value: ...}`)  
  
No deadline parameter on swap

The pair contract has **no`deadline`** on `swap`. Slippage protection is the only floor -- `amountOutMin` must be set tight enough to reject a stale quote. Compute as `quotedOut * (10000 - slippageBps) / 10000`.

### Events
```
Swap(address sender, address recipient, uint256 amountXIn, uint256 amountYIn,
     uint256 amountXOut, uint256 amountYOut, uint16 feeBps)
LiquidityAdded(address provider, uint256 amountX, uint256 amountY, uint256 lpTokensMinted)
LiquidityRemoved(address provider, uint256 amountX, uint256 amountY, uint256 lpTokensBurned)
LPFeesClaimed(address holder, address recipient, uint256 amount)
LPLocked(address holder, uint256 totalAmount, uint64 unlockTime)
LPUnlocked(address holder)
Sync(uint112 reserveX, uint112 reserveY)
```

### Custom errors
```
InsufficientLiquidity()   InsufficientOutput()   InsufficientInput()
SlippageExceeded()        InvalidRecipient()
LockedTransfer()          LockNotExpired()       LockMustExtend()
```

# DAMM Router

Stateless helper. The router exposes one convenience function: an ETH→LP one-shot for users adding liquidity from native ETH.

Address

[0x71225FC9…3dF205](https://basescan.org/address/0x71225FC9E1c522dC33E8f7CFA3E8dBCf2F3dF205#code)

### What it does

  * `addLiquidityETH(...)` -- wraps the user's ETH and adds liquidity to the pair in a single tx.
  * **No`swapETH`** on the current router. For pure swaps from ETH: wrap (`weth.deposit`) → approve → swap.
  * The router has no state, no admin functions, and no upgrade path. Safe to bind without timelock checks.

For LP management (lock / unlock / claim fees), interact with the Pair directly -- the router is intentionally minimal.

# DAMM Factory & beacon

Pair deployment + upgrade plumbing. ERC-1967 proxy on the factory; BeaconProxy on every pair, with the beacon's implementation pointer behind a 48h timelock.

### Addresses

Role| Address  
---|---  
DAMMFactory (proxy)| [`0xD14322b4…A1aCD5c`](https://basescan.org/address/0xD14322b444415d78DBBF646BB369Ec325a1aCD5c#code)  
DAMMFactory impl| [`0x5491E26d…816630`](https://basescan.org/address/0x5491E26dBFF761C10478e43d0D5c289CC8816630#code)  
DAMMPairBeacon| [`0xB6363F60…8569`](https://basescan.org/address/0xB6363F6091d185B431Ad51F7Cc0eF49797fa8569#code)  
DAMMPair impl| [`0x1d8752A8…3D0A`](https://basescan.org/address/0x1d8752A8533668FBC49E4721790592f3111A3D0A#code)  
DAMMPairPaused (emergency stub)| [`0xEcEc2DCB…76B22`](https://basescan.org/address/0xEcEc2DCBE4a61De7A7f8530B510a801b0EA76B22#code)  
  
### Upgrade discipline

Pair upgrades require **both** : a 48h governance timelock to propose, then a separate 48h beacon delay to land. The beacon can also be flipped to the paused stub (`0xEcEc…76B22`) for incident response -- that swap is itself behind the timelock.

### Reads
```
cast call <FACTORY> 'owner()(address)'        --rpc-url $RPC  # → U1/DAMM Timelock
cast call <FACTORY> 'pendingOwner()(address)' --rpc-url $RPC  # → 0x0 (no pending)
cast call <FACTORY> 'beacon()(address)'       --rpc-url $RPC  # → 0xB636…8569
cast call <BEACON>  'implementation()(address)' --rpc-url $RPC  # → 0x1d87…3D0A
```

# DAMM Subgraph

The Graph subgraph for the DAMM V4 constant-product DEX. Indexes pair creation, swaps, liquidity events, LP-fee + protocol-fee claims, LP lockup state, and time-series rollups.

### Schema (Uniswap V2-compatible shape)

Mirrors Uniswap V2 entity shape so aggregator adapters that already parse V2 read this cleanly.

  * `Factory` -- protocol-wide totals.
  * `Bundle` -- single ETH/USD reference price (id `"1"`). USD math reads here.
  * `Token` -- one per ERC-20 observed in any pair.
  * `Pair` -- one per DAMM pair contract. Live reserves, prices, volumes.
  * `Swap` -- one per Swap event (feeBps, USD value, direction).
  * `LiquidityEvent` -- one per Add / Remove (kind enum).
  * `FeeClaim` -- one per LP / Protocol fee claim.
  * `LPLock` -- current lock state per holder per pair.
  * `PairHourData` / `PairDayData` / `TokenDayData` -- time-series rollups.

### Notes

  * DAMM V4 LP tokens are **18-decimal**. Hardcoded in the subgraph mapping when converting `lpAmount`.
  * Fee token is **WETH** on the U1/WETH pair (per `feeToken`).
  * `Bundle.ethPriceUSD` is intentionally not populated by the subgraph for v1 -- the consuming app already has a WETH/USD feed at `/api/base/weth-usd`; USD math at the app layer multiplies the subgraph's `amountWETH` by the live rate. Add a stablecoin-paired data source to make the subgraph self-contained (deferred).
  * `tokenXPrice` reads as "WETH per U1" for the U1/WETH pair (uses `reserveY / reserveX`). Multiply by `ethPriceUSD` for USD display.

### Endpoints

Production gateway URL is issued per consumer. Local dev: `http://localhost:8000/subgraphs/name/umbrae/damm`. See Subgraph index for the cross-pool view.

# Custody keeper overview

Auto-rebalance + TP/SL orders for DLMM positions. v8 keeper-only stack (deployed 2026-06-23). Eligibility-gated: a wallet must lock an Ignis Elite NFT to use it.

### Two modes, never both on one position

The keeper does two different jobs. A single position NFT is in **exactly one** mode at a time. The UI must treat them as mutually exclusive.

Mode| Who holds the NFT| What it does| Contract the user calls  
---|---|---|---  
**A -- Auto-rebalance** (custody)| The **keeper** holds it| Keeps the position centered on the active price as it drifts| `CustodyRebalanceKeeper`  
**B -- Take-profit / Stop-loss** (non-custody)| The **user** keeps it| Closes the position when a price trigger fires| `OrderManager`  
  
### What v8 adds (over v7)

  * **Per-pool fee tracking** -- `poolAccounting(depositor, pair) → (feesX, feesY, rebalanceCount, retainedX, retainedY)` \+ the `FeesRealized` event.
  * **Retained reuse-buffer** -- `withdrawRetained(pair)` reclaims the depositor's own capital the keeper holds back across rebalances. See Withdraw retained.
  * **Fee modes** -- `setFeeMode(pair, separate)` / `separateFees(depositor, pair)` (Compound default vs Separate). See Fee modes.
  * **Cooldown off-switch** -- `minRebalanceInterval == 0` disables the per-position cooldown.

### Operational addresses (NOT called by integrators -- surface for verification)

Address| Role| Calls  
---|---|---  
`0x5986Bb…Ac387`| Keeper operator| `CustodyRebalanceKeeper.rebalance(...)`, `OrderExecutor.executeOrder(...)`  
`0x3489BD…1f6e907`| Sync operator| `EligibilityManager.setEligible(...)`, `batchSetEligible(...)`  
  
Both are whitelisted on-chain (`operators(addr)` / `syncOperators(addr)`). They are **rotatable** via the timelock -- prefer reading the mapping live over hardcoding when you need a trust check.

# Deposit (DepositorVault model)

How a user's position lands inside the keeper. v9 adds a per-depositor vault model: every depositor's NFT and LP shares live in their own deterministic CREATE2 clone.

### Three deposit paths

**First, one approval** : `positionManager.approve(<keeper>, tokenId)` -- or `setApprovalForAll` for repeat users.

The pair the position lives in must be governance-approved on the keeper (all 15 current DLMM pairs are approved at launch). If the pair isn't approved, deposit reverts -- surface as _"this pair isn't supported by the keeper yet."_

#### (a) Preset config -- easiest
```
keeper.depositWithPreset(uint256 tokenId, uint8 presetId)  // presetId 0..2
```

#### (b) Custom config -- full control
```
keeper.depositWithCustom(
  uint256 tokenId,
  Strategy strategy,        // 0=Spot, 1=Curve, 2=BidAsk
  uint24  binRange,
  uint8   bidAskRatio,
  uint24  triggerThreshold,
  uint24  idSlippage,
  bool    skipActiveBin,
  uint8   balanceCapPct,
  bool    triggerFromEdge,
  uint32  minRebalanceInterval
)
```

#### (c) One-step `safeTransferFrom` -- deposit + config in one tx, no approve
```
// preset:  data = abi.encode(uint8 presetId)             // presetId 0..2
// custom:  data = abi.encode(uint8(255), PositionConfig) // 255 = "custom follows"
positionManager.safeTransferFrom(user, <keeper>, tokenId, data)
```

### DepositorVault (v9 model)

Every depositor's NFT and LP shares live in their own deterministic `DepositorVault` clone (EIP-1167 minimal proxy + CREATE2). Same depositor always maps to the same vault address -- even before the vault is deployed. The vault is created lazily on the first deposit.
```
// Predict a depositor's vault address (always works, deploy or no deploy):
// salt   = bytes32(uint160(depositor))
// impl   = DepositorVault implementation
// deployer = keeper address
// returns the CREATE2-derived address

// Read whether the vault exists yet:
isVaultDeployed(address depositor) view returns (bool)
```

Reading LP shares

For per-bin LP, read from **the depositor's vault address** , not the keeper: `pair.balanceOf(vault, binId)`. In v9 the keeper holds nothing -- the vault is the holder.

### Change config later
```
updateConfigWithPreset(uint256 tokenId, uint8 presetId)
updateConfigWithCustom(uint256 tokenId, /* same 9 fields as depositWithCustom */ ...)
```

# Status & accounting reads

Everything an integrator needs to display a depositor's current state without a server-side index.

### Per-depositor
```
getUserTokenIds(address user) view returns (uint256[])      // all currently-held positions
getUserPositionCount(address user) view returns (uint256)
getDepositor(uint256 tokenId) view returns (address)        // 0 = not in keeper
isEnabled(uint256 tokenId) view returns (bool)              // auto-rebalance on/off
getPositionConfig(uint256 tokenId) view returns (PositionConfig)
```

### Per (depositor, pair) -- v8 fee accounting
```
poolAccounting(address depositor, address pair) view returns (
  uint256 feesX,
  uint256 feesY,
  uint256 rebalanceCount,
  uint256 retainedX,
  uint256 retainedY
)
separateFees(address depositor, address pair) view returns (bool)  // fee mode
```

For per-chain (open chains only) attribution, scan the `Rebalanced` \+ `FeesRealized` \+ `RetainedWithdrawn` events from block `47679950`; `poolAccounting` is the lifetime per-(depositor, pair) accumulator and inherits the running total across closed chains.

### Events
```
Rebalanced(uint256 indexed oldTokenId, uint256 indexed newTokenId, address indexed depositor,
           address tokenX, address tokenY, uint256 amountX, uint256 amountY,
           uint256 deployX, uint256 deployY)

FeesRealized(uint256 indexed tokenId, address indexed depositor, address indexed pair,
             uint256 feeX, uint256 feeY)

RetainedWithdrawn(address indexed depositor, address indexed pair,
                  uint256 amountX, uint256 amountY)

FeeModeSet(address indexed depositor, address indexed pair, bool separate)
```

### Presets
```
getPreset(uint8 presetId) view returns (Preset)  // 0..2; MAX_PRESET_ID = 2
```

`PositionConfig` fields: `enabled, strategy, binRange, bidAskRatio, triggerThreshold, idSlippage, skipActiveBin, balanceCapPct, triggerFromEdge, minRebalanceInterval`.

# Gas prepaid model

The keeper bot pays gas to rebalance and is reimbursed from a per-user balance you keep on the keeper. Closing a position does **not** refund it -- use `withdrawGas`.

### The flow
```
keeper.depositGas{value: ...}()           // top up (payable)
keeper.withdrawGas(uint256 amount)        // pull unused gas back
keeper.gasBalances(address owner) view returns (uint256)  // wei
```

### Surface in the UI

  * Always show the user's current gas balance.
  * Warn when low -- **a depleted balance means rebalances stop**.
  * On an _operator-funded_ revert, the operator pulls accumulated reimbursement via `claimOperatorPayment(to)`; integrators do not call this.

### Orders (TP/SL) have their own gas balance

Mode B (orders) uses a separate gas surface on `OrderExecutor`:
```
orderExecutor.depositGas{value: ...}()
orderExecutor.withdrawGas(uint256 amount)
orderExecutor.gasBalances(address owner) view returns (uint256)
```

An empty balance on a stop-loss reverts `InsufficientGasBalance` at the operator's pre-check; the order **stays armed but will not fire**. Prompt the user to fund gas in the order-setup flow.

### Recovering gas from a retired keeper

Prepaid gas is per-contract, keyed by depositor. It is **not migrated** on an upgrade. If you funded a retired keeper (pre-v8), recover with the same selectors:
```
cast call <OLD_KEEPER> "gasBalances(address)(uint256)" <YOUR_WALLET> --rpc-url https://mainnet.base.org
cast send <OLD_KEEPER> "withdrawGas(uint256)" <WEI>   --rpc-url https://mainnet.base.org --account <wallet>
```

The most-funded retired keeper as of 2026-06-24 is `0xe259EA63…d2dB54e6` (~0.227 ETH stranded), Basescan-verified so the Read/Write tabs work without tooling.

# Fee modes — Compound vs Separate

v8 lets a depositor choose what happens to realized fees on rebalance: redeploy into the new position (Compound, default) or route to the wallet on each rebalance (Separate).

### Set the mode
```
setFeeMode(address pair, bool separate)
// separate = false → Compound (default)
// separate = true  → Separate (fees pushed to wallet each rebalance)
```

### Read the mode
```
separateFees(address depositor, address pair) view returns (bool)
```

Default `false` when never set. Switching modes mid-chain is supported -- subsequent rebalances follow the new mode; previously-realized fees stay where they were (compounded or sent).

### P&L implications for indexers

  * **Compound mode** : realized fees ride inside current value. Do NOT add them again to NET P&L -- they're already in the position's reserves.
  * **Separate mode** : realized fees were pushed to the wallet. ADD them to NET P&L as "realized income."
  * The `FeesRealized` event fires in both modes -- distinguish by reading the live `separateFees` flag at the event's block (or by tracking `FeeModeSet` events).

### Event
```
FeeModeSet(address indexed depositor, address indexed pair, bool separate)
```

# Withdraw retained

v8 introduces a retained reuse-buffer: each rebalance, the keeper holds back some of the depositor's own capital instead of redeploying it. `withdrawRetained` reclaims it.

### The model

On every rebalance, the keeper computes the new position's deploy amounts and the remainder. The remainder is the depositor's own capital, **retained** in the keeper for reuse on the next rebalance. It is always reclaimable on demand -- nothing in the system can lock it away.

### Read
```
// per-(depositor, pair) lifetime retained (uint256, base units)
poolAccounting(depositor, pair) view returns (..., uint256 retainedX, uint256 retainedY)
```

### Withdraw
```
withdrawRetained(address pair) external
// Fully drains retainedX/retainedY for (msg.sender, pair) in one tx.
// Pair-wide drain: NOT per-position. Emits RetainedWithdrawn.
// No partial withdrawal API.
```

### Event
```
RetainedWithdrawn(address indexed depositor, address indexed pair,
                  uint256 amountX, uint256 amountY)
```

Per-chain attribution requires a scan

`poolAccounting.retainedX/Y` is the per-(depositor, pair) **lifetime** accumulator. A brand-new chain (fresh deposit in a pair where the depositor had prior keeper history) inherits the running total. For per-open-chain attribution: scan `Rebalanced` events (retained delta = `amountX - deployX`, `amountY - deployY` per event) plus `RetainedWithdrawn` as a pair-wide barrier, attributing each delta to the chain its `newTokenId` belongs to (root via the `Rebalanced` lineage).

# Exit (claimWithdrawal flow)

Withdraw a custody position out of the keeper. Always available, even while the keeper is paused.

### Withdraw the position
```
keeper.withdrawPosition(uint256 tokenId)
// Returns the NFT to the original depositor.
// Reverts if msg.sender is not the depositor.
```

### Escrow fallback

If the depositor's address cannot receive a payout token (e.g. USDC/USDT blocklist, a contract that rejects the token), the payout is escrowed on the keeper instead of reverting the withdraw. Pull it from a fresh address:
```
keeper.claimWithdrawal(address token, address to)
// Call from the depositor's address; routes the escrowed token to `to`.
```

### Auto-drain retained on last close

Closing the depositor's **last** custody position in a pair automatically drains retained for that pair -- no separate `withdrawRetained` call needed. If the user has other custody positions in the same pair, retained stays in place.

# Orders — TP / SL

Mode B: the user keeps their NFT and authorizes the OrderExecutor to close it when a price trigger fires. Bin-based stop-loss and take-profit only -- fee-target orders are unavailable on the current PM.

### Two approvals (both required)

A missing second approval makes the SL silently not fire

`setOrder` fails closed unless both are in place. Prompt for both in the order-setup flow. Read `pairAddress` from the position via the Position NFT Manager.
```
// 1. Let the executor close (transfer/burn) the position NFT:
positionManager.setApprovalForAll(<orderExecutor>, true)
// or: positionManager.approve(<orderExecutor>, tokenId)

// 2. Let the position manager burn the pair's LP shares on close (ERC-1155):
IUmbraeLBToken(pairAddress).setApprovalForAll(<positionManager>, true)
```

If #1 is missing, `setOrder` reverts `ExecutorNotApproved`. If #2 is missing, it reverts `PairNotApproved(pair)`.

### Set the order
```
struct SetOrderParams {
  uint256 tokenId;
  uint256 feeTargetX;       // 0 = disabled
  uint256 feeTargetY;       // 0 = disabled
  uint24  slBinId;          // 0 = disabled
  uint24  tpBinId;          // 0 = disabled
  uint128 minAmountX;       // min output on close (0 = none)
  uint128 minAmountY;       // 0 = none
  uint256 minValueFloor;    // combined-value floor on close (0 = disabled)
  uint8   valueDenomToken;  // 0 = value in tokenX, 1 = tokenY
}
orderManager.setOrder(SetOrderParams calldata params)
```

**Use`minValueFloor`** as the crash-resistant floor. In a sharp drop a per-token `minAmount` on the wrong side becomes unsatisfiable exactly when the stop-loss needs to fire; the value floor avoids that.

### Status & cancel
```
orderExecutor.checkOrder(uint256 tokenId) view returns (
  bool triggered, TriggerType triggerType, uint256 feeX, uint256 feeY
)
// TriggerType: 0 = FeeTarget, 1 = BinStopLoss, 2 = BinTakeProfit

orderManager.cancelOrder(uint256 tokenId)              // owner-only
orderManager.getOrder(uint256 tokenId) view returns (Order)
orderManager.isActive(uint256 tokenId) view returns (bool)
```

### Width limit (OE-01)

A position spanning more than **41 price bins** cannot be order-closed (reverts `TooManyBins`). Surface _"split this position"_ if a user tries to set an order on a wider one.

### Gas (owner-funded)

Orders are owner-funded for gas (KEEPER-054). Prompt the user to call `orderExecutor.depositGas{value: ...}()` in the setup flow. See Gas prepaid model.

# NFT Lock Vault (Ethereum)

Locks an Ignis Elite NFT on Ethereum for a chosen window (7 / 14 / 30 days). While the lock is active and not expired, the user's address is "eligible" for keeper service + platform rewards on Base.

Address

[0x7c0A392f…110D3C](https://etherscan.io/address/0x7c0A392fA177E5019FFd47f3daD5982BB1110D3C#code)

Chain

Ethereum mainnet

Lock windows

7 / 14 / 30 days

### What eligibility unlocks (on Base)

  * **Keeper auto-rebalance** -- gated on `EligibilityManager.isEligible(addr)` on Base.
  * **Platform-reward eligibility** -- the 35% Elite-holder share of platform fees and the 35% Elite share of arb-bot profits.

### Approved NFTs (constructor-set)

Phase| Contract| Token ID  
---|---|---  
Phase 1| [`0x42347DB4…6885b`](https://etherscan.io/address/0x42347DB440ef412BBE19C0841895A4b98256885b)| 1  
Phase 2| [`0x17A2b200…1712Ce`](https://etherscan.io/address/0x17A2b200CeC625b431C3AE7334D2d8dDB41712Ce)| 0  
Phase 3| [`0xab505a66…FeD1A`](https://etherscan.io/address/0xab505a667039d08d8E33cef95C81897A8B5fEd1A)| 0  
  
The owner can add or remove approved NFTs. Removing a contract blocks new locks against it; existing locks remain valid.

### Surface
```
lock(address nftContract, uint256 tokenId, uint8 durationDays)  // 7 / 14 / 30
extendLock(uint8 newDurationDays)                                // monotonic
unlock()                                                          // always works, even when paused
isApproved(address nftContract, uint256 tokenId) view returns (bool)
lockOf(address holder) view returns (uint256 tokenId, uint8 duration, uint64 unlockTime)
```

### Sync to Base

A backend sync service watches the vault on Ethereum and mirrors eligibility onto Base's EligibilityManager. Expect ~5 min from lock → eligibility on Base.

# Eligibility Manager

Gates keeper **service** -- not assets. An ineligible wallet can still deposit and can **always** withdraw; it just won't get rebalanced or have orders fire.

Address

[0x6AE6faf6…BD4102](https://basescan.org/address/0x6AE6faf6F82cf2ccE8e40bF473c2000D1eBD4102)

### Reads
```
isEligible(address user) view returns (bool)
graceUntil(address user) view returns (uint64)   // 0 = no grace; nonzero = grace timestamp
syncOperators(address writer) view returns (bool)
```

### Writes (depositor-facing)
```
renounceEligibility()  // self opt-out; clears grace on re-grant
```

### Writes (operator-only)
```
setEligible(address user, bool eligible)         // sync operator
batchSetEligible(address[] users, bool[] flags)  // sync operator
```

### UI rules

  * **Gate "enable" actions on`isEligible`** -- the call-to-action that turns on auto-rebalance or sets an order.
  * **Never gate withdraws** on eligibility.
  * **Explain the sync delay** -- "eligibility updates within a few minutes of locking your NFT on Ethereum."

### Events
```
EligibilityUpdated(address indexed user, bool eligible, uint64 graceUntil)
SyncOperatorUpdated(address indexed writer, bool approved)
```

# Subgraph index

Umbrae publishes two subgraphs -- one per trading stack -- on The Graph hosted gateway. They're the primary data source for pair listings, position history, and chart rollups.

Subgraph| What it indexes| Page  
---|---|---  
**umbrae-dlmm-base**|  DLMM Factory / Pair / PositionNFTManager / LBToken events -- pairs, positions, swaps, hourly/daily snapshots| DLMM Subgraph  
**umbrae-damm-base**|  DAMM Factory / Pair events -- Uniswap V2-compatible schema for aggregator parity| DAMM Subgraph  
  
### When to use the subgraph vs RPC

Use case| Recommended source  
---|---  
Live state (active bin, reserves right now)| RPC + pair viewer  
Historical swaps / volume / fees| Subgraph  
User position list| Subgraph (or PositionNFTManager + RPC)  
Charts (hourly / daily rollups)| Subgraph  
USD-denominated TVL / volume| Subgraph token amounts × your USD price feed  
  
### Endpoint policy

Each consumer uses their own gateway API key. Do not share a URL with the Umbrae key in the path -- that puts query load on our quota. Reach out for issuance.

# Listings & data partners

External aggregators that index Umbrae pools.

### Indexing status

Service| Status| Notes  
---|---|---  
GeckoTerminal| Pending submission| Submission is manual; auto-discovery does not index our custom factory + Swap event signature.  
DEX Screener| Pending submission| Same auto-discovery limitation.  
DEXTools| Pending submission| Same.  
  
### What we provide to listings

  * Subgraph endpoint (Uniswap V2-compatible shape for DAMM, custom for DLMM).
  * Verified source on Basescan for every contract.
  * Token metadata (logos, descriptions, supply audit links).

### Listing-data conventions

  * **U1 token contract** -- `0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3E`.
  * **Primary pair** -- DAMM V4 U1/WETH at `0x296964C34a571fCf85d3F74FB815ee871F5A08d4`.
  * **Secondary pair** -- DLMM U1/WETH at `0xd288889ef25ca7483277f39c262098f6ba615ff2` (100 bps, V2 stack).
  * USD price source for U1 = `(WETH per U1 from the pool) × (WETH/USD)`. Use `/api/base/weth-usd` or your own oracle for WETH/USD.

# Aggregator adapter pattern

How to surface Umbrae's pools through 1inch / OpenOcean / Matcha / Paraswap / KyberSwap. The pattern below is verified against the KyberSwap dex-lib adapter Umbrae built for upstream submission.

### Spec at a glance

Surface| Use  
---|---  
Subgraph| Pool discovery + reserves snapshot.  
Pair Viewer (DLMM)| Per-bin reads for accurate quotes on partially-filled swaps.  
DAMM Pair direct| `getAmountOut` \+ `getReserves` for one-shot quotes.  
Router (DLMM / DAMM)| Submit the swap with `amountOutMin` at the aggregator's tolerance.  
  
### Adapter responsibilities

  1. **Discover pools** -- query the subgraph for active pairs on each stack. Skip pairs with no liquidity.
  2. **Reserve / bin state** -- for DAMM, single `getReserves` call per pair. For DLMM, multicall `getBin(uint24)` across the active bin and its 2-3 neighbors (the bins a small swap would touch).
  3. **Quote** -- DAMM: `getAmountOut(amountIn, xToY)`. DLMM: run the local bin-by-bin quote (fee + impact-aware) using the bin reserves and the pair's static + variable fee parameters.
  4. **Encode the swap** -- emit the call the aggregator's swap executor will perform: `pair.swap(...)` for DAMM, `router.swapExactTokensForTokens(...)` for DLMM multi-hop.
  5. **Verify on PROD before announcing** -- integrate against the live mainnet pair. Test pool listings exist before publishing the integration.

### Test fixtures

Use the production mainnet pool for adapter validation -- `0xd288889ef25ca7483277f39c262098f6ba615ff2` (DLMM U1/WETH, 100 bps, V2 stack) and `0x296964C34a571fCf85d3F74FB815ee871F5A08d4` (DAMM U1/WETH). Quote a small amount (~0.001 WETH) and compare your adapter's quote to `pair.getAmountOut` -- they should match to the unit.

# Source repos

Where the code lives. All public-facing contracts are source-verified on Basescan; the repositories below host the canonical Solidity, scripts, and ABIs.

Repo| What  
---|---  
[github.com/Ignis-AI-Labs](https://github.com/Ignis-AI-Labs)| The Ignis AI Labs organization -- entry point.  
Ignis-AI-Labs/umbrae| Trading platform frontend + indexer + bots. Hosts these dev docs.  
Ignis-AI-Labs/umbrae-site| Public umbrae.io site. FAQ + landing.  
Ignis-AI-Labs/evm-contracts| All deployed Solidity: DLMM, DAMM V4, U1 + lock vault, custody keeper stack, NFT lock vault.  
  
### Per-subproject highlights (inside `evm-contracts`)

  * `base/dlmm` -- DLMM V2 pair, factory, router, position NFT manager, beacon, libraries.
  * `base/dlmm/src/keepers` -- CustodyRebalanceKeeper, DepositorVault, OrderManager, OrderExecutor, EligibilityManager, KeeperMath.
  * `base/damm` -- DAMM V4 pair, factory, beacon, router.
  * `base/u1` -- U1 token, migration, reserve LP, lock vault.
  * `base/dlmm/subgraph`, `base/damm/subgraph` -- subgraph mappings.
  * `ethereum/nft-lock-vault` -- the eligibility-gating vault on Ethereum mainnet.

### Licensing

  * **Custody keeper stack (v5+)** -- `BUSL-1.1`. Pre-v5 was MIT.
  * **DLMM core** -- MIT.
  * **DAMM V4** -- see contract source for the canonical license header.

# Audits

Every Umbrae contract on Base is source-verified on Basescan and has been audited or reviewed before deployment. Public audit reports are published when redaction policy permits.

### Audit history (summary)

Date| Scope| Outcome  
---|---|---  
2026-05-12| U1TokenLockVault| Signed off -- 39/39 tests, no Critical/High.  
2026-05-16| Comprehensive (BlackBox)| Multi-contract audit. Findings folded into the deploy pipeline; CSP report-only telemetry shipped 2026-05-18.  
2026-06-09| Custody keeper adversarial review| Four findings (grace-timestamp opt-out bypass, executor PM-interface check, escrow fallback, interface-gated withdrawPosition) addressed in v5 (BUSL-1.1 cutover).  
2026-06-16| v6 + v7 keeper redeploy| Full KA3 swap-safety + KEEPER-054 owner-funded order gas + OE-01/OE-02 + KEEPER-055 (KeeperMath extraction).  
2026-06-23| v8 keeper redeploy| Per-pool fee accounting + retained reuse-buffer + Compound/Separate fee mode + cooldown off-switch.  
  
### How to verify on-chain

Every active contract address in the Address Book links to its Basescan `#code` page -- verified Solidity source, optimizer settings, license header. Source verification is the strongest public artifact for any contract on Base.

### Reach the team

For audit report PDFs and detailed findings, contact the team through the [FAQ contact channels](https://www.umbrae.io/faq/). Public reports will be linked from this page as they're published.

# Changelog

Cutover dates, deprecations, and version bumps. Use this to align integrations with the active stack.

### Custody keeper

Version| Date| Change  
---|---|---  
**v8** (active)| 2026-06-23| Keeper + KeeperMath redeploy. Per-pool fee accounting (`poolAccounting` \+ `FeesRealized`), `withdrawRetained`, Compound/Separate fee mode (`setFeeMode`/`separateFees`), cooldown off-switch (`minRebalanceInterval == 0`). Peripherals (OrderManager / OrderExecutor / EligibilityManager / UmbraeSwapRouter / PositionNFTManager) reused from v7 -- eligibility and existing TP/SL orders preserved.  
v7| 2026-06-16| Raised max position width to ±50 bins (101 total). `MAX_BIN_RANGE` 20→50, gas estimates 10M→30M. Replaced by v8.  
v6| 2026-06-16| Full KA3 swap-safety + KEEPER-054 owner-funded order gas + OE-01/OE-02 + KEEPER-055. Same-day replaced by v7.  
v5| 2026-06-10| First BUSL-1.1 keeper. KEEPER-052 stack + 2026-06-09 audit fixes (grace-timestamp, executor PM-interface, escrow fallback, withdrawPosition).  
v4 and earlier| 2026-06-01 to 2026-06-08| Pre-BUSL deploys. Retired. See Gas prepaid model § Recovering gas for funds recovery on retired keepers.  
  
### DAMM

Version| Date| Change  
---|---|---  
**V4** (active)| 2026-05-07| Carries V-15 / V-16 / V-18 / V-19 audit fixes in deployed bytecode. BeaconProxy pattern.  
V3| 2026-05-07| Retired same day. Audit re-review surfaced findings folded into V4.  
V2 / V1| 2026-05-03| Retired. Pre-upgradeable era.  
  
### DLMM

Date| Change  
---|---  
2026-05-15| U1/WETH pair added (25 bps) -- verified via `factory.allPairs(13)`.  
  
### U1 token

Date| Change  
---|---  
≈ June 2026 (unix `1780388389`)| **Migration window closes for new entitlements.** Entitled-but-unclaimed entries stay claimable forever -- the close affects entitlement creation only.  
  
### Developer docs

Date| Change  
---|---  
2026-06-29| Phase 1 ships: Quickstart, Network, Address Book, ABIs, Deploy blocks at `/dev/`.  
2026-06-29| Phase 2-5 ships: every contract page, off-chain integration, resources.  
2026-10-23| Agents: agent sign-up, the agent API v1 (`/api/v1/{chain}/…`) in OpenAPI 3.1, the approvals endpoint, the agent quickstart, and the authentication section rewritten to the current wallet sign-in and API-key flow. This page is also published as markdown at `/docs/dev/index.md`.
