DEVELOPER DOCS
No sections match that search. Try router, address, abi, quickstart.
Dev / Get started / Quickstart

Quickstart

Connect to Base, read one contract, and send your first transaction. Five minutes end-to-end.

For integrators

Every Umbrae contract on Base is source-verified on Basescan. Every ABI on this site is byte-identical to the deployed implementation. No external indexer or aggregator dependency is required to integrate.

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 (BeaconProxy at 0x697B…55A0) for the active bin ID:

cast call 0x697B72320656e6Dc60Db7A4Bfb95084C9D9C55A0 \
  '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.
Dev / Get started / Network & RPC

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

RPC endpoints

EndpointUse
https://mainnet.base.orgPublic RPC. Rate-limited — fine for development, not for production.
Alchemy / Infura / QuickNodeProduction. Most providers offer Base in their default plan.

WETH on Base

The canonical wrapped-ETH contract is at 0x4200000000000000000000000000000000000006. Every Umbrae pair on Base uses this WETH as the Y side — never a wrapped-asset bridge or a synthetic.

Dev / Get started / Authentication

Authentication & sessions

Only relevant if you're hitting Umbrae's authenticated HTTP API. Pure on-chain integration (reading contracts, signing transactions) needs nothing in this section.

On-chain reads = no auth

Every contract on Umbrae is public on Base. Reading state, scanning logs, and submitting wallet-signed transactions never requires an Umbrae session.

When authentication applies

Authentication kicks in only for HTTP endpoints under /api/ that serve pre-computed aggregates (positions list, P&L summary, fee receipts) drawn from Umbrae's own indexer + cache. Those routes are session-gated to deny them as an unauthenticated rate-cost lever.

Sign-in flow

Sign-in is wallet-signature based: the client requests a nonce, signs it with the connected wallet (EVM or Solana), and POSTs the signature back. On success the server returns a session token in an HTTP-only cookie. Subsequent requests carry the cookie.

Session token shape

  • Sessions are stored server-side in auth_sessions with hashed tokens.
  • Each request validates: token exists, not expired, wallet matches.
  • Expired sessions are auto-deleted; the next request returns 401 and the client re-signs.
  • API keys (prefixed umbr_) are an alternative for automated integrations — reach out to the team for issuance.

Public HTTP API (coming soon)

The first-party HTTP API surface is currently scoped to internal use. A stable public surface for third-party integrators is on the roadmap — this page will be updated when it ships.

Dev / Reference / Address Book

Address Book

Every Umbrae contract on Base mainnet, plus the Ethereum-side NFT Lock Vault. Last verified 2026-06-23 (custody keeper stack v8); 2026-06-08 (DLMM / DAMM / token stacks).

Source of truth

Addresses here are mirrored from docs/infrastructure/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

NameAddressState
U1Token
Umbrae One · U1
0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3ESealed supply 5,000,000 × 1018. 18 decimals. No mint, no burn, no owner.
U1Migration0xb374f3bC4085dE6675AF44Fa81472A93402ad467Owner = TimelockController. Window OPEN until unix 1780388389 (≈ June 2026). 72h delay between entitlement set and claim().
U1ReserveLP0x9bb9961059f90041672AdE075450DDA398310fBdHolds 850,000 U1. Only outbound op: depositIntoLP to the DAMM pool.

Rewards

NameAddressState
U1TokenLockVault0xf771F202e8B49612e83f18B68D6b268765A40F725% protocol-fee payout vault for U1 lockers. Reward token = WETH. MIN_LOCK_DURATION = 7 days. Owner = U1/DAMM Timelock.

DLMM trading stack

NameAddressState
DLMM Router (proxy)0x4965DD6877ca9DE77caca2f57996651e7AF23c93Swaps + multi-hop. Owner = DLMM Timelock. UUPS upgradeable.
DLMM Factory0x17Da44dcbdffD8c715be7A368E19F252C2940FeeDeploys pairs as BeaconProxies. Owner = DLMM Timelock.
DLMM Beacon0xa1BF1667bE1Dab835E224CDA666728f3e5194496Holds pair-implementation pointer. Owner = Ops multisig.
Position NFT Manager (proxy)0x1a49ab1d93C269D22E98a8786823526813b04bD6ERC-721. Umbrae DLMM Position / UMBRAE-POS.
Pair Viewer (proxy)0xbA3A5400A95b055b1412e18ad1978EdF32Fc3F05Read-only helpers (getActiveBin, etc.).
Position NFT Viewer (proxy)0x13302a890EaA061f6015d8ab2c08FaB28b1C5538Read-only helpers for NFT position state.

DLMM implementation contracts (transparent — read source, never call directly)

NameAddress
Pair implementation0x76c325266E38fC92DBfE244876bA66343B00Ab47
Pair extension0x17fC15501F2b2B1Fb384608ed33CDd153080BA27
Paused implementation (emergency stub)0x322D09c5EC1e5CaE81D937Ef8f4d8FCFeC5d65F7
Router implementation0xba942766D3939ED6247b0C2330eaD5170055Bd81
Position NFT Manager impl0x2007e2a781C13D3E2cca730e5ca9AB72729A9125
Pair Viewer impl0xEFB267be7Ad049ba879b48B8B87dA9146de4981F
Position NFT Viewer impl0xa6fe598C972A344F1aba24f902e58ABA830cD019

DLMM live pairs

PairAddressbinStepState
U1 / WETH0x697B72320656e6Dc60Db7A4Bfb95084C9D9C55A025 bpstokenX = U1, tokenY = WETH. Verified via factory.allPairs(13).

For programmatic enumeration of every pair, query factory.allPairsLength() + factory.allPairs(i) directly.

Custody keeper stack (v8)

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.

ContractAddress
CustodyRebalanceKeeper (v8)0xa98D5e73D890A0A5C1db478a456F8d29B1581fE8
OrderManager0xA6861fD7a51f479dF84A300721222d350fFeC8c9
OrderExecutor0x8d28ce16cBf19F987A72B531b75756AA5f15C00e
EligibilityManager0x6AE6faf6F82cf2ccE8e40bF473c2000D1eBD4102
UmbraeSwapRouter0xA8F5f4DbdadD1de15fb9A77E0e53950a70186199
KeeperMath (linked library, v8)0x5B31C45d80EBD1b589AC5DE0f94755Ced3383275

DAMM V4 trading stack

NameAddress
DAMMPair (U1/WETH, DAMM U1-WETH LP)0x296964C34a571fCf85d3F74FB815ee871F5A08d4
DAMMFactory (proxy, ERC-1967)0xD14322b444415d78DBBF646BB369Ec325a1aCD5c
DAMMRouter0x71225FC9E1c522dC33E8f7CFA3E8dBCf2F3dF205
DAMMPairBeacon0xB6363F6091d185B431Ad51F7Cc0eF49797fa8569
DAMMPair impl0x1d8752A8533668FBC49E4721790592f3111A3D0A
DAMMPairPaused (emergency stub)0xEcEc2DCBE4a61De7A7f8530B510a801b0EA76B22
DAMMFactory impl0x5491E26dBFF761C10478e43d0D5c289CC8816630

Governance

RoleAddressDelay
U1 / DAMM Timelock0xC915dA2847A0640339D0071a0f92cC1562f8Eee448h
DLMM Timelock0x8c1Ab74D8FDE9d5B860896da2A1057d59513E2CF10 min

External canonical

NameAddressNotes
WETH (Base canonical)0x4200000000000000000000000000000000000006Wrapped ETH on Base. The Y side of every Umbrae pair.

Ethereum mainnet

NameAddressState
IgnisNFTLockVault0x7c0A392fA177E5019FFd47f3daD5982BB1110D3CLocks an Ignis Elite NFT for 7/14/30 days. Eligibility for keeper access + platform rewards.
Dev / Reference / ABIs

ABIs

Every ABI here is the ABI of what's actually deployed — pulled from verified source on Basescan. For a proxy, the ABI is the implementation's (the one you bind 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).

DLMM core + custody keeper

ContractAddressStatusABI
DLMMPair0x76c32526…00Ab47live-implDLMMPair.json
PositionNFTManager0x1a49ab1d…b04bD6live-implPositionNFTManager.json
CustodyRebalanceKeeper0xa98D5e73…1581fE8live (v8)CustodyRebalanceKeeper.json
DepositorVaultper-depositor clonelive (impl)DepositorVault.json
OrderManager0xA6861fD7…0fFeC8c9liveOrderManager.json
OrderExecutor0x8d28ce16…f15C00eliveOrderExecutor.json
EligibilityManager0x6AE6faf6…BD4102liveEligibilityManager.json
UmbraeSwapRouter0xA8F5f4Db…186199liveUmbraeSwapRouter.json
KeeperMath0x5B31C45d…3383275live (linked lib)KeeperMath.json
FeeRouteraddress publishing soonpre-deployFeeRouter.json
Bind at the proxy, not the implementation

For any live-impl contract above: 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.

Dev / Reference / Deploy blocks & explorer

Deploy blocks & explorer

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

Custody keeper stack v8 (active)

ContractDeploy blockNotes
CustodyRebalanceKeeper47679950v8 deploy 2026-06-23. Earliest block to scan for Rebalanced, FeesRealized, RetainedWithdrawn events.
KeeperMath (linked lib)47679950v8 redeploy alongside the keeper.

DLMM pairs

PairNotes
U1 / WETHAdded 2026-05-15 (verified via factory.allPairs(13)).

Block explorer

  • Basescanbasescan.org. Every contract on this site links to its #code page for verified source.
  • Etherscanetherscan.io, used only for the Ethereum-side NFT Lock Vault.
Dev / Tokens / U1 token

U1 token

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

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
Dev / Tokens / U1 lock vault

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.

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.
Dev / Tokens / Migration

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

StateDetection
No entitlemententitlementOf(user) == 0
Entitled, not yet maturedentitlement > 0 && now < claimableAt — show countdown
Claimable nowisClaimable(user) == true

Custom errors

NoEntitlement()
ClaimNotYetAvailable(uint256 claimableAt)
WindowAlreadyClosed()
Dev / DLMM / Overview

DLMM overview & architecture

Umbrae's concentrated-liquidity DEX. Bin-based AMM with ERC-721 LP positions, beacon-proxy upgrade pattern, dual-layer pause.

Mental model

  • Bins — the price range is divided into discrete bins, each holding liquidity at a single fixed price.
  • Active bin — the bin the next swap will draw from. Swaps move across bins as one fills.
  • 200–300x capital efficiency vs constant-product, because LPs choose the bin range they care about.
  • Positions are ERC-721 NFTs minted by PositionNFTManager — one NFT per (pair, owner, range).

Stack

UmbraeLBFactoryV2  ──creates──▶  BeaconProxy pairs  ──impl pointer──▶  UmbraeLBPairBeacon (48h timelock)
                                                                          └─ UmbraeLBPairUpgradeable (impl)
UmbraeLBRouterUpgradeable (UUPS, governed by UmbraeTimelock)
PositionNFTManager (ERC-721 wrapper around LP shares; Spot / Curve / BidAsk strategies)
UmbraeLBTokenUpgradeable (ERC-1155-like fungible LP shares per bin)

Bin math

Bins are anchored at 8388608 (223) = 1:1 price. The price for bin id at binStep bps:

price = (1 + binStep/10000)^(id - 8388608)
// binStep=25  →  bin 8388609 = 1.0025  (+0.25%)
// binStep=25  →  bin 8388607 = 0.9975  (-0.25%)

Token X vs Y

Address orderingtokenX < tokenY. Lower address = X. Always consistent for the same pair. The U1/WETH pair has tokenX = U1, tokenY = WETH because U1's address sorts lower.

Fee distribution

  • LPs get 90% — auto-compounded into bin reserves.
  • Protocol gets 10% — accumulates in _protocolFeesX/Y, swept via collectProtocolFees.

Security model

  • All state-changing functions are nonReentrant: swap, mint, burn, flashLoan.
  • Dual timelock: pair beacon (48h, ops multisig) + governance (10 min, DLMM timelock).
  • Balance-based accountingswap detects input via balance change, no amountIn parameter. Prevents front-running and handles fee-on-transfer tokens.
  • Pause blocks state mutations but still allows views and fee collection.
Dev / DLMM / Router

DLMM Router

Multi-hop swaps and liquidity management for the DLMM stack. UUPS-upgradeable, governed by the DLMM timelock.

ABI
DLMMPair.json + router impl

Swap quote (read)

Quote through the pair: read reserves + active bin + bin step, run the local quote, compute minAmountOut with your slippage tolerance, then submit through the router.

// Pair-direct quote: cheapest path
cast call <PAIR> 'getActiveId()(uint24)' --rpc-url $RPC
cast call <PAIR> 'getBin(uint24)(uint128,uint128)' <activeId> --rpc-url $RPC

// Router multi-hop: pass a path of pairs
swapExactTokensForTokens(
  uint256 amountIn,
  uint256 amountOutMin,
  uint256[] pairBinSteps,         // bin step per hop, matches pairs[] order
  address[] tokenPath,            // [tokenIn, ..., tokenOut]
  address to,
  uint256 deadline
)

Submit the swap

  1. Approve the input token to the router (skip if allowance already covers).
  2. Call swapExactTokensForTokens(...) with amountOutMin = quote × (10000 - slippageBps) / 10000.
  3. Decode revert reasons: SlippageExceeded(), InsufficientOutput(), InsufficientLiquidity().

Liquidity management

The router also exposes addLiquidity, removeLiquidity, swapNFTForTokens (close a position to tokens), and the ERC-1155 LP token approval helpers. See Position NFT Manager for the NFT-level surface.

Slippage guidance

Default 1% slippage for trades under $500, 2% for trades up to $5K, force a confirm dialog above that. The router enforces no deadline beyond the one you pass; pick something short (60–120 s) to limit MEV exposure.

Dev / DLMM / Factory

DLMM Factory

Deploys pairs as BeaconProxies. Owned by the DLMM Timelock. Source of truth for "what pairs exist."

Enumerate every pair

cast call <FACTORY> 'allPairsLength()(uint256)' --rpc-url $RPC
# returns N

# loop i in 0..N-1:
cast call <FACTORY> 'allPairs(uint256)(address)' <i> --rpc-url $RPC

Look up a specific pair

getPair(address tokenA, address tokenB, uint16 binStep) view returns (address pair)
// tokenA / tokenB are NOT required to be sorted — factory normalizes internally.

Token verification tiers

The factory enforces tiered verification on pair creation:

PARTNER          (3) — fully audited partner tokens
VERIFIED_UTILITY (2) — verified utility tokens
REVIEWED         (1) — basic review passed
UNVERIFIED       (0) — no verification

A pair is created only when both tokens meet the minimum tier policy (governance-set). Use getTokenVerification(token) to read a token's tier.

Pair-impl upgrade trust note

The beacon is owned by the ops multisig directly, not the DLMM Timelock — an asymmetry vs DAMM V4. Pair-implementation upgrades on DLMM do not have a contract-level timelock. Verify the current pair impl with:

cast call <BEACON> 'implementation()(address)' --rpc-url $RPC
# → 0x76c325266E38fC92DBfE244876bA66343B00Ab47 (current)
Dev / DLMM / Pair

DLMM Pair (BeaconProxy)

The bin-based pair contract. Every DLMM pair is a BeaconProxy at a unique address pointing at the current pair implementation.

Bind at the proxy

Pairs are BeaconProxies (e.g. U1/WETH at 0x697B…55A0). Bind DLMMPair.json at the proxy address — never call the implementation 0x76c3…Ab47 directly (no state).

Reads — pool state

getActiveId()         view returns (uint24)
getBin(uint24 id)     view returns (uint128 reserveX, uint128 reserveY)
getReserves()         view returns (uint128 reserveX, uint128 reserveY)
getFactory()          view returns (address)
getTokenX()           view returns (address)
getTokenY()           view returns (address)
getBinStep()          view returns (uint16)
Bin reserves ≠ getReserves

getReserves() returns the pair's totals across all bins. For per-bin liquidity (the right primitive for indexers and chart math) use getBin(uint24) via multicall across the bin range you care about. The pair's getReserves is a coarse aggregate and does not split per active bin.

Reads — oracle + fees

getOracleParameters() view returns (...)             // TWAP state
getProtocolFees()     view returns (uint128 X, uint128 Y)
getStaticFeeParameters() view returns (...)          // base fee, max fee, filter, decay
getVariableFeeParameters() view returns (...)        // volatility-driven multiplier

Writes — swap, mint, burn, flash

swap(bool swapForY, address to)  external returns (bytes32 amountsOut)
// NOTE: swap has NO amountIn parameter. Send tokens FIRST, then call swap.
// The contract reads the balance delta — see "balance-based accounting" in the Overview.

mint(address to, bytes32[] liquidityConfigs, address refundTo)
  external returns (bytes32 amountsReceived, bytes32 amountsLeft, uint256[] liquidityMinted)

burn(address from, address to, uint256[] ids, uint256[] amountsToBurn)
  external returns (bytes32[] amounts)

flashLoan(IFlashLoanBorrower borrower, bytes32 amounts, bytes data) external
// 0.05% fee. Borrower MUST repay in the same tx via the callback.

Events to index

Swap(address sender, address to, uint24 id, bytes32 amountsIn, bytes32 amountsOut,
     uint24 volatilityAccumulator, bytes32 totalFees, bytes32 protocolFees)
DepositedToBins(address sender, address to, uint256[] ids, bytes32[] amounts)
WithdrawnFromBins(address sender, address to, uint256[] ids, bytes32[] amounts)
ProtocolFeesCollected(address recipient, bytes32 amounts)
FlashLoan(address sender, IFlashLoanBorrower receiver, uint24 activeId,
          bytes32 amounts, bytes32 totalFees, bytes32 protocolFees)

Common errors

LBPair__InsufficientAmountIn()
LBPair__InsufficientAmountOut()
LBPair__InsufficientLiquidityForSwap()
LBPair__ZeroBorrowAmount()       // flash loan
ReentrancyGuard__ReentrantCall() // calling pair fns from a flash-loan callback
Dev / DLMM / Pair Viewer

DLMM Pair Viewer

Read-only helpers that bundle common pair reads into single calls. Use this surface for indexers and frontends — it's cheaper than N round-trips to the pair.

Why use it

The Pair Viewer is the right surface for indexers — it groups commonly-needed reads (bin ranges, active state, fee parameters) into single helpers so you don't issue N RPC calls per pair per block. Pure view functions; no state.

Typical helpers

  • Multi-bin read — fetch reserves for a contiguous bin range in one call.
  • Active-bin context — getActiveId + spot price + the 2–3 neighboring bins.
  • Position summaries — given a bin range, return total LP shares + token amounts.

Bind PairViewer ABI at the proxy above. Function names match the implementation source on Basescan.

Dev / DLMM / Position NFT Manager

Position NFT Manager

ERC-721 wrapper around DLMM LP shares. One NFT per position; each NFT references a pair + bin range + strategy.

Name / Symbol
Umbrae DLMM Position · UMBRAE-POS

Lifecycle

  1. Mint — deposit tokens into a pair across a bin range; receive an ERC-721.
  2. Increase / decrease liquidity — add or remove from the same position.
  3. Collect — pull accumulated fees in tokenX/Y.
  4. Burn — close the position fully; tokens return to the owner.

Strategy presets

StrategyValueLiquidity shape
Spot0Equal weight across every bin in the range.
Curve1Concentrated near the active bin (Gaussian-like). Higher fees when in range, more IL on a strong move.
BidAsk2Weighted to the edges of the range. Best for ranging markets.

Core functions

mint(MintParams calldata params)              returns (uint256 tokenId, ...)
// MintParams: pair, recipient, strategy, lowerBinId, upperBinId, amountX, amountY, ...

increaseLiquidity(uint256 tokenId, bytes32[] liquidityConfigs)
decreaseLiquidity(uint256 tokenId, bytes32[] liquidityConfigs)
collect(uint256 tokenId) external returns (uint256 amountX, uint256 amountY)

getPosition(uint256 tokenId) view returns (Position memory)
// Position: pairAddress, binIds[], strategy, owner, ...

Approvals for custody / orders

Two approvals are required if you want the custody keeper or the order executor to act on your position:

// 1. Allow the consumer to transfer/burn the NFT
positionManager.setApprovalForAll(<consumer>, true)
// or, single-position:
positionManager.approve(<consumer>, tokenId)

// 2. Allow the position manager to burn the pair's LP shares (ERC-1155) on close
IUmbraeLBToken(pairAddress).setApprovalForAll(positionManager, true)

If #2 is missing, setOrder reverts PairNotApproved(pair). See Orders.

Dev / DLMM / Position NFT Viewer

Position NFT Viewer

Read-only helpers for NFT position state. Use this surface to build portfolio views without making one RPC call per position field.

What it returns

  • Position metadata (pair, range, strategy, owner).
  • Current token amounts derived from bin reserves + the position's LP share per bin.
  • Unclaimed fees (note: getClaimableFees is not implemented on the current live PM — fee-target orders are unavailable for that reason; bin-based stop-loss / take-profit work).

Bind PositionNFTViewer ABI at the proxy above. Pair this surface with the Pair Viewer for index-friendly batches.

Dev / DLMM / Subgraph

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.

Dev / DAMM / Overview

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.
Dev / DAMM / Pair

DAMM Pair (U1/WETH)

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

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 picksxToY
Sell U1 → get WETHtrue
Buy U1 with WETHfalse
Buy U1 with native ETHfalse (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()
Dev / DAMM / Router

DAMM Router

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

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.

Dev / DAMM / Factory & beacon

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

RoleAddress
DAMMFactory (proxy)0xD14322b4…A1aCD5c
DAMMFactory impl0x5491E26d…816630
DAMMPairBeacon0xB6363F60…8569
DAMMPair impl0x1d8752A8…3D0A
DAMMPairPaused (emergency stub)0xEcEc2DCB…76B22

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
Dev / DAMM / Subgraph

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.

Dev / Custody keeper / Overview

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.

ModeWho holds the NFTWhat it doesContract the user calls
A — Auto-rebalance (custody)The keeper holds itKeeps the position centered on the active price as it driftsCustodyRebalanceKeeper
B — Take-profit / Stop-loss (non-custody)The user keeps itCloses the position when a price trigger firesOrderManager

What v8 adds (over v7)

  • Per-pool fee trackingpoolAccounting(depositor, pair) → (feesX, feesY, rebalanceCount, retainedX, retainedY) + the FeesRealized event.
  • Retained reuse-bufferwithdrawRetained(pair) reclaims the depositor's own capital the keeper holds back across rebalances. See Withdraw retained.
  • Fee modessetFeeMode(pair, separate) / separateFees(depositor, pair) (Compound default vs Separate). See Fee modes.
  • Cooldown off-switchminRebalanceInterval == 0 disables the per-position cooldown.

Operational addresses (NOT called by integrators — surface for verification)

AddressRoleCalls
0x5986Bb…Ac387Keeper operatorCustodyRebalanceKeeper.rebalance(...), OrderExecutor.executeOrder(...)
0x3489BD…1f6e907Sync operatorEligibilityManager.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.

Dev / Custody keeper / Deposit

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 */ ...)
Dev / Custody keeper / Status reads

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.

Dev / Custody keeper / Gas prepaid model

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.

Dev / Custody keeper / Fee modes

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)
Dev / Custody keeper / Withdraw retained

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).

Dev / Custody keeper / Exit

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.

Dev / Custody keeper / Orders

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.

Dev / Access control / NFT Lock Vault

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.

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)

PhaseContractToken ID
Phase 10x42347DB4…6885b1
Phase 20x17A2b200…1712Ce0
Phase 30xab505a66…FeD1A0

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.

Dev / Access control / Eligibility Manager

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.

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)
Dev / Off-chain / Subgraph index

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.

SubgraphWhat it indexesPage
umbrae-dlmm-baseDLMM Factory / Pair / PositionNFTManager / LBToken events — pairs, positions, swaps, hourly/daily snapshotsDLMM Subgraph
umbrae-damm-baseDAMM Factory / Pair events — Uniswap V2-compatible schema for aggregator parityDAMM Subgraph

When to use the subgraph vs RPC

Use caseRecommended source
Live state (active bin, reserves right now)RPC + pair viewer
Historical swaps / volume / feesSubgraph
User position listSubgraph (or PositionNFTManager + RPC)
Charts (hourly / daily rollups)Subgraph
USD-denominated TVL / volumeSubgraph 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.

Dev / Off-chain / Listings & data partners

Listings & data partners

External aggregators that index Umbrae pools.

Indexing status

ServiceStatusNotes
GeckoTerminalPending submissionSubmission is manual; auto-discovery does not index our custom factory + Swap event signature.
DEX ScreenerPending submissionSame auto-discovery limitation.
DEXToolsPending submissionSame.

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 contract0x14a4e80d633aF55Ace1160c320f5a36D41CCEd3E.
  • Primary pair — DAMM V4 U1/WETH at 0x296964C34a571fCf85d3F74FB815ee871F5A08d4.
  • Secondary pair — DLMM U1/WETH at 0x697B72320656e6Dc60Db7A4Bfb95084C9D9C55A0 (25 bps).
  • 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.
Dev / Off-chain / Aggregator adapter pattern

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

SurfaceUse
SubgraphPool discovery + reserves snapshot.
Pair Viewer (DLMM)Per-bin reads for accurate quotes on partially-filled swaps.
DAMM Pair directgetAmountOut + 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 — 0x697B72320656e6Dc60Db7A4Bfb95084C9D9C55A0 (DLMM U1/WETH, 25 bps) 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.

Dev / Resources / Source repos

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.

RepoWhat
github.com/Ignis-AI-LabsThe Ignis AI Labs organization — entry point.
Ignis-AI-Labs/umbraeTrading platform frontend + indexer + bots. Hosts these dev docs.
Ignis-AI-Labs/umbrae-sitePublic umbrae.io site. FAQ + landing.
Ignis-AI-Labs/evm-contractsAll 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.
Dev / Resources / Audits

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)

DateScopeOutcome
2026-05-12U1TokenLockVaultSigned off — 39/39 tests, no Critical/High.
2026-05-16Comprehensive (BlackBox)Multi-contract audit. Findings folded into the deploy pipeline; CSP report-only telemetry shipped 2026-05-18.
2026-06-09Custody keeper adversarial reviewFour findings (grace-timestamp opt-out bypass, executor PM-interface check, escrow fallback, interface-gated withdrawPosition) addressed in v5 (BUSL-1.1 cutover).
2026-06-16v6 + v7 keeper redeployFull KA3 swap-safety + KEEPER-054 owner-funded order gas + OE-01/OE-02 + KEEPER-055 (KeeperMath extraction).
2026-06-23v8 keeper redeployPer-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. Public reports will be linked from this page as they're published.

Dev / Resources / Changelog

Changelog

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

Custody keeper

VersionDateChange
v8 (active)2026-06-23Keeper + 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.
v72026-06-16Raised max position width to ±50 bins (101 total). MAX_BIN_RANGE 20→50, gas estimates 10M→30M. Replaced by v8.
v62026-06-16Full KA3 swap-safety + KEEPER-054 owner-funded order gas + OE-01/OE-02 + KEEPER-055. Same-day replaced by v7.
v52026-06-10First BUSL-1.1 keeper. KEEPER-052 stack + 2026-06-09 audit fixes (grace-timestamp, executor PM-interface, escrow fallback, withdrawPosition).
v4 and earlier2026-06-01 to 2026-06-08Pre-BUSL deploys. Retired. See Gas prepaid model § Recovering gas for funds recovery on retired keepers.

DAMM

VersionDateChange
V4 (active)2026-05-07Carries V-15 / V-16 / V-18 / V-19 audit fixes in deployed bytecode. BeaconProxy pattern.
V32026-05-07Retired same day. Audit re-review surfaced findings folded into V4.
V2 / V12026-05-03Retired. Pre-upgradeable era.

DLMM

DateChange
2026-05-15U1/WETH pair added (25 bps) — verified via factory.allPairs(13).

U1 token

DateChange
≈ 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

DateChange
2026-06-29Phase 1 ships: Quickstart, Network, Address Book, ABIs, Deploy blocks at /dev/.
2026-06-29Phase 2–5 ships: every contract page, off-chain integration, resources.