Aave V3 청산 추적기
Aave V3 프로토콜에서 발생하는 청산 이벤트를 실시간으로 모니터링하고 분석합니다. 이 프로젝트는 Quicknode Streams 활용한 서버리스 블록체인 데이터 스트리밍의 강력한 기능을 보여줍니다.
서론
Aave V3 프로토콜에서 발생하는 청산 이벤트를 실시간으로 모니터링하고 분석합니다. 이 프로젝트는 QuicknodeStreams를 활용한 서버리스 블록체인 데이터 스트리밍의 강력한 성능을 보여줍니다.
| 대시보드 개요 | 청산표 |
|---|---|
![]() |
주요 기능
- 📊 실시간 청산 모니터링
- 💰 상세한 지표 및 분석
- 📈 과거 청산 추세
- 🔄 자산 배분 분석
- ⚡ 서버리스 아키텍처
Aave 풀 계약 및 청산 요청 이벤트
Aave V3 풀 계약 개요
그 Pool.sol 계약 ( Ethereum Mainnet용, 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2) Aave V3 프로토콜 내의 이 기능은 대출 및 차입 시스템의 핵심입니다. 이 기능을 통해 예치, 차입, 상환 및 청산이 이루어집니다.
주목할 이벤트: LiquidationCall
LiquidationCall 이벤트는 Aave 프로토콜에서 청산이 발생할 때 발생합니다. 이 이벤트는 압류된 담보와 상환된 부채를 포함하여 청산 거래에 대한 중요한 정보를 제공합니다.
// 이벤트 시그니처
// 0xe413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286
청산 요청(
주소 담보자산,
주소 debtAsset,
주소 user,
uint256 상환해야 할 부채,
uint256 청산된담보금액,
address 청산인,
bool receiveAToken
)
이벤트 매개변수
| 매개변수 | 유형 | 설명 |
|---|---|---|
담보자산 | 주소 | 담보로 압류되는 자산의 주소. |
부채자산 | 주소 | 청산인이 상환하고 있는 자산의 주소. |
사용자 | 주소 | 청산 절차가 진행 중인 차주의 주소. |
상환해야 할 부채 | uint256 | 청산 과정에서 상환되는 채무의 금액. |
청산된 담보 금액 | uint256 | 청산인이 압류한 담보물의 금액. |
청산인 | 주소 | 청산을 수행하고 있는 청산인의 주소. |
receiveAToken | bool | 만약 true, 청산인은 담보 대신 aTokens를 받습니다. |
저희는 다음의 디코딩된 로그를 사용할 예정입니다. 청산 요청 Mainnet에서 청산 내역을 추적하기 위한 이벤트.
작동 원리
이 애플리케이션은 Aave V3 청산 이벤트를 추적하고 분석하기 위해 완전한 서버리스 아키텍처를 사용합니다:
- 데이터 수집: Streams 는 Aave V3의 청산 이벤트에 대한 원시 블록체인 이벤트 데이터를 과거 데이터와 실시간 데이터 모두에서 모니터링합니다.
- EVM 디코딩: StreamsEVM 디코더를 사용하여 원시 이벤트 데이터를 디코딩하고 관련 정보를 추출합니다
- 데이터 처리: 사용법
viem그리고 webhooks 통해, 들어오는 데이터를 일시적으로 수신하고, 토큰 가격 및 토큰 세부 정보를 조회하여 디코딩된 이벤트 데이터를 처리합니다. - 데이터 저장: 보강된 이벤트 데이터는 PostgreSQL 데이터베이스에 자동으로 저장됩니다.
- 프론트엔드 표시: React 애플리케이션이 데이터를 가져와 실시간으로 업데이트하며 표시합니다.
기술 스택
-
프론트엔드
- React
- TypeScript
- 데이터 가져오기를 위한 TanStack 쿼리
- 스타일링을 위한 Tailwind CSS
- UI 구성 요소를 위한 shadcn/ui
- 데이터 시각화를 위한 Recharts
-
백엔드
건축

이 아키텍처는 시스템이 Quicknode 인프라를 활용하여 블록체인 데이터를 효율적으로 처리하는 방식을 보여줍니다:
- Quicknode Aave V3 계약 이벤트를 모니터링합니다.
- 이벤트는 처리를 위해 웹훅 서버로 전송됩니다
- 데이터에는 토큰 및 가격 정보가 추가됩니다.
- 처리된 데이터는 Supabase에 저장됩니다.
- 프론트엔드는 실시간 데이터를 가져와 표시합니다
데이터 수집 및 처리
Quicknode Streams
Quicknode Streams 노드를 실행하지 않고도 실시간 블록체인 데이터를Streams :
- Aave V3 계약에서 청산 발생 여부를 모니터링합니다
- 관련 있는 이벤트만 필터링하여 전달합니다
- 재시도 메커니즘을 통해 안정적인 데이터 전달을 보장합니다
- 인프라 유지 관리가 전혀 필요하지 않습니다
웹훅 서버
저희는 웹훅 서버를 사용하여 수신되는 streams 수신하고 유효성을 확인하고 있습니다:
또한, 당사는 다음과 같은 데이터 항목을 제공하여 streams 보강합니다:
- 토큰 이름/기호 (WETH, USDC 등)
- 청산 시점의 USD 가격
- 서식 지정된 수치 (bigint를 소수형으로 변환)
시작하기
선행 조건
설치
- 저장소를 복제하세요:
git https://github.com/quiknode-labs/qn-guide-examples.git 복제
cdethereum
- 의존성 설치:
npm 설치
-
Supabase에서 새 프로젝트와 데이터베이스를 생성합니다. 나중에 사용할 수 있도록 데이터베이스 비밀번호를 저장해 둡니다.
-
만들기
.env파일을 열고 다음 내용을 추가하세요. Supabase URL 및 게시용 키. Supabase 대시보드에서 다음을 클릭하면 URL과 키를 확인할 수 있습니다. 연결 프로젝트 내의 버튼.
VITE_SUPABASE_URL=사용자의_Supabase_URL
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=사용자의_Supabase_게시 가능 키
- 제공된 스키마를 사용하여 데이터베이스를 설정하십시오:
CREATE TABLE liquidations (
id SERIAL PRIMARY KEY,
liquidator_address character varying(42) NOT NULL,
liquidated_wallet character varying(42) NOT NULL,
collateral_asset character varying(42) NOT NULL,
collateral_asset_name character varying(100),
collateral_asset_symbol character varying(20),
collateral_asset_price numeric(20,6),
collateral_seized_amount numeric(78,18),
debt_asset character varying(42) NOT NULL,
debt_asset_name character varying(100),
debt_asset_symbol character varying(20),
debt_asset_price numeric(20,6),
debt_repaid_amount numeric(78,18),
transaction_hash character varying(66) NOT NULL,
block_number bigint NOT NULL,
receive_a_token boolean NOT NULL,
timestamp timestamp without time zone NOT NULL
);
- 개발 서버를 시작합니다:
npm run dev
웹훅 서버 설정
- 다음 명령을 실행하여 웹훅 서버를 시작하세요.
npm run server. - 별도의 터미널에서 다음 명령을 실행하십시오.
ngrok http YOUR_PORT_NUMBER. 이 프로젝트의 경우 기본값은 다음과 같이 설정되어 있습니다.3005하지만 원하시면 이 부분을 자유롭게 수정하셔도 됩니다..env. - ngrok 서버가 설정되면, 생성된 ngrok 링크(예:
https://abcd.ngrok-free.app)를 추가하고/웹훅Streams Streams에서 들어오는 데이터를 수신 대기하는 endpoint .
Quicknode Streams 설정하기
- quicknode.com에서 Quicknode 만드세요
- 새 스트림 설정하기:
- 체인 및 네트워크(예: Ethereum Mainnet)를 선택하세요.
- 데이터셋을 다음과 같이 선택하십시오.
영수증이 첨부된 블록 - 시작 및 종료 블록 번호를 선택하십시오(실시간 데이터의 경우,
스트림 종료~처럼끝나지 않는다) - '페이로드 수정' 섹션을 선택하고 다음 Streams 코드를 추가하십시오.
- 테스트 블록을 선택하십시오(예:
18983377)을 클릭하고테스트 실행 - 목적지를 다음과 같이 선택하십시오.
웹훅그리고 다음을 사용하여 웹훅 URL을 추가하세요./웹훅endpoint . - 선택 사항으로, 다음 위치에 스트림의 보안 토큰을 추가할 수 있습니다.
.env~을 활용하다 고급 보안 기능.
계약 주소, 테스트 블록 및 기타 구성 사항은 진행 중인 구체적인 프로젝트와 네트워크에 따라 달라질 수 있습니다. 이 앱에서는Mainnet Aave V3 프로토콜을 사용합니다.
아래에서 Streams 함수의 전체 코드를 확인하거나, 여기에서 볼 수 있습니다.
Streams 코드 보기
// Filtering function on Streams
function main(stream) {
try {
// Aave V3 LiquidationCall ABI
const aaveLiquidationAbi = `[{
"anonymous": false,
"inputs": [
{"indexed": true, "type": "address", "name": "collateralAsset"},
{"indexed": true, "type": "address", "name": "debtAsset"},
{"indexed": true, "type": "address", "name": "user"},
{"indexed": false, "type": "uint256", "name": "debtToCover"},
{"indexed": false, "type": "uint256", "name": "liquidatedCollateralAmount"},
{"indexed": false, "type": "address", "name": "liquidator"},
{"indexed": false, "type": "bool", "name": "receiveAToken"}
],
"name": "LiquidationCall",
"type": "event"
}]`;
// Pool.sol addresses for Aave V3 (lowercase)
const normalizedAddresses = ["0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2"];
// Handle both payload and stream parameter styles
const data = stream?.data ? stream.data : stream;
// Early validation - return null to skip (per Quicknode docs)
if (!data?.[0]?.block?.timestamp || !data?.[0]?.receipts?.length) {
return null;
}
const timestamp = parseInt(data[0].block.timestamp, 16);
// Decode receipts
const decodedReceipts = decodeEVMReceipts(data[0].receipts, [aaveLiquidationAbi]);
// Validate decoded receipts
if (!decodedReceipts || !Array.isArray(decodedReceipts) || decodedReceipts.length === 0) {
return null;
}
// Filter for liquidation events
const filteredLogs = decodedReceipts
.filter(r => r?.decodedLogs?.length > 0)
.flatMap(r => r.decodedLogs)
.filter(log =>
log?.name === "LiquidationCall" &&
log?.address &&
normalizedAddresses.includes(log.address.toLowerCase())
);
// Return data if found, null otherwise (per Quicknode docs)
return filteredLogs?.length > 0
? { timestamp, filteredLogs }
: null;
} catch (e) {
// Return null to skip on error (per Quicknode docs)
return null;
}
}
Quicknode
- Quicknode 로그인하거나 무료 체험 계정을 생성하세요.
- 체인 및 네트워크(예: Ethereum Mainnet) Endpoint 새로운 Endpoint 생성하세요.
- RPC URL을 복사하여 다음 위치에 붙여넣으십시오.
QUICKNODE의 변수.env파일.
데이터베이스 설정
- Supabase 대시보드에 로그인하세요.
- 프로젝트로 이동한 후 ‘연결’ 버튼을 클릭하세요.
- ‘연결 정보’에서 ‘앱 프레임워크’를 클릭한 다음, ‘프레임워크’를 ‘React ’로 설정하고 ‘Vite 프레임워크 사용’을 선택합니다.
- 복사
VITE_SUPABASE_URL그리고VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY~에.env.
PostgreSQL 함수 설정하기
데이터베이스에서 데이터를 가져오려면 다음 PostgreSQL 쿼리를 사용하여 몇 가지 함수를 생성할 수 있습니다. 이 함수들은 프론트엔드에서 데이터를 표시하는 데 사용됩니다.
PostgreSQL 함수 코드 보기
-- get_asset_distributions
CREATE OR REPLACE FUNCTION get_asset_distributions(time_range text)
RETURNS json AS $$
DECLARE
interval_start timestamp;
BEGIN
interval_start := CASE time_range
WHEN '24h' THEN NOW() - INTERVAL '24 hours'
WHEN '7d' THEN NOW() - INTERVAL '7 days'
WHEN '30d' THEN NOW() - INTERVAL '30 days'
WHEN '365d' THEN NOW() - INTERVAL '365 days'
ELSE NOW() - INTERVAL '30 days'
END;
RETURN json_build_object(
'topCollateralAssets', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
collateral_asset_symbol as symbol,
COUNT(*) as count,
SUM(collateral_seized_amount * collateral_asset_price) as totalValueUSD,
SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as totalProfitUSD,
(COUNT(*)::float / (SELECT COUNT(*) FROM liquidations WHERE timestamp >= interval_start)::float * 100) as percentageOfTotal
FROM liquidations
WHERE timestamp >= interval_start
GROUP BY collateral_asset_symbol
ORDER BY count DESC
LIMIT 5
) t
),
'topDebtAssets', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
debt_asset_symbol as symbol,
COUNT(*) as count,
SUM(debt_repaid_amount * debt_asset_price) as totalValueUSD,
SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as totalProfitUSD,
(COUNT(*)::float / (SELECT COUNT(*) FROM liquidations WHERE timestamp >= interval_start)::float * 100) as percentageOfTotal
FROM liquidations
WHERE timestamp >= interval_start
GROUP BY debt_asset_symbol
ORDER BY count DESC
LIMIT 5
) t
)
);
END;
$$ LANGUAGE plpgsql;
-- get_metrics_overview
CREATE OR REPLACE FUNCTION get_metrics_overview()
RETURNS json AS $$
BEGIN
RETURN json_build_object(
'total24h', (
SELECT json_build_object(
'count', COUNT(*),
'valueUSD', COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0),
'profitUSD', COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0)
)
FROM liquidations
WHERE timestamp >= NOW() - INTERVAL '24 hours'
),
'total7d', (
SELECT json_build_object(
'count', COUNT(*),
'valueUSD', COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0),
'profitUSD', COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0)
)
FROM liquidations
WHERE timestamp >= NOW() - INTERVAL '7 days'
),
'total30d', (
SELECT json_build_object(
'count', COUNT(*),
'valueUSD', COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0),
'profitUSD', COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0)
)
FROM liquidations
WHERE timestamp >= NOW() - INTERVAL '30 days'
),
'total365d', (
SELECT json_build_object(
'count', COUNT(*),
'valueUSD', COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0),
'profitUSD', COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0)
)
FROM liquidations
WHERE timestamp >= NOW() - INTERVAL '365 days'
),
'topLiquidators', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
liquidator_address as address,
COUNT(*) as count,
SUM(debt_repaid_amount * debt_asset_price) as totalValueUSD,
AVG(debt_repaid_amount * debt_asset_price) as avgLiquidationUSD,
SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as totalProfitUSD,
AVG((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as avgProfitUSD
FROM liquidations
GROUP BY liquidator_address
ORDER BY totalProfitUSD DESC
LIMIT 5
) t
),
'topLiquidatedUsers', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
liquidated_wallet as address,
COUNT(*) as count,
SUM(debt_repaid_amount * debt_asset_price) as totalValueUSD,
AVG(debt_repaid_amount * debt_asset_price) as avgLiquidationUSD,
SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as totalLossUSD,
AVG((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) as avgLossUSD
FROM liquidations
GROUP BY liquidated_wallet
ORDER BY count DESC
LIMIT 5
) t
),
'largestLiquidations', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
transaction_hash as txHash,
timestamp,
debt_repaid_amount * debt_asset_price as valueUSD,
(collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price) as profitUSD,
collateral_asset_symbol as collateralAsset,
debt_asset_symbol as debtAsset,
liquidator_address as liquidator,
liquidated_wallet as liquidatedUser
FROM liquidations
ORDER BY debt_repaid_amount * debt_asset_price DESC
LIMIT 5
) t
),
'mostProfitableLiquidations', (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
transaction_hash as txHash,
timestamp,
debt_repaid_amount * debt_asset_price as valueUSD,
(collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price) as profitUSD,
collateral_asset_symbol as collateralAsset,
debt_asset_symbol as debtAsset,
liquidator_address as liquidator,
liquidated_wallet as liquidatedUser
FROM liquidations
ORDER BY ((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)) DESC
LIMIT 5
) t
)
);
END;
$$ LANGUAGE plpgsql;
-- get_liquidation_trends
CREATE OR REPLACE FUNCTION get_liquidation_trends(time_range text)
RETURNS json AS $$
DECLARE
interval_start timestamp;
grouping_interval text;
BEGIN
SELECT
CASE time_range
WHEN '24h' THEN NOW() - INTERVAL '24 hours'
WHEN '7d' THEN NOW() - INTERVAL '7 days'
WHEN '30d' THEN NOW() - INTERVAL '30 days'
WHEN '365d' THEN NOW() - INTERVAL '365 days'
ELSE NOW() - INTERVAL '30 days'
END,
CASE time_range
WHEN '24h' THEN 'hour'
WHEN '7d' THEN 'day'
WHEN '30d' THEN 'day'
WHEN '365d' THEN 'month'
ELSE 'day'
END
INTO interval_start, grouping_interval;
RETURN (
SELECT json_agg(row_to_json(t))
FROM (
SELECT
date_trunc(grouping_interval, timestamp) as date,
COUNT(*) as count,
COALESCE(SUM(debt_repaid_amount * debt_asset_price), 0) as totalValueUSD,
COALESCE(SUM((collateral_seized_amount * collateral_asset_price) - (debt_repaid_amount * debt_asset_price)), 0) as totalProfitUSD
FROM liquidations
WHERE timestamp >= interval_start
GROUP BY date_trunc(grouping_interval, timestamp)
ORDER BY date_trunc(grouping_interval, timestamp)
) t
);
END;
$$ LANGUAGE plpgsql;
PostgreSQL 인덱스 설정
쿼리 성능을 높이기 위해, 다음 테이블에 인덱스를 생성할 수 있습니다. 청산 테이블. 다음은 필요한 인덱스를 생성하는 방법의 예시입니다:
생성 INDEX 생성 idx_debt_asset ON 청산(부채_자산_심볼 텍스트_연산);
CREATE INDEX idx_timestamp ON liquidations(타임스탬프 timestamp_ops);
CREATE INDEX idx_liquidator_address ON liquidations(청산자 주소 텍스트_작업);
CREATE INDEX idx_청산된_지갑 ON liquidations(청산된_지갑 text_ops);
CREATE INDEX idx_collateral_asset ON 청산(담보_자산_심볼 text_ops);
CREATE INDEX idx_liquidations_assets ON liquidations(담보_자산_심볼 text_ops,부채_자산_심볼 text_ops);
개발
프로젝트 구조
src/
├── components/ # 재사용 가능한 UI 컴포넌트
├── hooks/ # 사용자 정의 React 훅
├── lib/
│ ├── security.ts # 수신되는 Streams 유효성 검사
│ ├── server.ts # 웹훅 서버
│ ├── supabase-backend.ts # 백엔드 작업을 위한 Supabase 키
│ ├── supabase-frontend.ts # 프론트엔드 작업을 위한 Supabase 키
│ ├── transform.ts # 수신되는 Streams 의 변환
│ └── utils.ts # 일반 유틸리티 함수
└── types/ # TypeScript 타입 정의
사용 가능한 스크립트
npm run dev: 개발 서버 시작npm run build: 프로덕션용 빌드npm run server: 웹훅 서버 시작npm run lint: ESLint 실행
향후 추가될 수 있는 기능
고급 기능
- 고래 지갑 추적
- 사용자 지정 경고 임계값
- 포트폴리오 위험 평가
- 다른 DeFi 프로토콜과의 통합
- 다음 항목에 대한 실시간 알림:
- 대규모 청산
- 특정 자산
- 가격 임계값 알림
기술적 개선 사항
- 실시간 업데이트를 위한 WebSocket 지원
- 다중 네트워크 지원 (Polygon, Arbitrum 등)
- 고급 데이터 시각화 옵션
- 모바일 앱 버전
- 개발자를 위한 API 이용
- 저장소를 포크하기
- 기능 브랜치 생성:git checkout -b feature/amazing-feature
- 변경 사항을 커밋하세요:git commit -m "멋진 기능 추가"
- 브랜치를 푸시하세요:git push origin feature/amazing-feature
- 풀 리퀘스트를 제출하세요.

