Skip to main content

Liquidity Nodes

Source: Dobhooks/contracts/src/DobLPRegistry.sol

DobDex uses a permissionless Liquidity Node (LN) system. Anyone can deposit USDC and earn yield by providing exit liquidity for RWA token sells and liquidations. Unlike a generic AMM, LPs do not deposit into a shared bonding curve — they create an LP position, then explicitly back specific assets with their own conditions, and are matched by the hook FIFO-style at fill time.

The two-step LP model

Becoming an LN takes two steps:

  1. Register an LP position with USDC.
  2. Back individual assets with conditions (price floor, minimum discount, exposure cap) and an earmarked USDC allocation.
struct LPPosition {
uint256 usdcDeposited; // lifetime deposit
uint256 usdcAvailable; // not yet allocated to any asset
uint48 registeredAt; // anti-flash-loan timestamp
bool active;
}

struct AssetBacking {
uint256 minOraclePrice; // 18-decimal USD floor — fills only when oracle >= this
uint16 minPenaltyBps; // minimum discount (LP-priced exit margin)
uint256 maxExposure; // dobRWA cap for this asset
uint256 currentExposure;
uint256 usdcAllocated;
uint256 usdcUsed;
uint48 backedAt; // anti-frontrun timestamp
bool active;
}

So an LP "node" really means (LPPosition, AssetBacking): one LP, one specific asset, one set of conditions. A single LP can back many assets simultaneously (one AssetBacking per asset).

Functions an LP calls

FunctionWhat it does
register(amount)One-time. Deposits USDC, creates the LPPosition.
depositMore(amount)Add more USDC to the position later.
backAsset(rwaToken, minOraclePrice, minPenaltyBps, maxExposure, usdcAllocation)Earmark USDC for a specific asset under specific conditions.
updateConditions(rwaToken, ...)Change conditions for an existing backing (re-sorts the per-asset backer list if minPenaltyBps changes).
increaseAllocation(rwaToken, amount)Move more USDC from usdcAvailable into a specific backing.
stopBacking(rwaToken)Stop backing — see Stop-backing and the reserve hold below.
requestWithdrawal(amount)Start the 24h timer to withdraw usdcAvailable.
executeWithdrawal()After 24h, pull the USDC out.
cancelWithdrawal()Cancel a pending withdrawal.
claimRwaTokens(rwaToken, dobRwaAmount)Convert dobRWA earned from fills into the underlying RWA via the hook + vault.
releaseReserve(holdIndex)Unlock a 33% reserve hold (see below).

How the hook matches LPs

The hook calls one of two functions on the registry during a sell, both nonReentrant and gated to msg.sender == hook:

Hook pathFunctionDiscount source
Liquidation enabledqueryAndFill(rwaToken, oraclePrice, currentPenaltyBps, usdcNeeded)Protocol-mandated penaltyBps.
Normal sellqueryAndFillAtMarket(rwaToken, oraclePrice, usdcNeeded)Each LP's own minPenaltyBps.

Both iterate the per-asset backer list, which is sorted ascending by minPenaltyBps (cheapest fills first), and skip any backer that:

  • is younger than MIN_BACKING_AGE (1 hour),
  • has oraclePrice < minOraclePrice,
  • has currentPenaltyBps < minPenaltyBps (liquidation path only),
  • has zero usdcAllocated - usdcUsed,
  • has zero maxExposure - currentExposure.

For each filled LP, the registry charges PROTOCOL_FEE_BPS = 150 (1.5%) into accumulatedFees, transfers the net USDC to the hook, and credits rwaOwed[lp][rwaToken] for later claim.

Constants

uint48  public constant WITHDRAWAL_DELAY          = 24 hours;
uint48 public constant MIN_BACKING_AGE = 1 hours;
uint8 public constant MAX_BACKERS_PER_ASSET = 50; // per-asset cap
uint16 public constant PROTOCOL_FEE_BPS = 150; // 1.5% of every LP fill
uint16 public constant RESERVE_BPS = 3300; // 33% reserve on distressed exit
uint48 public constant RESERVE_WITHDRAWAL_DELAY = 7 days;

MIN_BACKING_AGE blocks flash-loan-then-fill attacks. WITHDRAWAL_DELAY blocks LPs from front-running known liquidations. MAX_BACKERS_PER_ASSET = 50 keeps gas bounded for the O(n) matching loop.

Stop-backing and the reserve hold

stopBacking(rwaToken) always returns the LP's unused USDC allocation, but if the asset is currently in distress (liquidation enabled with penaltyBps > 0), the registry holds back 33% of the returned USDC as a ReserveHold:

struct ReserveHold {
uint256 amount; // 33% locked
address rwaToken; // distressed asset that triggered the hold
uint48 createdAt;
}

The reserve unlocks when either:

  • the asset returns to healthy (liquidation disabled or penaltyBps == 0), or
  • RESERVE_WITHDRAWAL_DELAY = 7 days has elapsed since createdAt.

The LP then calls releaseReserve(holdIndex) to move the USDC back to usdcAvailable. This is the only place where the 33% reserve applies — it does not continuously lock 33% of every LP's deposit.

Returns for LPs

LPs earn returns in two complementary ways:

  • Discount on RWA tokens received during fills. The seller pays the discount (minPenaltyBps for normal sells, protocol penaltyBps for liquidations); the LP receives dobRwa = fillUsdc * 10000 / (10000 - effectivePenaltyBps).
  • Underlying RWA yield. After claimRwaTokens, the LP holds the actual RWA tokens and earns whatever those tokens yield off-chain.

Net of fees: every LP fill is taxed PROTOCOL_FEE_BPS = 150 (1.5%) — the protocol fee accrues to accumulatedFees and is withdrawn by the owner via withdrawFees() to protocolTreasury.

LP fill math (normal sell @ market):
penalty = backing.minPenaltyBps (each LP's own choice)
fillUsdc = remaining (capped by usdcAllocated - usdcUsed)
dobRwaAmount = fillUsdc * 10000 / (10000 - penalty)

LP fill math (liquidation):
penalty = registry.liquidations[token].penaltyBps (protocol-set)

Reading LP state

positions(lp) -> LPPosition
backings(lp, asset) -> AssetBacking
getAssetBackers(rwaToken) -> address[] // sorted ascending by minPenaltyBps
getAssetBackerCount(rwaToken) -> uint256
getAssetLiquidity(rwaToken) -> uint256 // total available USDC across active backers
getReserveHolds(lp) -> ReserveHold[]
rwaOwed(lp, rwaToken) -> uint256 // claimable dobRWA

Risk factors

RiskMitigation in the code
Backing a stale-priced or worthless assetminOraclePrice per-backing floor.
Flash loan in / out around a known fillMIN_BACKING_AGE = 1h.
Front-run withdrawal before liquidationWITHDRAWAL_DELAY = 24h.
Cascading distressed exitsRESERVE_BPS = 33% reserve hold for 7d on stop-backing into distress.
Concentrationper-backing maxExposure cap.
Per-asset gasMAX_BACKERS_PER_ASSET = 50 per token.

See also