Skip to content

Commit e239077

Browse files
committed
feagure: frontend dashboard
1 parent 97e853d commit e239077

108 files changed

Lines changed: 13387 additions & 1304 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ body:
2525
- Contracts (contracts/ComplianceDVN.sol, on-chain veto)
2626
- Worker — service (worker/service.ts, verify/commit/veto loop)
2727
- Worker — assess / risk engine (worker/assess/)
28-
- Worker — tracker (worker/tracker/)
29-
- CLI (pnpm cli assess / trace)
28+
- Demo dashboard (demo/dashboard/)
3029
- Deploy / wiring (hardhat tasks, layerzero.config.ts)
3130
- Build / tooling / CI
3231
- Documentation

BRANCH_DIFF.md

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# `backend``main` 차이
22

3-
이 문서를 처음 읽는 사람을 위한 안내입니다. 마지막 확인: 2026-07-30.
3+
이 문서를 처음 읽는 사람을 위한 안내입니다. 마지막 확인: 2026-07-31.
44

55
## ⚠️ 먼저 알아야 할 것: 아직 커밋되지 않았습니다
66

@@ -34,7 +34,7 @@ main 70ffd60 ← 같은 커밋
3434
| 판정 | 제재 주소 목록 대조 → 통과/거부 | 라벨 가중치 점수 + 소스 신뢰도 천장 → **4단계 행동** |
3535
| 행동 | allow / block | allow / **delay** / **manual-review** / block |
3636
| 보류 해제 || 시계(delay) 또는 **owner의 온체인 `approvePacket`** |
37-
| 그래프 분석 | 없음 | 신규 `indexer/` 패키지 (Postgres, 1-hop 근접성) |
37+
| 그래프 분석 | 없음 | 신규 `indexer/` 패키지 (Postgres, 최대 3-hop 근접성) |
3838
| 감사 추적 | 없음 | 온체인 `RiskVerdict` 이벤트 → 인덱서 → Postgres → Grafana |
3939
| 컨트랙트 검증 | 없음 | Sourcify v2 조회 → `unverified_contract` 라벨 |
4040
| 관측 | Prometheus 메트릭 | + Grafana 대시보드 2개 (compose 프로파일) |
@@ -78,6 +78,12 @@ function submitVerification(
7878
컨트랙트는 업그레이더블이 아니므로 **기존 배포 주소를 재사용할 수 없습니다.** `dvn:preflight`
7979
이 불일치를 감지해 알려줍니다.
8080

81+
**생성자도 바뀌었습니다**`assignJob`이 send library로 게이트되면서 `_sendUln` 파라미터가
82+
추가됐습니다 (`(_owner, _operator, _sendUln, _receiveUln, _fee)`). 게이트가 없으면 아무나
83+
`assignJob`을 호출해 임의 payloadHash로 `JobAssigned`를 발생시킬 수 있고, worker는 그걸 "우리
84+
일"로 믿고 남의 패킷을 심사·verify하며 operator 가스를 태우게 됩니다. 아래 §5의 기존 배포는
85+
이 게이트 이전 버전이므로 **재배포가 필요합니다** (`dvn:preflight``sendUln()` 부재를 감지).
86+
8187
신규 함수·이벤트:
8288

8389
| 항목 | 권한 | 용도 |
@@ -112,12 +118,15 @@ Docker + PostgreSQL로 도는 별도 프로세스. worker와 코드를 공유하
112118
2. reorg 처리 — 저장된 블록 해시가 노드와 다르면 `REORG_DEPTH`(32) 창을 통째로 롤백
113119
3. Sourcify v2로 컨트랙트 검증 여부 조회. 응답을 못 받은 `unknown`과 미검증 `unverified`
114120
구분하고 **라벨은 후자만** 만듭니다
115-
4. **1-hop 근접성** — 제재 주소로 보냈으면 `sanctions_1hop`(70점), 제재 주소가 보냈으면
116-
`sanctions_1hop_inbound`(40점). 후자는 `TOKEN_MINIMUMS` 이상만 인정합니다 (dust를 보내
117-
임의 주소에 라벨을 붙이는 걸 막기 위함)
121+
4. **최대 3-hop 근접성** — 홉마다 라벨·가중치가 다릅니다: outbound `sanctions_1hop`(70) /
122+
`sanctions_2hop`(45) / `sanctions_3hop`(25), inbound 40/20/10, 믹서 60/35/20. 경로는 같은
123+
체인·블록 비내림차순·단순 경로만 인정하고 최단 거리만 라벨링합니다. 주체 본인의 첫 outbound
124+
엣지를 제외한 **모든 엣지는 `TOKEN_MINIMUMS` 이상**이어야 합니다 (dust로 임의 주소를
125+
오염시키거나, 한 다리 건너 스미어하는 걸 막기 위함)
118126
5. 10분마다 피드 빌드 → 정수만 쓰는 canonical JSON + EIP-191 서명 → `:9091`로 서빙
119127

120-
깊이는 **1홉으로 고정**입니다 (`GRAPH_DEPTH = 1`). 정책상 합의된 값이고 미구현이 아닙니다.
128+
깊이는 **3홉까지**입니다 (`GRAPH_DEPTH = 3`, 정책 v2). 3-hop 라벨은 단독으로는 어떤 행동도
129+
일으키지 않고(25 < delay 임계 30) 다른 신호와 합산될 때만 작용합니다.
121130

122131
---
123132

@@ -153,9 +162,11 @@ Docker + PostgreSQL로 도는 별도 프로세스. worker와 코드를 공유하
153162

154163
worker의 서명 키가 `PRIVATE_KEY`**`OPERATOR_PRIVATE_KEY`** 로 바뀌었습니다. 루트 `.env`
155164
`PRIVATE_KEY`는 owner 키이므로, 루트 파일을 worker로 복사하면 worker가 owner 권한을 갖게
156-
됩니다. 그래서 worker는 **`PRIVATE_KEY`가 있으면 부팅을 거부하고 이유를 설명합니다.**
165+
됩니다. 그래서 worker 서비스는 **환경에 `PRIVATE_KEY``OWNER_PRIVATE_KEY`가 있으면 —
166+
`OPERATOR_PRIVATE_KEY`가 함께 있어도 — 부팅을 거부하고 이유를 설명합니다.**
157167

158-
`OWNER_PRIVATE_KEY`는 CLI 셸에만 두고 worker 환경에는 **절대** 넣지 마세요.
168+
`OWNER_PRIVATE_KEY`는 배포 셸에만 두세요. owner 액션(승인·거절)은 데모 대시보드에서 MetaMask
169+
서명으로 이루어지므로, 어떤 서비스 환경에도 owner 키가 들어갈 일이 없습니다.
159170

160171
---
161172

@@ -190,9 +201,9 @@ worker의 서명 키가 `PRIVATE_KEY` → **`OPERATOR_PRIVATE_KEY`** 로 바뀌
190201
| base-sepolia | MyOFT | `0x81129e01913aBE10AB620B1fBB91820783DcDBf2` |
191202
| optimism-sepolia | MyOFT | `0x81129e01913aBE10AB620B1fBB91820783DcDBf2` |
192203

193-
`deployments/`에 배포 기록이 커밋되어 있으므로 재배포 없이 이어서 쓸 수 있습니다.
194-
**owner/operator 키는 작성자가 들고 있습니다.** 직접 트랜잭션을 보내려면 본인 키로
195-
재배포해야 합니다.
204+
⚠️ **위 주소들은 `assignJob` 게이트(§1) 이전 버전입니다.** 게이트를 적용하려면 재배포·재와이어링이
205+
필요하고, `dvn:preflight`가 이를 경고합니다. 그 외에도 **owner/operator 키는 작성자가 들고
206+
있으므로**, 직접 트랜잭션을 보내려면 어차피 본인 키로 재배포해야 합니다.
196207

197208
동작 확인된 경로:
198209

@@ -265,9 +276,9 @@ Grafana <http://localhost:3000/d/compliance-dvn-indexer> — datasource와 대
265276

266277
| 대상 | 결과 | 명령 |
267278
| ------------------- | -------------- | ----------------------------------------------------- |
268-
| worker 단위 테스트 | **239 passed** | `cd worker && pnpm test` |
269-
| indexer 단위 테스트 | **102 passed** | `cd indexer && pnpm test` |
270-
| 컨트랙트 (hardhat) | **13 passed** | `npx hardhat test test/hardhat/ComplianceDVN.test.ts` |
279+
| worker 단위 테스트 | **254 passed** | `cd worker && pnpm test` |
280+
| indexer 단위 테스트 | **121 passed** | `cd indexer && pnpm test` |
281+
| 컨트랙트 (hardhat) | **15 passed** | `npx hardhat test test/hardhat/ComplianceDVN.test.ts` |
271282
| 타입 체크 | clean | 각 패키지 `pnpm typecheck` |
272283

273284
`worker/``indexer/`는 루트 prettier 대상이 **아닙니다** (`.prettierignore`에서 제외).
@@ -286,7 +297,7 @@ Grafana <http://localhost:3000/d/compliance-dvn-indexer> — datasource와 대
286297
- **native 값 엣지** — 이벤트를 남기지 않으므로 trace API가 필요하고, 공용 RPC 대부분이
287298
제공하지 않습니다
288299
- **`honeypot_suspect`** — 시뮬레이션이 필요합니다
289-
- **깊이 2 이상 순회** — 정책상 1홉 고정이므로 미구현이 아니라 결정 사항입니다
300+
- **깊이 4 이상 순회** — 정책상 3홉 고정(정책 v2)이므로 미구현이 아니라 결정 사항입니다
290301

291302
엔드투엔드로 확인되지 **않은** 경로:
292303

@@ -320,3 +331,53 @@ Grafana <http://localhost:3000/d/compliance-dvn-indexer> — datasource와 대
320331
- [ ] `TEST_DENYLIST`가 데모용 `0x…dEaD`로 남아 있습니다. 정리 후 재시작 권장
321332
- [ ] 커밋을 쪼갤 것: 컨트랙트 ABI 변경 / 리스크 엔진 / 인덱서 신규 / 관측·문서 정도로
322333
나누면 리뷰가 가능합니다. 지금은 49 + 64 파일이 한 덩어리입니다
334+
335+
---
336+
337+
## 10. 데모용 가짜 스테이블코인 (테스트넷 전용)
338+
339+
`contracts/mocks/FakeStablecoinMock.sol` — USDC 심볼을 주장하는 미끼 컨트랙트입니다. 가치가 없고
340+
스테이블코인도 아니며, 리스크 엔진의 사칭 탐지를 실제로 굴려보기 위한 것입니다.
341+
342+
| 체인 | 주소 |
343+
| ---- | ---- |
344+
| base-sepolia | `0x1B48E40F971298b03B6AD0Ae3CA047CD11b7eA6e` |
345+
| optimism-sepolia | `0x7Ec44363Fdaa7EEC9B49a858220Ff543Bcd43ac7` |
346+
347+
한 컨트랙트로 두 신호를 보여줍니다 (실측 확인):
348+
349+
- 그대로 두면 `fake_stablecoin_suspect` 65점 → **manual-review**
350+
- `worker/.env``SCAM_TOKENS`에 주소를 넣으면 `scam_token` 100점 → **block**
351+
352+
수취인은 **도착 체인** 상태로 심사되므로, base→op 전송이면 op 쪽 주소를 수취인으로 지정해야 합니다.
353+
354+
### 데모용 컨트랙트 두 개 (테스트넷 전용)
355+
356+
| 컨트랙트 | 체인 | 주소 | 발화 신호 |
357+
| -------- | ---- | ---- | --------- |
358+
| `FakeStablecoinMock` | base-sepolia | `0x1B48E40F971298b03B6AD0Ae3CA047CD11b7eA6e` | `fake_stablecoin_suspect` 65 → manual-review |
359+
| `FakeStablecoinMock` | optimism-sepolia | `0x7Ec44363Fdaa7EEC9B49a858220Ff543Bcd43ac7` | (SCAM_TOKENS에 넣으면 `scam_token` 100 → block) |
360+
| `RiskyProxyMock` | optimism-sepolia | `0x9771013D82dcC2bdb489B982B4f201FD698A15e6` | `upgradeable_proxy` 15 + `contract_admin_risk` 50 = 65 → manual-review |
361+
362+
`RiskyProxyMock`은 EIP-1967 슬롯에 admin을 `0x…dEaD`(TEST_DENYLIST 주소)로 써 둡니다. 실권자가
363+
오염된 업그레이더블 컨트랙트를 흉내내는 것이고, 다른 admin으로 배포하려면 `RISKY_ADMIN`을 주면
364+
됩니다.
365+
366+
### 차단이 채널을 막는 문제와 `skip`
367+
368+
검증을 보류한 패킷은 그 nonce가 영구히 비어 있고, LayerZero 채널은 nonce를 순서대로만 처리하므로
369+
**뒤의 정상 메시지가 모두 갇힙니다** (`LZ_InvalidNonce`). 해소는 `EndpointV2.skip`이며 OApp의
370+
delegate(= owner)만 호출할 수 있습니다. 대시보드 "보류 패킷" 탭의 **메시지 채널 상태** 섹션이
371+
막힌 nonce를 찾아 owner 서명으로 건너뛰게 해 줍니다.
372+
373+
### 데모 지연시간 설정
374+
375+
| 설정 | 파일 ||
376+
| ---- | ---- | -- |
377+
| `POLL_MS` | worker | 3000 |
378+
| `SCAN_CONFIRMATIONS` | worker | 1 (온체인 증명값 `DVN_CONFIRMATIONS`=5는 ULN 요구조건이라 유지) |
379+
| `FEED_REFRESH_MS` | worker | 5000 — 피드만 재수신 (전체 재빌드는 60초) |
380+
| `POLL_MS` / `CONFIRMATIONS` | indexer | 5000 / 1 |
381+
| `FEED_REBUILD_MS` | indexer | 60000 — 단, **새 엣지가 잡히면 즉시 재발행** |
382+
383+
전송 → 판정 ≈ 3.5초, 그래프 라벨 반영 ≈ 10초.

CONTRIBUTING.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ need them for live deploy/wire/demo flows (see the README).
7676
| `contracts/` | `ComplianceDVN.sol` (the thin on-chain DVN) and friends |
7777
| `worker/service.ts` | Always-on worker: watches `JobAssigned`, verifies/commits or vetoes |
7878
| `worker/assess/` | Chain-independent risk engine — `assess()` / `combine()` over the merged denylist |
79-
| `worker/tracker/` | Tier 1 observation-only route tracker |
80-
| `worker/cli.ts` | `pnpm cli assess <addr>` / `pnpm cli trace <txHash>` |
79+
| `demo/` | Demo assets: dashboard, decoy contracts, mint script (see `demo/README.md`) |
8180
| `deploy/`, `tasks/` | Hardhat deploy scripts and operator tasks |
8281
| `test/` | Foundry (`*.t.sol`) and Hardhat/Vitest tests |
8382
| `layerzero.config.ts` | DVN wiring (required DVN, per-chain ULN config) |

README.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ direct-hit lookup over a denylist merged from four sources:
6464
veto: blocked if any party is flagged. The live denylist built to 101 entries during
6565
the demo.
6666

67-
`worker/tracker/` adds the Tier 1 observation-only tracker. `cli trace <txHash>`
68-
reconstructs a route via the LayerZero Scan API and colors each endpoint with `assess()`.
67+
Screening results, held packets, and owner actions are all surfaced in the demo dashboard
68+
(`demo/dashboard/`), which links each verdict to its LayerZero Scan route.
6969

7070
## Deployed contracts
7171

@@ -94,14 +94,11 @@ npx hardhat lz:deploy --ci --networks base-sepolia,optimism-sepolia --tags Comp
9494
npx hardhat lz:oapp:wire --oapp-config layerzero.config.ts --ci
9595
npx hardhat dvn:status --network base-sepolia # sanity
9696

97-
# Run the worker (always-on) + send demos
97+
# Run the worker (always-on)
9898
pnpm worker # screens both chains, verifies/commits or vetoes
99-
npx hardhat demo:send --network optimism-sepolia --to <clean_addr> --dst base # delivers
100-
npx hardhat demo:send --network optimism-sepolia --to $TEST_DENYLIST --dst base # vetoed
10199

102-
# One-shot CLI
103-
pnpm cli assess <address>
104-
pnpm cli trace <txHash> # Tier 1 route + risk coloring
100+
# Demo dashboard (MetaMask sends, owner review, sanctions/graph views)
101+
cd demo/dashboard && python serve.py # http://localhost:8080 — see demo/README.md
105102
```
106103

107104
## Tests
@@ -111,8 +108,9 @@ Foundry (31): `ComplianceDVN` unit tests (fee, job, operator-gating, admin) and
111108
one reverts `commitVerification` with `LZ_ULN_Verifying`, so the recipient balance
112109
stays 0.
113110

114-
Vitest (16): `assess()`/`combine()`, the 81-byte header and OFT-message decoders, the
115-
`JobAssigned` assignment filter, the durable checkpoint, and the tracker transform.
111+
Vitest (worker + indexer): `assess()`/`combine()`, the 81-byte header and OFT-message decoders,
112+
the `JobAssigned` assignment filter, the durable checkpoint, the deferred queue, the signed feed,
113+
and the 3-hop proximity graph.
116114

117115
## CI / CD
118116

@@ -160,11 +158,11 @@ verifying other OApps' packets on the shared endpoint.
160158
contracts/ComplianceDVN.sol ComplianceDVN.t.sol + ComplianceDvnVeto.t.sol (veto proof)
161159
contracts/ToyOFT.sol demo OFT
162160
deploy/ hardhat-deploy scripts
161+
demo/ demo assets: dashboard, decoy contracts, mint script (demo/README.md)
163162
layerzero.config.ts requiredDVNs = our DVN, both directions
164-
tasks/ dvn:status, demo:send
163+
tasks/ dvn:status, dvn:preflight, dvn:verify-wiring
165164
worker/assess/ Tier 0 risk engine (OFAC + OpenSanctions + mixers + test)
166-
worker/chain/ header/message decoders, PacketSent scanner, verify/commit
167-
worker/tracker/ Tier 1 LayerZero-Scan tracker
168-
worker/service.ts always-on watcher (fail-closed) worker/cli.ts one-shot
165+
worker/chain/ header/message decoders, PacketSent scanner
166+
worker/service.ts always-on watcher (fail-closed)
169167
docs/superpowers/ design spec + implementation plan
170168
```

contracts/ComplianceDVN.sol

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import { IReceiveUlnE2 } from "@layerzerolabs/lz-evm-messagelib-v2/contracts/uln
1111
/// attestation behind an operator key. Withholding `submitVerification` IS the veto.
1212
contract ComplianceDVN is ILayerZeroDVN, Ownable {
1313
address public operator; // off-chain worker key
14+
address public sendUln; // SendUln302 on this chain — the only address allowed to assign jobs
1415
address public receiveUln; // ReceiveUln302 on this chain
1516
uint256 public fee;
1617

1718
event JobAssigned(uint32 dstEid, bytes32 payloadHash, uint64 confirmations, address sender);
1819
event OperatorSet(address operator);
20+
event SendUlnSet(address sendUln);
1921
event ReceiveUlnSet(address receiveUln);
2022
event FeeSet(uint256 fee);
2123

@@ -46,6 +48,7 @@ contract ComplianceDVN is ILayerZeroDVN, Ownable {
4648
uint8 public constant ACTION_BLOCK = 3;
4749

4850
error NotOperator();
51+
error NotSendLibrary();
4952
error UnknownAction(uint8 action);
5053
/// @dev Submitting a verification asserts the packet was allowed; any other action would be
5154
/// a self-contradicting record.
@@ -59,10 +62,18 @@ contract ComplianceDVN is ILayerZeroDVN, Ownable {
5962
_;
6063
}
6164

62-
constructor(address _owner, address _operator, address _receiveUln, uint256 _fee) Ownable(_owner) {
65+
constructor(
66+
address _owner,
67+
address _operator,
68+
address _sendUln,
69+
address _receiveUln,
70+
uint256 _fee
71+
) Ownable(_owner) {
6372
require(_operator != address(0), "zero operator");
73+
require(_sendUln != address(0), "zero sendUln");
6474
require(_receiveUln != address(0), "zero receiveUln");
6575
operator = _operator;
76+
sendUln = _sendUln;
6677
receiveUln = _receiveUln;
6778
fee = _fee;
6879
}
@@ -77,6 +88,10 @@ contract ComplianceDVN is ILayerZeroDVN, Ownable {
7788
}
7889

7990
function assignJob(AssignJobParam calldata _param, bytes calldata) external payable returns (uint256) {
91+
// Only the send library assigns jobs. The worker treats a JobAssigned payloadHash as
92+
// "this packet is ours to screen" and spends operator gas verifying it, so an open
93+
// assignJob would let anyone point the worker at packets no one asked it to verify.
94+
if (msg.sender != sendUln) revert NotSendLibrary();
8095
// NOTE: SendUln302 calls assignJob WITHOUT forwarding value (msg.value == 0); the
8196
// messagelib accrues each worker's fee internally and workers withdraw separately
8297
// (see SendUlnBase._assignJobs). So we must NOT require msg.value >= fee here — doing
@@ -136,6 +151,12 @@ contract ComplianceDVN is ILayerZeroDVN, Ownable {
136151
emit OperatorSet(_operator);
137152
}
138153

154+
function setSendUln(address _sendUln) external onlyOwner {
155+
require(_sendUln != address(0), "zero sendUln");
156+
sendUln = _sendUln;
157+
emit SendUlnSet(_sendUln);
158+
}
159+
139160
function setReceiveUln(address _receiveUln) external onlyOwner {
140161
require(_receiveUln != address(0), "zero receiveUln");
141162
receiveUln = _receiveUln;

0 commit comments

Comments
 (0)