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
47 changes: 47 additions & 0 deletions api/db/init/09_reputation_deployer.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
-- Deployer Reputation Schema
-- Issue #606: Lending pool deployer and user reputation system

CREATE TABLE IF NOT EXISTS deployer_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
deployer_address TEXT NOT NULL UNIQUE,
pool_count INTEGER NOT NULL DEFAULT 0,
total_tvl NUMERIC NOT NULL DEFAULT 0,
total_borrows NUMERIC NOT NULL DEFAULT 0,
total_defaults INTEGER NOT NULL DEFAULT 0,
flagged BOOLEAN NOT NULL DEFAULT FALSE,
reputation_score INTEGER NOT NULL DEFAULT 0,
reputation_tier TEXT NOT NULL DEFAULT 'Bronze',
bond_amount NUMERIC NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_deployer_profiles_address ON deployer_profiles (deployer_address);
CREATE INDEX IF NOT EXISTS idx_deployer_profiles_score ON deployer_profiles (reputation_score DESC);

CREATE TABLE IF NOT EXISTS pool_reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pool_address TEXT NOT NULL,
reviewer_address TEXT NOT NULL,
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
comment TEXT,
verified_deposit BOOLEAN NOT NULL DEFAULT FALSE,
deposit_amount NUMERIC,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_pool_reviews_pool ON pool_reviews (pool_address);
CREATE INDEX IF NOT EXISTS idx_pool_reviews_reviewer ON pool_reviews (reviewer_address);
CREATE UNIQUE INDEX IF NOT EXISTS idx_pool_reviews_unique ON pool_reviews (pool_address, reviewer_address);

CREATE TABLE IF NOT EXISTS reputation_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
deployer_address TEXT NOT NULL,
event_type TEXT NOT NULL,
pool_address TEXT,
details JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_reputation_events_deployer ON reputation_events (deployer_address);
CREATE INDEX IF NOT EXISTS idx_reputation_events_type ON reputation_events (event_type);
102 changes: 102 additions & 0 deletions api/src/controllers/reputation/deployerReputation.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Request, Response, Router } from 'express';

const router = Router();

interface DeployerProfileRecord {
deployerAddress: string;
poolCount: number;
totalTvl: number;
totalBorrows: number;
totalDefaults: number;
flagged: boolean;
reputationScore: number;
reputationTier: string;
bondAmount: number;
}

const deployers: Map<string, DeployerProfileRecord> = new Map();
const reviews: Map<string, { reviewer: string; rating: number; comment: string | null; verifiedDeposit: boolean; depositAmount: number | null; createdAt: number }[]> = new Map();

router.get('/:address', (req: Request, res: Response) => {
const profile = deployers.get(req.params.address);
if (!profile) return res.status(404).json({ error: 'Deployer not found' });
res.json({ profile });
});

router.get('/leaderboard', (req: Request, res: Response) => {
const sorted = Array.from(deployers.values()).sort((a, b) => b.reputationScore - a.reputationScore);
res.json({ profiles: sorted });
});

router.post('/', (req: Request, res: Response) => {
const { deployerAddress, poolCount, totalTvl, totalBorrows, totalDefaults, bondAmount } = req.body;

let score = 0;
score += Math.min(300, (poolCount || 0) * 30);
score += Math.min(300, Math.round((totalTvl || 0) / 100000));
score += Math.min(200, Math.round((totalBorrows || 0) / 100000));
score -= Math.min(200, (totalDefaults || 0) * 50);
score = Math.max(0, Math.min(1000, score));

const tier = score >= 750 ? 'Platinum' : score >= 500 ? 'Gold' : score >= 250 ? 'Silver' : 'Bronze';

const profile: DeployerProfileRecord = {
deployerAddress,
poolCount: poolCount || 0,
totalTvl: totalTvl || 0,
totalBorrows: totalBorrows || 0,
totalDefaults: totalDefaults || 0,
flagged: false,
reputationScore: score,
reputationTier: tier,
bondAmount: bondAmount || 0,
};

deployers.set(deployerAddress, profile);
res.status(201).json({ success: true, profile });
});

router.post('/flag', (req: Request, res: Response) => {
const { deployerAddress } = req.body;
const profile = deployers.get(deployerAddress);
if (!profile) return res.status(404).json({ error: 'Deployer not found' });

profile.flagged = true;
deployers.set(deployerAddress, profile);
res.json({ success: true, profile });
});

router.post('/reviews', (req: Request, res: Response) => {
const { poolAddress, reviewerAddress, rating, comment } = req.body;

if (rating < 1 || rating > 5) {
return res.status(400).json({ error: 'Rating must be between 1 and 5' });
}

const key = `pool:${poolAddress}`;
const poolReviews = reviews.get(key) || [];

const existing = poolReviews.find(r => r.reviewer === reviewerAddress);
if (existing) {
return res.status(400).json({ error: 'Review already exists from this reviewer' });
}

poolReviews.push({
reviewer: reviewerAddress,
rating,
comment: comment || null,
verifiedDeposit: false,
depositAmount: null,
createdAt: Math.floor(Date.now() / 1000),
});

reviews.set(key, poolReviews);
res.status(201).json({ success: true, review: poolReviews[poolReviews.length - 1] });
});

router.get('/reviews/:poolAddress', (req: Request, res: Response) => {
const key = `pool:${req.params.poolAddress}`;
res.json({ reviews: reviews.get(key) || [] });
});

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

interface DeployerProfile {
deployerAddress: string;
poolCount: number;
totalTvl: number;
totalBorrows: number;
totalDefaults: number;
flagged: boolean;
reputationScore: number;
reputationTier: 'Bronze' | 'Silver' | 'Gold' | 'Platinum';
bondAmount: number;
}

interface PoolReview {
poolAddress: string;
reviewerAddress: string;
rating: number;
comment: string | null;
verifiedDeposit: boolean;
depositAmount: number | null;
createdAt: number;
}

export class DeployerReputationService {
private contractEndpoint: string;

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

async getDeployerProfile(deployerAddress: string): Promise<DeployerProfile | null> {
try {
const res = await axios.get(`${this.contractEndpoint}/deployers/${deployerAddress}`);
return res.data.profile;
} catch {
return null;
}
}

async getDeployerLeaderboard(): Promise<DeployerProfile[]> {
const res = await axios.get(`${this.contractEndpoint}/deployers/leaderboard`);
return res.data.profiles || [];
}

async flagDeployer(adminAddress: string, deployerAddress: string): Promise<boolean> {
const res = await axios.post(`${this.contractEndpoint}/deployers/flag`, {
adminAddress,
deployerAddress,
});
return res.data.success;
}

async submitReview(reviewerAddress: string, poolAddress: string, rating: number, comment?: string): Promise<boolean> {
const res = await axios.post(`${this.contractEndpoint}/reviews`, {
reviewerAddress,
poolAddress,
rating,
comment,
});
return res.data.success;
}

async getPoolReviews(poolAddress: string): Promise<PoolReview[]> {
const res = await axios.get(`${this.contractEndpoint}/reviews/${poolAddress}`);
return res.data.reviews || [];
}

calculateReputationScore(profile: {
poolCount: number;
totalTvl: number;
totalDefaults: number;
totalBorrows: number;
}): { score: number; tier: string } {
let score = 0;

score += Math.min(300, profile.poolCount * 30);

const tvlScore = Math.min(300, Math.round(profile.totalTvl / 100000));
score += tvlScore;

const borrowScore = Math.min(200, Math.round(profile.totalBorrows / 100000));
score += borrowScore;

const defaultPenalty = Math.min(200, profile.totalDefaults * 50);
score = Math.max(0, score - defaultPenalty);

score = Math.min(1000, score);

const tier = score >= 750 ? 'Platinum' : score >= 500 ? 'Gold' : score >= 250 ? 'Silver' : 'Bronze';

return { score, tier };
}
}

export const deployerReputationService = new DeployerReputationService(
process.env.CONTRACT_API_URL || 'http://localhost:3001/api'
);
Loading