# 풀 데이터 연동 타당성 조사 — Uniswap 풀 목록 + 포크 주소 직접 읽기

조사: 2026-10-13 · 상태: **A안 구현 완료, B안 읽기 전용 조회 구현** (§6)
발단: 1A단계에서 "수수료는 원리적으로 정확히 계산 불가"로 남긴 부분을 실제로 풀 수 있는지 검토

이 문서의 모든 숫자는 **이 PC에서 직접 호출해 확인한 값**이다 (ran-here). 추측 없음.
다음 세션은 §1을 다시 확인하지 말고 §4부터 시작하면 된다.

---

## 1. 검증된 사실 (2026-10-13 직접 호출)

| 확인 대상 | 결과 |
|---|---|
| `yields.llama.fi/pools` | HTTP 200, **키 불필요**, 0.7초, 전체 16,903개 풀 |
| └ uniswap-v3 | **1,874개 / 10체인** (Ethereum 562, Base 500, Arbitrum 229, BSC 220, Polygon 195, OP 79, Avalanche 41, Celo 37, Monad 8, Bob 3) |
| └ uniswap-v2 / v4 | 1,682개 / **1,875개** |
| `ethereum-rpc.publicnode.com` | **CORS `*` + 키 불필요 + 실제 데이터 반환** |
| Base / Arbitrum / Polygon / BSC publicnode | 전부 `eth_call` OK |
| `eth.llamarpc.com` | ✗ CORS 헤더 없음 (브라우저에서 차단됨) + 525 에러 |
| `cloudflare-eth.com` | CORS는 OK, 그러나 `eth_call` 내부 에러 |
| `rpc.ankr.com/eth` | CORS는 OK, 그러나 **API 키 요구** |

### 1-1. DeFiLlama 풀 레코드에 들어 있는 것

```json
{ "chain": "Base", "project": "uniswap-v3", "symbol": "WETH-USDC",
  "tvlUsd": 149655460, "apyBase": 82.60999, "poolMeta": "0.3%",
  "volumeUsd1d": 112904437, "volumeUsd7d": 412152182,
  "underlyingTokens": ["0x4200...0006", "0x8335...2913"],
  "pool": "b99bcdf5-1350-4269-981e-0e9b5cccb007" }
```

**함정: `pool` 필드는 컨트랙트 주소가 아니라 DeFiLlama 자체 UUID다** (1,874개 중 주소 형태 0개).

해결: `underlyingTokens` 2개 + `poolMeta`(수수료 등급)로 팩토리에 물어보면 주소가 나온다.

```
factory.getPool(token0, token1, fee)   셀렉터 0x1698ee82
Ethereum 팩토리 0x1F98431c8aD98523631AE4a59f267346ea31F984
→ getPool(USDC, WETH, 500) = 0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640  (알려진 주소와 일치 확인)
```

**이 변환은 GitHub Actions에서 1회만 하고 결과를 JSON에 박아 넣는다.** 브라우저가 할 일이 아니다.

### 1-2. 풀 컨트랙트에서 읽히는 값 (B안용, 확인만 해 둠)

| 함수 | 셀렉터 | 쓰임 |
|---|---|---|
| `slot0()` | `0x3850c7bd` | sqrtPriceX96, tick → 현재가 |
| `liquidity()` | `0x1a686502` | **현재 활성 유동성** — 수수료 지분 계산의 핵심 |
| `token0()` / `token1()` | `0x0dfe1681` / `0xd21220a7` | 토큰 주소 |
| `fee()` / `tickSpacing()` | `0xddca3f43` / `0xd0c93a7c` | 수수료 등급, 틱 간격 |
| `decimals()` / `symbol()` | `0x313ce567` / `0x95d89b41` | 토큰별 |

---

## 2. 이게 수수료 추정을 실제로 바꾸는가 — 그렇다

ETH/USDC 0.05% 풀(Ethereum)에 **$10,000을 ±2% 구간**에 넣는 경우, 2026-10-13 실측:

| 항목 | 값 |
|---|---:|
| ETH 현재가 (풀에서 직접 읽음) | $2,516.08 |
| 풀 TVL (DeFiLlama) | $84,185,194 |
| 풀 24h 거래량 (DeFiLlama) | $48,469,142 |
| DeFiLlama가 보여주는 풀 APR | 10.51% |
| 풀 평균 집중도 (온체인 L ÷ TVL로 역산) | **3.21배** |
| 내 포지션 집중도 | **100.5배** (독립공식 `1/(1-(Pa/Pb)^0.25)` 검증값과 일치) |
| 상대 집중도 | 31.3배 |
| **내 예상 APR (구간 안에 머무는 동안)** | **약 328%** |

### 교차검증 — 두 독립 방법이 일치

| 방법 | 계산 | 결과 |
|---|---|---:|
| ① 온체인 유동성 지분 | 내 L ÷ (풀 활성 L + 내 L) × 풀 하루 수수료 × 365 | **328.0%** |
| ② 집중도 역산 | 풀 APR × (내 집중도 ÷ 풀 평균 집중도) | **329.2%** |

차이 **0.4%**. 서로 다른 입력(①은 거래량·수수료등급, ②는 DeFiLlama APR)에서 같은 답이 나왔다.

> **현재 계산기와 비교**: 지금은 사용자가 "Fee APR 24%"를 직접 입력하면 그 값을 그대로 믿는다.
> 실제 답이 328%인 상황에서 24%를 쓰면 13배 틀린다. 반대로 틀릴 수도 있다.
> 이 숫자는 **누구도 추측으로 맞출 수 없다.**

---

## 3. 반드시 지켜야 할 주의사항 (조사 중 실제로 당한 것 포함)

### 3-1. 단위 환산이 최대 위험 — 조사 중 실제로 틀렸다

이 조사의 첫 계산에서 **6.6%** 가 나왔다. `√P`를 한 번 더 곱해 풀 유동성을 50배 부풀린 탓이다.
무서운 건 **6.6%가 그럴듯해 보였다는 점**이다 (풀 APR 10.51%와 같은 자릿수).

올바른 환산:

```
풀의 활성 유동성을 계산기와 같은 "USD 가격공간" 단위로:
    L_usd = liquidity() / 10^((decimals0 + decimals1) / 2)
√P 를 곱하지 않는다.

현재가:
    raw   = (sqrtPriceX96 / 2^96)^2          token1/token0, 원시 단위
    human = raw × 10^(decimals0 - decimals1)  token1 per token0
    (USDC/WETH 풀처럼 token0가 스테이블이면 ETH 가격 = 1 / human)
```

**구현 시 알려진 풀 3개 이상으로 가격을 검증할 것.** 소수자리가 다른 조합(6/18, 18/18, 8/18)을
반드시 섞어서 테스트한다. `docs/calculator-tests.md`와 같은 방식으로 기대값 표를 남긴다.

### 3-2. "328%"를 그냥 띄우면 지금보다 더 위험하다

328%는 **구간 안에 머무는 동안의** APR이다. ETH ±2% 구간은 몇 시간이면 벗어난다.
구간 이탈 가능성 없이 328%만 보여주면 "틀린 숫자를 보여주는 정보 사이트"가 된다.

화면에는 반드시 함께 표시한다:
- 이 숫자는 **구간 안에 있을 때만** 발생한다는 것
- 구간을 벗어나면 수수료 0 (1A에서 이미 구현됨)
- 가능하면 변동성 기준 예상 체류 기간

### 3-3. Uniswap V4는 이 방식이 통하지 않는다

V4는 풀마다 컨트랙트가 없고 **PoolManager 싱글톤** 하나에 전부 들어 있다.
"풀 주소 붙여넣기"는 V3와 그 포크 전용이다.

그런데 DeFiLlama 기준 **V4가 1,875개로 V3(1,874개)보다 이미 많다.**
A안 풀 목록에서 V4를 포함할지, V3만 할지 먼저 정해야 한다.

### 3-4. `eth_call`은 코드 없는 주소에 에러를 내지 않는다 (B안용)

실측:

| 대상 | 결과 |
|---|---|
| 토큰 컨트랙트에 `slot0()` | `ERR: execution reverted` ← 정상적으로 걸러짐 |
| **코드 없는 주소에 `slot0()`** | **`OK: 0x`** ← 성공으로 보이지만 빈 값 |

빈 값 `0x`를 그냥 파싱하면 전부 0이 된다. **반환 길이를 반드시 확인**해야 한다.
읽기 전용(`eth_call`)이라 자금 위험은 없지만, 숫자가 조용히 틀린다.

### 3-5. 공개 RPC는 SLA가 없다

`publicnode`는 무료·무키지만 보장이 없다. B안을 할 때는 체인별 대체 RPC 목록과
**실패했을 때의 화면**을 정의해야 한다 (architecture.md §9-6 요구사항).

---

## 4. A안 설계 — 2026-10-13 구현 완료

> **구현 결과 요약.** 아래 설계대로 만들었고, 실제로 만든 것은 다음과 같다.
>
> | 파일 | 역할 |
> |---|---|
> | `scripts/lib/uniswap-pools.mjs` | 수집기. DeFiLlama 풀 목록 + 온체인 `slot0`/`liquidity`/`decimals` |
> | `scripts/fetch-data.mjs` | 기존 잡 배열에 수집기 호출 2줄 추가 (이전 결과를 주소 캐시로 재사용) |
> | `data/uniswap-pools.json` | 7체인 70풀, 35KB. 풀마다 가격·TVL·24h 거래량·풀 APR·풀 집중도 |
> | `src/calc-math.js` → `v3FeeProjection` | 상대 집중도 → 구간 내 APR. `poolApr` 은 **비율**(0.1051) |
> | `src/uniswap-calculator.js` | 체인/풀 드롭다운, 구간 폭 프리셋, 구간 막대, 토큰 구성 막대 |
> | `src/calculator.css` | 이 화면 전용 스타일 |
> | `tests/calculators.test.js` | C1~C7 7개 추가, 총 31개 통과 |
>
> **결정 사항** (§4 "먼저 정할 것"에 대한 답):
> 1. V4 는 제외 (사용자 결정). §3-3 그대로
> 2. 체인별 TVL 상위만, 전체 70개 (`POOL_LIMIT=220`, TVL $250k·거래량 $10k 하한)
> 3. 자동 입력값은 모두 **수정 가능한 입력칸**에 들어가고, 갱신 시각과
>    "가격은 실시간이 아닙니다" 문구를 드롭다운 바로 아래 고정 배치했다
> 4. 풀 집중도 < 0.05 인 죽은 풀은 수집 단계와 `v3FeeProjection` 양쪽에서 걸러낸다
> 5. 구간을 벗어나면 예상 APR 을 **0%** 로 적는다 — 수수료 $0 과 말이 맞아야 하므로

### 당시 설계 (기록)


기존 아키텍처 D1을 그대로 따른다. **브라우저는 외부를 직접 호출하지 않는다.**

```
GitHub Actions (하루 1회)
  1. yields.llama.fi/pools 받기 (11.6MB)
  2. project == "uniswap-v3" 필터 -> 1,874개
  3. TVL 기준 상위 N개만 추리기 (N은 측정 후 결정, 200~300 제안)
  4. 체인별 factory.getPool(token0, token1, fee) 로 풀 주소 계산
  5. data/uniswap-pools.json 으로 커밋 (수 KB)
       { updatedAt, source: "defillama", status: "live",
         data: [{ chain, address, symbol, feeTier, tvlUsd, volumeUsd1d,
                  apyBase, token0, token1, decimals0, decimals1 }] }
Vercel 정적 배포
  -> 계산기가 이 JSON을 읽어 체인/풀 드롭다운을 채운다
```

### 화면에서 바뀌는 것

| 지금 | A안 적용 후 |
|---|---|
| 현재 가격 (USD) 직접 입력 | 풀 선택 시 자동 입력 (수정 가능) |
| 예상 Fee APR (%) 직접 입력 | 풀의 실제 apyBase 자동 입력 (수정 가능) |
| — | 풀 평균 집중도 표시 → 내 집중도와 비교 가능 |
| — | TVL·24h 거래량 표시 (데이터 상태 배지 + 갱신 시각) |

**1A에서 만든 `src/calc-math.js`는 그대로 쓴다.** 수식은 안 바뀌고, 입력값의 출처만 바뀐다.
자동 입력된 값과 사용자가 직접 고친 값은 화면에서 구분해야 한다 (AGENTS.md: mock/실데이터 구분).

### 먼저 정할 것

1. V4 포함 여부 (§3-3) — V4가 더 많지만 구조가 다르다
2. 상위 몇 개를 커밋할지 — 1,874개 전부면 JSON이 커진다
3. 자동 입력값을 사용자가 고쳤을 때의 표시 방법

---

## 5. B안 — 구현 전 보류 조건 (당시 기록)

사용자가 임의의 Uniswap 포크 풀 주소를 붙여넣으면 브라우저가 직접 읽는 방식.
**타당성은 확인됐다** (§1-2, §2). 보류 이유는 기술이 아니라 순서다.

재개 전에 필요한 것 (2026-10-13 기준 상태):
- ~~A안이 먼저 돌아갈 것~~ → **충족.** 2026-10-13 구현 완료 (§4)
- ~~§3-1 단위 환산 테스트 세트~~ → **충족.** `tests/calculators.test.js` C1~C7, `derivePoolMetrics` 가 단위 환산을 한곳에 모았다
- §3-2 구간 체류 기간 표시 방법 결정
- §3-5 RPC 실패 화면 정의
- 브라우저→외부 RPC 직접 호출을 아키텍처에 추가하는 결정 (현재 D1에 없는 패턴.
  금지 사항인 지갑 연결·브라우저 API키·유료 플랜에는 걸리지 않음)

## 6. 브라우저 직접 읽기 — P1 구현 (2026-10-13)

### D1의 좁은 예외와 네 가지 제한

기존 D1의 기본 경로는 Actions → 커밋된 JSON → 정적 사이트다. 오케스트레이터의
결정에 따라 이 계산기에 한해 다음 **읽기 전용 예외**를 적용했다.

1. 브라우저는 **공개·무키 JSON-RPC**에만 접근한다. 체인별 주소는 아래 표와 같다.
2. **사용자 동작으로만** 시작한다. `주소로 풀 읽기` 클릭·주소 입력칸 Enter,
   `현재가 새로고침` 클릭이 전부다. 페이지 로드·타이머·입력 도중 자동 RPC 호출은 없다.
3. 실패하면 **기존 입력값과 스냅샷을 유지**하고 짧은 한국어 안내를 표시한다.
   각 배치 요청은 12초 제한, 자동 재시도 **0회**, 자동 대체 엔드포인트 없음이다.
   사용자가 다시 버튼을 눌러 재조회할 수 있다. 조회 도중 체인·풀·입력·구간이 바뀌면
   오래된 응답을 적용하지 않는다. 요청을 병렬로 무한 반복하지 않는다.
4. API 키·지갑 연결·유료 플랜·쓰기 호출이 없다. RPC 메서드는 `eth_call`과
   컨트랙트 존재 여부를 확인하는 `eth_getCode`뿐이다. 주문·승인·서명·자금 이동 없음.

| 체인 | 사용 RPC (체인당 1개) |
|---|---|
| Ethereum | `https://ethereum-rpc.publicnode.com` |
| Base | `https://base-rpc.publicnode.com` |
| Arbitrum | `https://arbitrum-one-rpc.publicnode.com` |
| Polygon | `https://polygon-bor-rpc.publicnode.com` |
| OP Mainnet | `https://optimism-rpc.publicnode.com` |
| BSC | `https://bsc-rpc.publicnode.com` |
| Avalanche | `https://avalanche-c-chain-rpc.publicnode.com` |

주소는 수집기 `scripts/lib/uniswap-pools.mjs`의 `CHAINS`에서 복사했으며 Node 모듈을
브라우저에 가져오지 않는다. 스냅샷 파일 로딩이 실패해도 지원 체인과 주소 조회는 유지된다.
공개 RPC는 SLA가 없고 공급자의 CORS·서비스 상태에 따라 실패할 수 있다.

### 무엇을 읽고, 무엇은 얻지 못하는가

`src/pool-reader.js`는 풀의 `slot0`, `liquidity`, `token0`, `token1`, `fee`와
토큰의 `decimals`, `symbol`을 읽는다. 풀 읽기는 존재 확인·풀 값 1개 배치와 토큰 정보
1개 배치로 이루어진다. `symbol`은 ABI string·bytes32·실패를 모두 처리하며 실패 시
`0x1234…5678` 같은 주소로 대체한다. 심볼은 표시용으로 안전하게 텍스트 처리한다.

**TVL·24시간 거래량·풀 APR은 이 호출로 얻을 수 없다.** 따라서 풀 집중도도 없으며
집중도를 이용한 Fee APR 추정을 하지 않는다. 직접 읽기에 성공하면 이전 풀 APR을
지우고 `내 포지션 Fee APR 직접 입력 (%)`으로 전환한다. 빈 APR은 계산에 필요한 값으로
표시하며 임의의 수수료 숫자를 만들지 않는다. 구간 밖 수수료·APR은 0이다.
수량·IL·수수료 계산은 기존 `window.PaguCalc`만 사용하고 수식 모듈은 변경하지 않았다.

스냅샷 풀의 `현재가 새로고침`은 **slot0만** 읽어서 현재가 입력만 바꾼다.
조회 시각과 원본 스냅샷 가격 대비 변화율(%)을 표시한다. 종료가·가격 구간·TVL·거래량·
APR·집중도는 그대로이며 화면에서도 이를 설명한다. 원본 스냅샷 가격은 비교 기준으로 보존한다.

스테이블 심볼 목록과 환산 방향은 수집기의 `derivePoolMetrics`와 같다. 한쪽이 스테이블이면
변동 토큰당 스테이블 수량을 가격으로 쓴다(스테이블 1개 = 1 USD라는 기존 가정).
달러 기준이 없는 쌍은 token0당 token1 비율이며, 가격·예치금·손익 단위를 **기준 토큰**으로
표시한다. USD 가격으로 오인하지 않도록 별도 안내한다. 심볼을 읽을 수 없으면 주소 단위를
사용한다. 심볼만으로 스테이블의 실제 가치나 토큰 진위를 보증할 수는 없다.

### 검증과 사용자에게 보이는 실패 안내

- 잘못된 주소: `풀 주소는 0x로 시작하는 40자리 영문·숫자 주소여야 합니다.`
  16진수만 허용하며 대소문자 혼합 주소는 소문자로 정규화한다. EIP-55 체크섬 검증은 하지 않는다.
- 코드 없는 주소: `이 주소에는 컨트랙트가 없습니다`
- 풀 ABI·값 오류: `V3 형식의 풀을 읽을 수 없습니다. 체인과 풀 주소를 확인하세요.`
- RPC·HTTP·응답 형식·시간 초과: `현재 조회할 수 없습니다. 기존 값으로 계산하거나 잠시 후 다시 눌러 주세요.`
- 지원 체인 오류: `이 체인은 직접 조회를 지원하지 않습니다.`

실패 안내 뒤에는 `기존 입력값을 유지했습니다.`가 붙는다. `0x`를 0으로 디코드하지 않는다.
slot0는 완전한 워드 최소 7개·양수 uint160 가격을 요구하며 tick·메타데이터 ABI 범위도 확인한다.
서로 다른 0이 아닌 토큰 주소, 양쪽 decimals 0..36, uint128 유동성·uint24 fee 범위를 확인한다.
V4 및 slot0가 7워드보다 짧은 포크 변형은 지원 대상에서 제외된다.

### 오프라인 단위 환산 기대값

테스트는 직접 만든 hex와 아래 손계산 상수를 사용하며 네트워크를 호출하지 않는다.
기존 수집기와 동일 입력의 가격이 정확히 일치하는 검사도 포함한다.

| 사례 | sqrtPriceX96 입력 | d0/d1 | token1/token0 (human) | 표시 가격 |
|---|---:|---:|---:|---:|
| WETH/USDC | 3961408125713216879677197 | 18/6 | 약 2500 | 약 2500 USDC/WETH |
| USDC/WETH | 1584563250285286751870879006720000 | 6/18 | 0.0004 | 2500 USDC/WETH (역수) |
| WBTC/USDT | 1584563250285286751870879006720 | 8/6 | 40000 | 40000 USDT/WBTC |
| WBTC/WETH (환산용 합성값) | 158456325028528675187087900672 | 8/18 | 0.0000000004 | 0.0000000004 WETH/WBTC |
| 동일 소수자리 (환산용 합성값) | 158456325028528675187087900672 | 18/18 | 4 | 4 기준 토큰/변동 토큰 |

첫 행은 `floor(2^96 / 20000)`이므로 정수 절삭 오차가 있다. 정의에서
`(1/20000)^2 × 10^(18−6) = 0.0000000025 × 10^12 = 2500`이며 테스트 상대 오차 한도는
`1e-12`다. 표는 실시간 시세가 아닌 환산 검증 입력이다. 패킷의 8/6 예시는 d0>d1이므로
음의 지수는 6/18·8/18 사례로 별도로 검증한다.

검증 명령: `node --check src/pool-reader.js`, `node --check src/uniswap-calculator.js`,
`node --test tests/pool-reader.test.js` (21개), `node --test tests/calculators.test.js`
(기존 31개, 파일 변경 없음). 네트워크 실패 화면과 모바일·테마 동작은 별도 화면 확인 대상이다.

이 Worker에서 직접 확인(ran-here): 위 21개·31개 모두 통과, 두 JS 문법 검사 통과.
로컬 4188 서버에서 HTML·reader JS·calculator CSS 모두 HTTP 200을 확인한 뒤 서버를 종료했다.
Edge 화면에서 360px·1280px 및 라이트·다크 모드의 가로 넘침 없음, 조회 버튼 최소 44px,
긴 토큰 라벨 줄바꿈을 확인했다. RPC 응답을 브라우저 안에서 대체하여 Enter 조회·APR 초기화·
직접 APR 사용·구간 밖 0·가격만 갱신·실패 시 기존 값 유지·입력 변경 시 응답 무시·스냅샷
로딩 실패 시 주소 조회 유지도 확인했다. **실제 RPC 재호출로 시세나 가용성을 재검증하지는 않았다.**
사용한 브라우저는 종료하고 임시 프로필을 삭제했으며 해당 msedge/chrome 프로세스가 없음을 확인했다.

## 7. 팩토리로 같은 쌍의 V3 풀 찾기 (2026-10-13)

사용자가 풀 주소를 읽거나 스냅샷 풀을 직접 고르면 계산기는 그 풀의 `factory()`를
읽고, 같은 체인·토큰 쌍으로 `getPool(token0, token1, fee)`를 다섯 티어
`[100, 500, 2500, 3000, 10000]`에 조회한다. 있는 풀만 티어 버튼으로 표시하며,
현재 풀에는 `지금`을 붙인다. 버튼을 누르면 해당 주소를 기존 직접 풀 읽기 경로로 보낸다.
스냅샷의 첫 풀 자동 선택은 페이지 로드 시점이므로 이때 RPC를 시작하지 않는다.

두 토큰 주소 검색은 사용자가 버튼을 누를 때만 실행한다. 같은 티어 조회 함수를 쓰며
팩토리는 이 체인에서 이미 읽은 풀의 `factory()` 결과를 우선 사용하고, 다음으로 아래
기본값을 채운다. 사용자는 팩토리 입력을 다른 포크 주소로 바꿀 수 있다.

| 체인 | 기본 Uniswap V3 팩토리 |
|---|---|
| Ethereum | `0x1f98431c8ad98523631ae4a59f267346ea31f984` |
| Base | `0x33128a8fc17869897dce68ed026d694621f6fdfd` |
| Arbitrum | `0x1f98431c8ad98523631ae4a59f267346ea31f984` |

기본값은 2026-10-13 publicnode의 실제 `eth_getCode`와 `eth_call(getPool)`로 확인했다.
세 체인의 각 팩토리 주소는 코드를 반환했고, 두 테스트용 토큰 주소에 대한 조회는
정상 ABI의 0 주소를 반환했다. 요청에서 제안한 Ethereum 주소를 Base에서 호출하면
`0x`가 반환되어 Base 기본값으로 사용하지 않았다. (Ethereum과 Arbitrum은 요청 주소와 동일.)

팩토리로는 주어진 토큰 쌍의 지정 티어만 확인할 수 있다. 모든 풀 목록을 탐색하지 않으며
UI에서 토큰 두 주소가 필요하다고 안내한다. 직접 읽은 풀은 TVL·24시간 거래량·풀 APR을
얻지 못하므로 기존 직접 입력 경로와 똑같이 Fee APR을 비우고 사용자가 직접 입력하게 한다.

주소 종류 판별은 성공한 풀 읽기를 `풀`, `factory` 선택자 응답을 `팩토리`, 정상
`decimals()`와 `symbol()` 응답을 `토큰`, 빈 코드 조회를 `컨트랙트 아님`으로 표시한다.
이 호출들이 증명하지 못하면 `알 수 없음`을 표시한다. 팩토리로 판별되면 주소를 토큰 쌍
검색 폼의 팩토리 칸에 넣는다. 잘못된 팩토리나 RPC 실패는 주소 확인 안내를 표시하고
기존 스냅샷·입력값은 유지한다. 자동 재시도나 백그라운드 조회는 추가하지 않았다.

오프라인 테스트에는 지정된 WETH/USDT live-result 주소를 ABI hex fixture로 넣어
factory 디코드, 정확한 `getPool` calldata, 빈 `0x` 거부, 0 주소 처리, 네 개의 존재
티어 및 현재 티어 표시, 주소 종류, 토큰 순서 불변성을 확인한다. 이 테스트는 네트워크를
호출하지 않는다.
