Skip to main content

DEX Hook Contracts

Source on GitHub: github.com/Dobprotocol/DobhooksDobhooks/contracts/src/

The DobDex smart-contract suite is built around DobPegHook, a Uniswap V4 Custom Accounting hook. The hook intercepts swaps, prices buys at the oracle USD value, and routes sells through the DobLPRegistry so Liquidity Nodes (LPs) — not a bonding curve — provide exit liquidity.

The supporting contracts are:

ContractFilePurpose
DobPegHookDobPegHook.solUniswap V4 hook — buys, sells, liquidations, RWA resale market
DobValidatorRegistryDobValidatorRegistry.solOn-chain price + liquidation oracle
DobLPRegistryDobLPRegistry.solPermissionless LP positions and per-asset backings
DobPooledLNDobPooledLN.solShared LN vault — pooled USDC under one LP entry
DobRwaVaultDobRwaVault.solWraps approved RWA tokens into the dobRWA ERC-20
DobDirectSwapDobDirectSwap.solStandalone dUSDC↔USDC peg for chains without V4
DobSwapRouterDobSwapRouter.solUser-facing router for V4 pools
DobTokenFactoryDobTokenFactory.solFactory for mock RWA tokens (testing)
OracleAlertReceiverOracleAlertReceiver.solCross-chain alert sink (Reactive Network callback)
ReactiveOracleSyncReactiveOracleSync.solReactVM contract that mirrors PriceUpdated events
RWAFaucet (MockRWA, MockUSDC)RWAFaucet.solDemo faucets — 1-hour cooldown per address

Architecture

                 Uniswap V4 PoolManager
|
DobPegHook (beforeSwap, beforeInitialize, beforeAddLiquidity)
/ | \\
DobValidatorRegistry DobRwaVault DobLPRegistry <--- DobPooledLN
^ ^
| |
ReactiveOracleSync (Reactive Network) ----> OracleAlertReceiver

For chains without Uniswap V4 the hook is replaced by a standalone DobDirectSwap contract, but pricing and the LP registry remain unchanged.

DobPegHook

Implements BaseHook from @openzeppelin/uniswap-hooks and uses the V4 Custom-Accounting (NoOp) pattern: _beforeSwap returns a BeforeSwapDelta that the PoolManager honours instead of the bonding-curve math.

Hook permissions

The hook registers the following V4 permissions (getHookPermissions):

HookEnabled
beforeInitializeYes — only the admin address may initialise pools
beforeAddLiquidityYes — only the admin may seed liquidity
beforeSwapYes — core custom-accounting logic
beforeSwapReturnDeltaYes — emits the custom delta
All other hooksNo

Swap paths in _beforeSwap

_beforeSwap requires exact-input swaps (amountSpecified < 0). It decodes hookData for the RWA token address and an optional minAmountOut (slippage protection):

DirectionPath
USDC -> dobRWA (buy)1:1 peg. Hook mints ERC-6909 USDC claims, settles dobRWA out.
dobRWA -> USDC (sell), liquidation enabledApplies penaltyBps from the registry, calls lpRegistry.queryAndFill, records the liquidation, and emits LiquidationSwap.
dobRWA -> USDC (sell), no liquidationRoutes through _handleNormalSell — LP-first via lpRegistry.queryAndFillAtMarket, then hook reserves cover any remainder. lpOnlyMode[rwaToken] skips reserves entirely.

Sells additionally pay an optional protocolFeeBps (max 5%) that accrues to protocolReserveUsdc. Slippage protection reverts with SlippageExceeded when minAmountOut > 0 && amountOut < minAmountOut.

Permissionless USDC LP pool

Anyone can deposit USDC into the hook's pool with depositUsdc(amount) and receive shares; swapFeeBps (max 10%) accrues to totalLpUsdc. The first deposit burns 1000 dead shares (DEAD_SHARES) to prevent first-depositor inflation. Withdrawals enforce MIN_LP_DURATION = 1 hours.

RWA Resale Market

LPs (and any seller) can list their accumulated RWA tokens for sale at oracle price using listRwaForSale(token, amount); buyers call buyListedRwa(token, amount) and the hook fills sellers FIFO. MAX_RWA_SELLERS = 50 per token.

Key state

DobRwaVault            public immutable vault;
ERC20 public immutable usdc;
ERC20 public immutable dobRwa;
DobValidatorRegistry public immutable registry;
DobLPRegistry public lpRegistry;
address public admin;
uint16 public swapFeeBps;
uint16 public protocolFeeBps;
uint256 public protocolReserveUsdc;
uint256 public totalLpUsdc;
uint256 public totalShares;
uint256 public totalLpDobRwaOwed;
mapping(address => bool) public lpOnlyMode;

Notable events

PegSwap, LiquidationSwap, LPFill, RwaListed, RwaPurchased, RwaSold, UsdcDeposited, UsdcWithdrawn, LPRegistrySet, Paused, Unpaused.

DobValidatorRegistry

DobValidatorRegistry is Owned (Solmate) — there is no OpenZeppelin AccessControl, no VALIDATOR_ROLE, and no registerAsset step. The owner address is the validator.

struct PriceData {
uint256 priceUsd; // 18-decimal USD price per 1e18 base units
uint48 updatedAt;
}

struct LiquidationData {
bool enabled;
uint16 penaltyBps; // 1..10000
uint256 cap; // per-asset dobRWA cap
uint256 liquidatedAmount; // running total
}

mapping(address => PriceData) public prices;
mapping(address => LiquidationData) public liquidations;

uint48 public constant MAX_ORACLE_DELAY = 1 days;
uint256 public globalLiquidationCap;
uint256 public globalLiquidatedAmount;
address public hook;
uint16 public maxPriceChangeBps; // 0 = unlimited
bool public paused;

Owner functions

FunctionPurpose
setPrice(token, priceUsd)Set/update the 18-decimal USD price; enforces maxPriceChangeBps after the first set.
emergencySetPrice(token, priceUsd)Bypasses maxPriceChangeBps for legitimate large corrections.
setLiquidationParams(token, penaltyBps, cap)Enable liquidation for token; preserves the running counter.
disableLiquidation(token)Disable liquidation and reset the per-asset counter.
setGlobalLiquidationCap(cap)Set the global dobRWA liquidation cap.
setMaxPriceChange(maxBps)Per-update price-change guardrail.
setHook(hook)Authorise the hook that may call recordLiquidation.
pause() / unpause()Emergency stop for setPrice / setLiquidationParams.

Reads

getPrice(token) returns (priceUsd, updatedAt) and reverts PriceNotSet if never written. getLiquidationParams(token) returns the full LiquidationData tuple.

Events

PriceUpdated, LiquidationEnabled, LiquidationDisabled, LiquidationRecorded, GlobalLiquidationCapSet, HookSet, MaxPriceChangeSet, Paused, Unpaused.

DobLPRegistry

LPs in DobDex go through two steps: register an LP position with USDC, then back specific assets with conditions. The registry is Owned + ReentrancyGuard.

Position vs. backing

struct LPPosition {
uint256 usdcDeposited; // lifetime total
uint256 usdcAvailable; // not yet allocated
uint48 registeredAt; // anti-flash-loan timestamp
bool active;
}

struct AssetBacking {
uint256 minOraclePrice; // 18-decimal USD floor
uint16 minPenaltyBps; // minimum discount required
uint256 maxExposure; // dobRWA cap
uint256 currentExposure;
uint256 usdcAllocated;
uint256 usdcUsed;
uint48 backedAt; // anti-frontrun timestamp
bool active;
}

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

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
uint16 public constant PROTOCOL_FEE_BPS = 150; // 1.5% fee on LP fills
uint16 public constant RESERVE_BPS = 3300; // 33% reserve on distressed exit
uint48 public constant RESERVE_WITHDRAWAL_DELAY = 7 days;

LP lifecycle

  1. Registerregister(amount) deposits USDC and creates the LPPosition.
  2. Back assetsbackAsset(rwaToken, minOraclePrice, minPenaltyBps, maxExposure, usdcAllocation) earmarks USDC for one asset and inserts the LP into a sorted backer list (ascending by minPenaltyBps).
  3. Update / increaseupdateConditions(...) re-sorts on penalty change; increaseAllocation(...) adds USDC to an existing backing.
  4. Stop backingstopBacking(rwaToken) returns the unused USDC. If the asset is currently distressed, 33% is held as a ReserveHold and only released by releaseReserve(holdIndex) once the asset is healthy or after RESERVE_WITHDRAWAL_DELAY (7 days).
  5. Withdraw positionrequestWithdrawal(amount) then executeWithdrawal() after WITHDRAWAL_DELAY.
  6. Claim earned RWAclaimRwaTokens(rwaToken, dobRwaAmount) instructs the hook to convert dobRWA into the underlying RWA via the vault.

Hook-only fill functions

FunctionUsed in
queryAndFill(rwaToken, oraclePrice, penaltyBps, usdcNeeded)Liquidation path — protocol-mandated penalty.
queryAndFillAtMarket(rwaToken, oraclePrice, usdcNeeded)Normal sell path — each LP's own minPenaltyBps is the discount.

Both iterate the sorted backer list, charge PROTOCOL_FEE_BPS (1.5%) into accumulatedFees, transfer net USDC to the hook, and credit rwaOwed[lp][rwaToken] for later claim.

Notable events

LPRegistered, LPDeposited, AssetBacked, ConditionsUpdated, AllocationIncreased, BackingStopped, FillExecuted, WithdrawalRequested, WithdrawalExecuted, WithdrawalCancelled, DobRwaClaimed, ReserveHeld, ReserveReleased, ProtocolFeeCollected, ProtocolFeeWithdrawn.

DobDirectSwap

A lightweight 1:1 dUSDC ↔ USDC peg for chains without Uniswap V4 (currently Robinhood Chain Testnet 46630). Holds USDC reserves and exposes:

function sellDusdc(uint256 amount) external;     // applies swapFeeBps to LP pool
function buyDusdc(uint256 amount) external; // 1:1, no fee
function swap(bool zeroForOne, uint256 amountIn, bytes calldata) external returns (uint256 amountOut);
function depositUsdc(uint256 amount) external returns (uint256 shares);
function withdrawUsdc(uint256 shares) external returns (uint256 amount);
function seedUsdc(uint256 amount) external; // protocol reserves
function withdrawProtocolReserve(uint256 amount) external; // owner only

There is no liquidate(...) function on DobDirectSwap; the user-facing liquidation path lives on the hook (see LiquidationSwap event in DobPegHook._beforeSwap).

DobSwapRouter

User-facing wrapper around IPoolManager that encodes PoolKey, SwapParams, and hookData so callers do not have to interact with V4 primitives directly. The router does not change pricing — it forwards into DobPegHook.

DobTokenFactory

Factory used in tests/demos to mint mock RWA tokens. Production RWA tokens are issued by ERC-3643 / SAC pipelines outside the DEX.

Cross-chain oracle (Reactive Network)

ReactiveOracleSync runs on Reactive Network (Lasna Testnet, chain 5318007). It subscribes to PriceUpdated and LiquidationEnabled events emitted by DobValidatorRegistry on Unichain Sepolia (chain 1301). When a price drops below alertThresholds[token], the contract emits Callback(...), which the Reactive Callback Proxy delivers as a call to OracleAlertReceiver.onPriceAlert(...) on Unichain Sepolia. LiquidationEnabled events are forwarded similarly via onLiquidationEnabled(...). See ReactiveOracleSync.sol and OracleAlertReceiver.sol.

Test coverage

The Foundry test suite covers swap mechanics, oracle staleness, LP fills, liquidation caps, vault accounting, and reserve-hold edge cases.

Deployed Addresses

TODO: verify and fill in once published. The Dobhooks repository contains script/Deploy*.s.sol files per chain; the canonical address list will live in the repo's deployments/ folder.

ContractArbitrum Sepolia (421614)Base Sepolia (84532)Robinhood Testnet (46630)Unichain Sepolia (1301)
DobPegHookTODOTODOn/a (no V4)TODO
DobRwaVaultTODOTODOTODOTODO
DobValidatorRegistryTODOTODOTODOTODO
DobLPRegistryTODOTODOTODOTODO
DobPooledLNTODOTODOTODOTODO
DobDirectSwapn/an/aTODOn/a
DobSwapRouterTODOTODOn/aTODO
OracleAlertReceivern/an/an/aTODO
ReactiveOracleSync (Reactive Network 5318007)TODO