DEX Hook Contracts
Source on GitHub: github.com/Dobprotocol/Dobhooks —
Dobhooks/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:
| Contract | File | Purpose |
|---|---|---|
DobPegHook | DobPegHook.sol | Uniswap V4 hook — buys, sells, liquidations, RWA resale market |
DobValidatorRegistry | DobValidatorRegistry.sol | On-chain price + liquidation oracle |
DobLPRegistry | DobLPRegistry.sol | Permissionless LP positions and per-asset backings |
DobPooledLN | DobPooledLN.sol | Shared LN vault — pooled USDC under one LP entry |
DobRwaVault | DobRwaVault.sol | Wraps approved RWA tokens into the dobRWA ERC-20 |
DobDirectSwap | DobDirectSwap.sol | Standalone dUSDC↔USDC peg for chains without V4 |
DobSwapRouter | DobSwapRouter.sol | User-facing router for V4 pools |
DobTokenFactory | DobTokenFactory.sol | Factory for mock RWA tokens (testing) |
OracleAlertReceiver | OracleAlertReceiver.sol | Cross-chain alert sink (Reactive Network callback) |
ReactiveOracleSync | ReactiveOracleSync.sol | ReactVM contract that mirrors PriceUpdated events |
RWAFaucet (MockRWA, MockUSDC) | RWAFaucet.sol | Demo 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):
| Hook | Enabled |
|---|---|
beforeInitialize | Yes — only the admin address may initialise pools |
beforeAddLiquidity | Yes — only the admin may seed liquidity |
beforeSwap | Yes — core custom-accounting logic |
beforeSwapReturnDelta | Yes — emits the custom delta |
| All other hooks | No |
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):
| Direction | Path |
|---|---|
USDC -> dobRWA (buy) | 1:1 peg. Hook mints ERC-6909 USDC claims, settles dobRWA out. |
dobRWA -> USDC (sell), liquidation enabled | Applies penaltyBps from the registry, calls lpRegistry.queryAndFill, records the liquidation, and emits LiquidationSwap. |
dobRWA -> USDC (sell), no liquidation | Routes 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
| Function | Purpose |
|---|---|
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
- Register —
register(amount)deposits USDC and creates theLPPosition. - Back assets —
backAsset(rwaToken, minOraclePrice, minPenaltyBps, maxExposure, usdcAllocation)earmarks USDC for one asset and inserts the LP into a sorted backer list (ascending byminPenaltyBps). - Update / increase —
updateConditions(...)re-sorts on penalty change;increaseAllocation(...)adds USDC to an existing backing. - Stop backing —
stopBacking(rwaToken)returns the unused USDC. If the asset is currently distressed, 33% is held as aReserveHoldand only released byreleaseReserve(holdIndex)once the asset is healthy or afterRESERVE_WITHDRAWAL_DELAY(7 days). - Withdraw position —
requestWithdrawal(amount)thenexecuteWithdrawal()afterWITHDRAWAL_DELAY. - Claim earned RWA —
claimRwaTokens(rwaToken, dobRwaAmount)instructs the hook to convert dobRWA into the underlying RWA via the vault.
Hook-only fill functions
| Function | Used 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.solfiles per chain; the canonical address list will live in the repo'sdeployments/folder.
| Contract | Arbitrum Sepolia (421614) | Base Sepolia (84532) | Robinhood Testnet (46630) | Unichain Sepolia (1301) |
|---|---|---|---|---|
| DobPegHook | TODO | TODO | n/a (no V4) | TODO |
| DobRwaVault | TODO | TODO | TODO | TODO |
| DobValidatorRegistry | TODO | TODO | TODO | TODO |
| DobLPRegistry | TODO | TODO | TODO | TODO |
| DobPooledLN | TODO | TODO | TODO | TODO |
| DobDirectSwap | n/a | n/a | TODO | n/a |
| DobSwapRouter | TODO | TODO | n/a | TODO |
| OracleAlertReceiver | n/a | n/a | n/a | TODO |
| ReactiveOracleSync (Reactive Network 5318007) | — | — | — | TODO |