No sections match that search. Try router, address, abi, quickstart.
Dev / Get started
/ Quickstart
Quickstart
Start with public market data, or use verified contract interfaces for your own wallet integration.
For integrators
The DLMM V2 ABI index 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
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
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.
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
The public market-data adapters require no authentication. App endpoints outside that surface have route-specific access rules: some reads are anonymous, while protected operations require sessions or scoped API keys. They are not a versioned external execution contract.
Sign-in flow
Sign-in is EVM wallet-signature based. Use the platform's sign-in flow for account access; the public market-data interface does not require signing in.
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.
- Scoped API keys are available through the account API-key flow. Creation requires an existing session and the account's email policy; an API key cannot issue another API key.
- An account API key resolves to that account's primary wallet. The swap builder requires write scope and binds both caller and recipient to the resolved wallet; it is not an arbitrary-recipient routing interface.
Public HTTP API
Use the OpenAPI specification for supported anonymous market-data operations. Read the integration guide for units, retries and the boundary between discovery and execution.
Dev / Public API & agents
Public API & agents
Read Umbrae market data on Base without an API key, session or wallet connection.
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.
Trading and liquidity provision
This schema describes market data only. Use the DLMM V2 ABI bundle as an interface reference, verify the current deployment, quote and simulate from the actual caller, and sign with the caller's wallet. Multi-step swaps and LP exits need explicit completion tracking. Account keys do not grant authority over unrelated wallets.
Documentation prepared by GPT-6 via Codex, 2026-09-08.
Dev / Reference
/ Address Book
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
Rewards
Current DLMM V2 trading stack — Base mainnet
Remediated deployment of 2026-08-24. Use these addresses with the V2 ABI bundle. Download the deployment registry. Implementation and extension addresses are source references; call the appropriate proxy or pair.
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.
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.
DAMM V4 trading stack
Governance
External canonical
Ethereum mainnet
ABIs
For DLMM V2, use the current application ABI bundle. 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, paired with the generated deployment registry. 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.
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)
| 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. Every contract on this site links to its
#code page for verified source.
- Etherscan — 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.
Name / Symbol
Umbrae One · U1
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.
Lifecycle
- Approve U1 to the vault, then
lock(amount, duration).
- Rewards accumulate as protocol fees are deposited; query your share with
pendingReward(holder).
- Claim WETH any time via
claimReward() — works while still locked.
- Extend the lock by calling
extendLock(newUnlockTime) — monotonic, new unlock time must be ≥ current.
- 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 with 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 with 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 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 with 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 with 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 with 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.
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.
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
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.
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.
| 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.
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.
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)
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.
| 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.
Dev / Off-chain
/ Listings & data partners
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.
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
| 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
- Discover pools — query the subgraph for active pairs on each stack. Skip pairs with no liquidity.
- 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).
- 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.
- Encode the swap — emit the call the aggregator's swap executor will perform:
pair.swap(...) for DAMM, router.swapExactTokensForTokens(...) for DLMM multi-hop.
- 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.
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.
| Repo | What |
| 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. 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
| 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. |