Search projects, hackathons, sponsors and tech

    ← All projects

    Bailiff

    Any keeper can liquidate restricted-RWA loans via Uniswap v4. The RWA never leaves the allowlist.

    Demo

    About

    Restricted RWA tokens (ERC-3643 style) only move to wallets the issuer has allowlisted. On lending markets like Aave Horizon, seized RWA collateral can only go to the issuer's allowlisted holders. So liquidators must be KYC'd, pre-funded in stablecoins and able to hold the RWA. If that small group is slow or missing, lenders can be left with bad debt. Bailiff splits the trigger from the receiver. Any keeper can call the LiquidationAdapter, even with 0 USDC. In one transaction it sells the seized RWA on a Uniswap v4 permissioned pool, repays the liquidated debt and pays the keeper a USDC bounty. The keeper never touches the RWA. If the sale cannot cover the debt, everything reverts. Bailiff expands the keeper set without expanding the holder set. The issuer keeps control. On the liquidation side it flags one sell-only contract, the adapter, next to the pool's own wrapper, and never the keeper. The token's controls (allowlist, freeze, pause, forced transfer) and the issuer's bounded NAV oracle still apply. Revoke the adapter and the same keeper call reverts in Uniswap's hook. Bailiff is an extra route next to KYC liquidators, not a replacement. The buyer is the pool's allowlisted LP, a KYC market maker in the demo, whose USDC sits in the pool ahead of time. Aave's liquidationCall sends the collateral first and pulls the stablecoin from the caller at the end, with no callback in between. v4 flash accounting lets the adapter sell the RWA before it holds it, so the keeper fronts nothing.

    How it's made

    Solidity 0.8.26 and Foundry on Uniswap v4-core and v4-periphery. No custom hook. The demo runs on an Anvil fork of Sepolia and the pinned tests on a Foundry fork, both at block 11782723, against Uniswap Labs' deployed PoolManager, PermissionsAdapterFactory and PermissionedHooks. My PermissionsAdapter comes from the real factory, and the adapter's constructor checks verifiedPermissionsAdapterOf. Inside one PoolManager.unlock, LiquidationAdapter: - previews the exact repay and seize amounts - swaps exactly the seize amount of vRWA (the PermissionsAdapter wrapper) for USDC. Flash accounting leaves the vRWA delta open, so it sells collateral it does not hold yet. No keeper capital, no flash loan. - before any transfer: reverts if proceeds < repay; bounty = min(surplus * keeperBps / 10,000, the market's 6% bonus on repay); reverts if bounty < minBounty - takes the USDC, repays the market, receives the RWA - closes the vRWA delta: sync, RWA to the PermissionsAdapter, wrapToPoolManager, settle - pays the keeper and pushes the rest to the borrower Notes: - msgSender() returns the adapter, so Uniswap's hook checks the adapter, not the keeper. - PermissionedV4Router checks allowedHooks and the deployed hook leaves that to callers. The adapter calls PoolManager directly, so it runs the same check (test_adapter_hookNotAllowed). - MockRWA3643 (ERC-3643-style: HOLDER, freeze, pause, forcedTransfer; no ONCHAINID) is its own IAllowlistChecker. One issuer registry answers Uniswap's SWAP/LIQUIDITY flags and the token's HOLDER gate. - The console's keeper has no swap permission, so it quotes by eth_call on the adapter, then sends with minBounty at 97% of the quote. Known gaps: - No NAV execution floor yet: the swap has no price limit. The protocol-side guard is proceeds >= repay; minBounty only protects the keeper's cut. - The surplus is pushed to the borrower's wallet, not credited, so a borrower that cannot receive USDC can block its own liquidation. Tests: 59/59 pass (unit, fuzz, 6 invariants on a simulated venue, local PoolManager, fork tests on latest Sepolia). A 32-test acceptance suite pins block 11782723, its parent hash and the Labs codehashes: 13 pass, and 19 are red on purpose because they specify fixes not built yet, including a claimable borrower credit and the NAV floor. Main proof: test_anonDirectFails_adapterSucceeds and test_wrapperRevocationChangesSameCall. Demo console: SvelteKit + viem, local fork only, 225 passing bun tests. FEEDBACK.md has integrator notes for Uniswap.