Pular para o conteúdo principal

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:

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. Register — register(amount) deposits USDC and creates the LPPosition.
  2. Back assets — backAsset(rwaToken, minOraclePrice, minPenaltyBps, maxExposure, usdcAllocation) earmarks USDC for one asset and inserts the LP into a sorted backer list (ascending by minPenaltyBps).
  3. Update / increase — updateConditions(...) re-sorts on penalty change; increaseAllocation(...) adds USDC to an existing backing.
  4. Stop backing — stopBacking(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 position — requestWithdrawal(amount) then executeWithdrawal() after WITHDRAWAL_DELAY.
  6. Claim earned RWA — claimRwaTokens(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