Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Smart Contracts

Bid on liquidated BTC collateral, or run a keeper, with direct contract calls on Base.

This is the no-dependency path. The Auction SDK wraps exactly these calls in TypeScript; the fragments below are enough to ship a full discover, quote, and buy flow in any language.

Both roles are permissionless by protocol design. Anyone may call checkHealth to open a liquidation auction on an unhealthy position, and anyone may call buy to clear one. It is fully non-custodial: you build the transactions, the user signs them in their own wallet.

How a liquidation auction works

Every position is a BTC-collateralized, USDC-denominated credit line identified by a position id.

  1. When a position's collateral ratio falls below its market threshold, checkHealth(positionId) opens a Dutch auction. The collateral for sale is fixed at open time, so a lower clearing price buys the same BTC for less USDC.
  2. The price steps down at a fixed interval until it reaches a floor, a percentage of the original debt, and stays there. Always read getCurrentPrice(positionId) rather than assuming the curve.
  3. buy(positionId, btcAddress) pays the current price in USDC, retires the debt, and records your BTC address as the collateral recipient.

BTC delivery is off-chain and not atomic. You pay USDC at purchase time, and the protocol's signer network broadcasts the Bitcoin payout to your address minutes to hours later. This is a trust assumption on the protocol operator, so size your exposure to it.

Networks and addresses

Key your config by chainId so switching networks switches addresses with no code branch. The market provider for the health check is read on-chain from markets(marketId), so you never hardcode it.

Base Sepolia (testnet)Base (mainnet)
chainId845328453
AuctionHouse0x2373abCF75bCBeee90e6556dac04727777ae0b910xE5Ff1E177dDE3FC33f5457855b14e6bD3B0C6566
VaultManager0x4b6b3Ea5936d144A3edC783aFDfdcC1f95f1818e0x0D5D12de1cC71060A38F25DD9d24DA1DD6eB705a
LiquidityPool0xed9613914c004Db819C8f0994a7388770E932Ef00xEE755F1BbcbF6e3260469D0f473522d71d3bdDda
Oracle0x8172Db638e71382c4bD3d0ed425011a09c73642A0x54DE003026dCa32E7cb28BDC79dDDcdB1bc1194D
USDC0x036CbD53842c5426634e7929541eC2318f3dCF7e0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913

USDC is the settlement asset (6 decimals). The oracle returns the BTC/USD liquidation rate scaled by 1e27. Public RPC endpoints are https://sepolia.base.org and https://mainnet.base.org; use a dedicated endpoint in production.

TypeScript
export const SURGE_AUCTION = {
  84532: {
    name: "base-sepolia",
    auctionHouse: "0x2373abCF75bCBeee90e6556dac04727777ae0b91",
    vaultManager: "0x4b6b3Ea5936d144A3edC783aFDfdcC1f95f1818e",
    liquidityPool: "0xed9613914c004Db819C8f0994a7388770E932Ef0",
    oracle: "0x8172Db638e71382c4bD3d0ed425011a09c73642A",
    usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  },
  8453: {
    name: "base",
    auctionHouse: "0xE5Ff1E177dDE3FC33f5457855b14e6bD3B0C6566",
    vaultManager: "0x0D5D12de1cC71060A38F25DD9d24DA1DD6eB705a",
    liquidityPool: "0xEE755F1BbcbF6e3260469D0f473522d71d3bdDda",
    oracle: "0x54DE003026dCa32E7cb28BDC79dDDcdB1bc1194D",
    usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  },
} as const;
 
export const USDC_DECIMALS = 6;
export const ORACLE_DECIMALS = 27;
export const SATS_PER_BTC = 100_000_000;

Minimal ABI

These fragments are the complete set an auction integration calls. The examples load the AuctionHouse ABI below as auctionAbi (TypeScript) or AUCTION_ABI (Python).

AuctionHouse ABI (getAuction, getCurrentPrice, buy, LogAuctionCreated)
[
  { "type": "function", "name": "getCurrentPrice", "stateMutability": "view",
    "inputs": [{ "name": "nftId", "type": "uint256" }],
    "outputs": [{ "name": "currentPrice", "type": "uint256" }] },
  { "type": "function", "name": "getAuction", "stateMutability": "view",
    "inputs": [{ "name": "nftId", "type": "uint256" }],
    "outputs": [
      { "name": "active", "type": "bool" },
      { "name": "token", "type": "address" },
      { "name": "debtToPay", "type": "uint256" },
      { "name": "collateralSatsForSale", "type": "uint256" },
      { "name": "originalOwner", "type": "address" },
      { "name": "createdAt", "type": "uint256" },
      { "name": "expiresAt", "type": "uint256" },
      { "name": "winnerEvm", "type": "address" },
      { "name": "winnerBtcAddress", "type": "bytes" }
    ] },
  { "type": "function", "name": "buy", "stateMutability": "nonpayable",
    "inputs": [
      { "name": "nftId", "type": "uint256" },
      { "name": "btcAddress", "type": "bytes" }
    ], "outputs": [] },
  { "type": "event", "name": "LogAuctionCreated", "inputs": [
      { "name": "nftId", "type": "uint256", "indexed": true },
      { "name": "token", "type": "address", "indexed": true },
      { "name": "debtToPay", "type": "uint256", "indexed": false },
      { "name": "collateralSatsForSale", "type": "uint256", "indexed": false },
      { "name": "originalOwner", "type": "address", "indexed": false },
      { "name": "expiresAt", "type": "uint256", "indexed": false } ] }
]
VaultManager ABI (checkHealth + health reads)
[
  { "type": "function", "name": "checkHealth", "stateMutability": "nonpayable",
    "inputs": [{ "name": "nftId", "type": "uint256" }], "outputs": [] },
  { "type": "function", "name": "getCollateralRatio", "stateMutability": "view",
    "inputs": [{ "name": "nftId", "type": "uint256" }],
    "outputs": [{ "name": "cr", "type": "uint256" }] },
  { "type": "function", "name": "positionMarket", "stateMutability": "view",
    "inputs": [{ "type": "uint256" }], "outputs": [{ "type": "uint256" }] },
  { "type": "function", "name": "getLegacyParams", "stateMutability": "view",
    "inputs": [],
    "outputs": [
      { "name": "minCR", "type": "uint256" },
      { "name": "liqThreshold", "type": "uint256" },
      { "name": "liqPenalty", "type": "uint256" }
    ] }
]
Oracle + LiquidityPool + USDC
[
  { "type": "function", "name": "getExchangeRateLiquidate", "stateMutability": "view",
    "inputs": [], "outputs": [{ "type": "uint256" }] },
  { "type": "function", "name": "markets", "stateMutability": "view",
    "inputs": [{ "type": "uint256" }],
    "outputs": [
      { "name": "provider", "type": "address" }, { "name": "token", "type": "address" },
      { "name": "active", "type": "bool" }, { "name": "totalSupplyShares", "type": "uint256" },
      { "name": "totalSupplyAssets", "type": "uint256" }, { "name": "totalPhysicalSupply", "type": "uint256" },
      { "name": "totalBorrowShares", "type": "uint256" }, { "name": "totalBorrowAssets", "type": "uint256" },
      { "name": "totalPhysicalBorrow", "type": "uint256" }, { "name": "supplyExchangeRate", "type": "uint256" },
      { "name": "borrowExchangeRate", "type": "uint256" }, { "name": "protocolEarnings", "type": "uint256" },
      { "name": "protocolEarningsAvailable", "type": "uint256" }, { "name": "originationFeeBps", "type": "uint256" },
      { "name": "reserveRateBps", "type": "uint256" }, { "name": "maxLtvBps", "type": "uint256" },
      { "name": "liquidationThresholdBps", "type": "uint256" }, { "name": "lastAccrueTime", "type": "uint256" }
    ] }
]

For USDC use any standard ERC-20 ABI (approve, allowance, balanceOf); it is built into viem as erc20Abi.

Discover auctions

Scan LogAuctionCreated for position ids, then read each with getAuction. Public Base RPCs cap eth_getLogs at 2000 blocks per request, so scan in chunks.

TypeScript
const logs = await client.getContractEvents({
  address: cfg.auctionHouse, abi: auctionAbi, eventName: "LogAuctionCreated",
  fromBlock, toBlock, // step in <= 2000-block windows
});
// the decoded field is nftId, the name the contracts use for a position id
const ids = logs.map((l) => l.args.nftId as bigint);
 
// getAuction returns 9 values positionally, so destructure — it is not an object
const [active, , , , , , , winnerEvm] = await client.readContract({
  address: cfg.auctionHouse, abi: auctionAbi, functionName: "getAuction", args: [positionId],
});
 
const buyable = active && winnerEvm === "0x0000000000000000000000000000000000000000";

buy checks only active, winnerEvm and a non-empty BTC address, so do not filter on expiresAt. Past that point the price has bottomed out at the floor rather than closing, which usually makes those the cheapest auctions on the board.

Read the current price and quote profit

getCurrentPrice is the live Dutch price in USDC. Value the collateral at the oracle rate and apply your own margin.

TypeScript
import { formatUnits } from "viem";
 
const priceRaw = await client.readContract({
  address: cfg.auctionHouse, abi: auctionAbi, functionName: "getCurrentPrice", args: [positionId],
});
const priceUsdc = Number(formatUnits(priceRaw, 6));
 
// getCurrentPrice returns 0 for an inactive auction and reverts on some older ones.
// buy() prices internally, so a 0 here means unbuyable — never treat it as free.
if (priceUsdc <= 0) return;
 
const rateRaw = await client.readContract({
  address: cfg.oracle, abi: oracleAbi, functionName: "getExchangeRateLiquidate",
});
const btcPriceUsd = Number(formatUnits(rateRaw, 27));
 
// getAuction returns 9 values positionally; index 3 is collateralSatsForSale
const [, , , collateralSatsForSale] = await client.readContract({
  address: cfg.auctionHouse, abi: auctionAbi, functionName: "getAuction", args: [positionId],
});
 
const collateralBtc = Number(collateralSatsForSale) / SATS_PER_BTC;
const collateralValueUsd = collateralBtc * btcPriceUsd;
const netProfit = collateralValueUsd - priceUsdc - EST_BTC_FEE_USD;
const profitable = netProfit >= collateralValueUsd * (TARGET_MARGIN_BPS / 10_000);

The oracle rate is the protocol's liquidation rate, not the spot price you would sell BTC at, so "profitable" is relative to that basis. Because delivery lands hours after purchase, BTC price can move against you in between. Both are real risks; see the security notes.

Buy

Approve USDC to the AuctionHouse, then buy. The BTC address is passed as UTF-8 bytes. Validate it against the network first: a wrong-network payout address means the seized BTC is unspendable.

TypeScript
import { parseUnits, toHex } from "viem";
 
// 1) approve (exact price, or a working-capital cap). Spender = AuctionHouse.
await wallet.writeContract({
  address: cfg.usdc, abi: erc20Abi, functionName: "approve",
  args: [cfg.auctionHouse, priceRaw],
});
 
// 2) buy at the current price; btc address as UTF-8 bytes
await wallet.writeContract({
  address: cfg.auctionHouse, abi: auctionAbi, functionName: "buy",
  args: [positionId, toHex(btcAddress)],
});

Read the current price and your USDC balance right before you send, and clamp to the live price; the auction is competitive, so a buy can lose to a faster bidder even after a clean pre-flight.

Open an auction (keeper)

checkHealth(positionId) opens a Dutch auction on an unhealthy position and is permissionless. On a healthy position it emits LogHealthChecked and returns without opening anything, so an unchecked call burns gas silently. It reverts only if the position is inactive or already in liquidation. Pre-check before sending. The health test is the collateral ratio (scaled so 1e16 = 100%) against the position market's threshold.

TypeScript
const cr = await client.readContract({
  address: cfg.vaultManager, abi: vaultAbi, functionName: "getCollateralRatio", args: [positionId],
});
const marketId = await client.readContract({
  address: cfg.vaultManager, abi: vaultAbi, functionName: "positionMarket", args: [positionId],
});
// threshold: legacy market (marketId == MAX_UINT256) uses getLegacyParams().liqThreshold;
// otherwise (10000*10000) / markets(marketId).liquidationThresholdBps
const liquidatable = cr !== 0n && cr < crThresholdBps * 10n ** 12n;
 
if (liquidatable) {
  await wallet.writeContract({
    address: cfg.vaultManager, abi: vaultAbi, functionName: "checkHealth", args: [positionId],
  });
}

Security and operational notes

  • BTC delivery is a trust assumption. You pay USDC now and the signer network delivers BTC later. Size exposure accordingly.
  • Price risk between buy and delivery. BTC can move against you before the payout confirms.
  • Oracle basis. Profit is measured against the liquidation oracle, not your exit venue.
  • Approvals. Approve the exact amount you plan to spend, spender = AuctionHouse, from a dedicated wallet funded only with working capital and gas.
  • Competition. Buying is permissionless and races other bidders; a stale pre-flight just means a revert, never a loss.
  • Dust economics. Every win needs its own BTC delivery with its own fee, so skip auctions whose margin does not clear your fee plus target.

For a TypeScript wrapper of all of the above, use the Auction SDK. For an end-to-end keeper you can deploy, see the reference surge-liquidation-bot.

v0.1.0