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
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. 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
Rewards
DLMM trading stack
DLMM implementation contracts (transparent — read source, never call directly)
DLMM live pairs
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.
DAMM V4 trading stack
Governance
External canonical
Ethereum mainnet
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
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)
| 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 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 ordering — tokenX < 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 accounting —
swap 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.
DLMM Router
Multi-hop swaps and liquidity management for the DLMM stack. UUPS-upgradeable, governed by the DLMM timelock.
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
- Approve the input token to the router (skip if allowance already covers).
- Call
swapExactTokensForTokens(...) with amountOutMin = quote × (10000 - slippageBps) / 10000.
- 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.
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)
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
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
- Mint — deposit tokens into a pair across a bin range; receive an ERC-721.
- Increase / decrease liquidity — add or remove from the same position.
- Collect — pull accumulated fees in tokenX/Y.
- Burn — close the position fully; tokens return to the owner.
Strategy presets
| Strategy | Value | Liquidity shape |
Spot | 0 | Equal weight across every bin in the range. |
Curve | 1 | Concentrated near the active bin (Gaussian-like). Higher fees when in range, more IL on a strong move. |
BidAsk | 2 | Weighted 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.
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
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
| 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 — 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.
| 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. |