Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/db/init/06_liquidation_integration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE auction_events ADD COLUMN IF NOT EXISTS pool_address TEXT;
ALTER TABLE auction_events ADD COLUMN IF NOT EXISTS borrower TEXT;
ALTER TABLE auction_events ADD COLUMN IF NOT EXISTS liquidation_type TEXT DEFAULT 'dutch_auction';
ALTER TABLE auction_events ADD COLUMN IF NOT EXISTS discount_floor_bps INTEGER;
ALTER TABLE auction_events ADD COLUMN IF NOT EXISTS min_price_bps INTEGER;
121 changes: 121 additions & 0 deletions api/src/controllers/liquidationAuction.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Request, Response, Router } from 'express';

const router = Router();

interface AuctionData {
id: number;
pool: string;
borrower: string;
collateralAsset: string;
debtAsset: string;
collateralAmount: string;
debtAmount: string;
oraclePrice: string;
currentPrice: string;
startTime: number;
endTime: number;
status: 'Active' | 'Settled' | 'Expired';
highestBidder: string | null;
highestBidAmount: string | null;
}

const auctions: Map<number, AuctionData> = new Map();
let nextId = 1;

router.post('/', (req: Request, res: Response) => {
const { pool, borrower, collateralAsset, debtAsset, collateralAmount, debtAmount, oraclePrice, durationSecs } = req.body;

const id = nextId++;
const now = Math.floor(Date.now() / 1000);

const auction: AuctionData = {
id,
pool,
borrower,
collateralAsset,
debtAsset,
collateralAmount,
debtAmount,
oraclePrice,
currentPrice: oraclePrice,
startTime: now,
endTime: now + (durationSecs || 3600),
status: 'Active',
highestBidder: null,
highestBidAmount: null,
};

auctions.set(id, auction);

res.status(201).json({ success: true, auction });
});

router.get('/active', (req: Request, res: Response) => {
const active = Array.from(auctions.values()).filter(a => a.status === 'Active');
res.json({ auctions: active });
});

router.get('/pool/:poolAddress', (req: Request, res: Response) => {
const poolAuctions = Array.from(auctions.values()).filter(
a => a.pool === req.params.poolAddress
);
res.json({ auctions: poolAuctions });
});

router.get('/:id', (req: Request, res: Response) => {
const auction = auctions.get(parseInt(req.params.id));
if (!auction) return res.status(404).json({ error: 'Auction not found' });
res.json({ auction });
});

router.post('/bid', (req: Request, res: Response) => {
const { auctionId, bidder, repayAmount } = req.body;
const auction = auctions.get(auctionId);

if (!auction) return res.status(404).json({ error: 'Auction not found' });
if (auction.status !== 'Active') return res.status(400).json({ error: 'Auction not active' });

const now = Math.floor(Date.now() / 1000);
if (now >= auction.endTime) {
return res.status(400).json({ error: 'Auction has ended' });
}

const elapsed = now - auction.startTime;
const duration = auction.endTime - auction.startTime;
const oraclePrice = parseFloat(auction.oraclePrice);
const minPrice = oraclePrice * 0.7;
const priceRange = oraclePrice - minPrice;
const priceDrop = (priceRange * elapsed) / duration;
const currentPrice = (oraclePrice - priceDrop).toFixed(7);

const repayNum = parseFloat(repayAmount);
const collateralRatio = parseFloat(auction.collateralAmount) / parseFloat(auction.debtAmount);
const collateralReceived = (repayNum * collateralRatio).toFixed(7);

auction.status = 'Settled';
auction.highestBidder = bidder;
auction.highestBidAmount = repayAmount;
auction.currentPrice = currentPrice;

res.json({
success: true,
collateralReceived,
debtRepaid: repayAmount,
currentPrice,
});
});

router.post('/:id/expire', (req: Request, res: Response) => {
const auction = auctions.get(parseInt(req.params.id));
if (!auction) return res.status(404).json({ error: 'Auction not found' });

const now = Math.floor(Date.now() / 1000);
const originalDuration = auction.endTime - auction.startTime;

auction.endTime = now + originalDuration * 2;
auction.currentPrice = (parseFloat(auction.oraclePrice) * 0.5).toFixed(7);

res.json({ success: true, auction });
});

export default router;
96 changes: 96 additions & 0 deletions api/src/services/liquidationAuction.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import axios from 'axios';

interface AuctionConfig {
durationSecs: number;
minPriceBps: number;
discountFloorBps: number;
maxParallelAuctions: number;
}

interface LiquidationAuction {
id: number;
pool: string;
borrower: string;
collateralAsset: string;
debtAsset: string;
collateralAmount: string;
debtAmount: string;
oraclePrice: string;
currentPrice: string;
startTime: number;
endTime: number;
status: 'Active' | 'Settled' | 'Expired';
highestBidder: string | null;
highestBidAmount: string | null;
}

export class LiquidationAuctionService {
private contractEndpoint: string;

constructor(contractEndpoint: string) {
this.contractEndpoint = contractEndpoint;
}

async createAuction(params: {
pool: string;
borrower: string;
collateralAsset: string;
debtAsset: string;
collateralAmount: string;
debtAmount: string;
oraclePrice: string;
durationSecs?: number;
}): Promise<number> {
const res = await axios.post(`${this.contractEndpoint}/auctions`, params);
return res.data.auction.id;
}

async getActiveAuctions(): Promise<LiquidationAuction[]> {
const res = await axios.get(`${this.contractEndpoint}/auctions/active`);
return res.data.auctions || [];
}

async getCurrentPrice(auctionId: number): Promise<string> {
const res = await axios.get(`${this.contractEndpoint}/auctions/${auctionId}`);
const auction = res.data.auction;
const now = Math.floor(Date.now() / 1000);

if (now >= auction.endTime) {
return (parseFloat(auction.oraclePrice) * 0.7).toFixed(7);
}

const elapsed = now - auction.startTime;
const duration = auction.endTime - auction.startTime;
const priceRange = parseFloat(auction.oraclePrice) * 0.3;
const priceDrop = (priceRange * elapsed) / duration;
return (parseFloat(auction.oraclePrice) - priceDrop).toFixed(7);
}

async placeBid(auctionId: number, bidder: string, repayAmount: string): Promise<{
collateralReceived: string;
debtRepaid: string;
}> {
const res = await axios.post(`${this.contractEndpoint}/auctions/bid`, {
auctionId,
bidder,
repayAmount,
});
return res.data;
}

calculateDiscountBps(oraclePrice: string, currentPrice: string): number {
const oracle = parseFloat(oraclePrice);
const current = parseFloat(currentPrice);
if (oracle <= 0) return 0;
return Math.round(((oracle - current) * 10000) / oracle);
}

estimateProfit(auction: LiquidationAuction): number {
const collateralVal = parseFloat(auction.collateralAmount) * parseFloat(auction.currentPrice) / 10000;
return collateralVal - parseFloat(auction.debtAmount);
}
}

export const liquidationAuctionService = new LiquidationAuctionService(
process.env.CONTRACT_API_URL || 'http://localhost:3001/api'
);
7 changes: 7 additions & 0 deletions bots/liquidation-bot/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ POLL_INTERVAL_MS=5000
DRY_RUN=true
MAX_CONCURRENT_LIQUIDATIONS=3

# Dutch Auction Strategy
BID_STRATEGY=earliest_discount
# Options: earliest_discount, highest_premium, fastest_fill
MIN_DISCOUNT_BPS=500
MAX_AUCTION_BID_AMOUNT=1000000000
ANALYTICS_ENABLED=true

# WebSocket
WS_URL=ws://localhost:3000/api/ws/health-updates

Expand Down
Loading