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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ services/toolkit/dist/
services/toolkit/node_modules/
services/operator-api/dist/
services/operator-api/node_modules/
services/compliance-data/dist/
services/compliance-data/node_modules/
deployments/
5 changes: 4 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Execution Integration Kit로 구성한다. Corner Store reference DEX는 이 공
| `services/toolkit/` | versioned Toolkit config schema와 공통 validation primitives |
| `services/operator-api/` | private-key 없는 read-only operator snapshot/event API |
| `services/operator-dashboard/` | Operator API를 소비하는 read-only snapshot/proposal review 화면 |
| `services/compliance-data/` | provider-neutral TA lot, person-group state와 reject/surveillance audit SDK |
| `tools/deploy-v3/` | 독립적으로 유지하는 vendored Uniswap v3 배포 도구 |
| `lib/` | Foundry 의존성 |
| `scripts/` | 저장소 setup, 검증과 정리 명령 |
Expand All @@ -34,7 +35,9 @@ Execution Integration Kit로 구성한다. Corner Store reference DEX는 이 공
- ERC-3643과 ONCHAINID는 외부 token/identity trust boundary다.
- Element, Recipe, Manifest, Operator의 이름 기반 4-Layer compliance model을
사용한다.
- applicable Recipe는 cumulative AND로 평가한다.
- Manifest는 bounded `RecipeBinding[]`로 Recipe/version/mode를 고정한다.
- `REQUIRED_BLOCKING`은 AND, 같은 `pathGroupId`의 `PATH_OPTION`은 OR,
서로 다른 path group은 AND로 평가하며 `FLAG_ONLY` 실패는 기록만 한다.
- `tokenIn`과 `tokenOut` 양쪽의 classification과 Manifest를 평가한다.
- Asset Compliance Manifest는 자산별 Recipe, engine, version과 발행 측 coverage를
binding한다.
Expand Down
100 changes: 96 additions & 4 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ Recipe를 선택하는 인상을 주었다. 법률 연구는 한 거래에 발
- Manifest: 자산별 Recipe/engine/version/coverage binding
- Operator: 판단·승인·감시

거래마다 applicable Recipe를 식별하고 Element 합집합을 cumulative AND로 평가한다.
거래마다 applicable Recipe를 식별한다. Required Recipe의 Element는 cumulative
AND로 평가하고, ADR-007 이후 명시적 path option과 flag-only binding을 분리한다.
기존 ExecutionRouter, ComplianceEngine과 Adapter 분리는 유지한다.

### Alternatives Considered
Expand Down Expand Up @@ -486,17 +487,19 @@ custom error `Errors.InvalidManifestTransition`으로 revert한다.
| UNKNOWN 또는 RETIRED | `registerManifest` | PROPOSED | onlyOwner |
| PROPOSED | `approveManifest` | ACTIVE | onlyOperator (issuance recipe 필수) |
| ACTIVE | `suspendManifest(reasonCode)` | SUSPENDED | onlyOperator |
| SUSPENDED | `resumeManifest` | ACTIVE | onlyOperator |
| SUSPENDED | `scheduleManifestResume` 후 `resumeManifest` | ACTIVE | owner schedule + timelock + operator execute |
| ACTIVE 또는 SUSPENDED | `retireManifest(reasonCode)` | RETIRED (terminal) | onlyOperator |
| UNKNOWN | `setUnregulated` | UNREGULATED | onlyOwner |
| UNREGULATED | `clearUnregulated` | UNKNOWN | onlyOwner |

`registerManifest`는 caller가 넣은 `m.status`를 무시하고 항상 PROPOSED로 착지하며
`declaredBy = msg.sender`를 기록한다. `approveManifest`는 `approvedBy = msg.sender`를
기록하고 `issuanceRecipeId == 0`이면 registry-level completeness floor로 revert한다.
기록한다. MANIFEST-002 이후 completeness floor는 deprecated issuance field가 아니라
비어 있지 않고 최소 하나의 blocking/path gate가 있는 validated `RecipeBinding[]`다.
RETIRED는 terminal이며 재발행은 RETIRED→register→approve 경로로만 가능하다. gating
model: owner가 자산을 classify/declare(register/setUnregulated/clearUnregulated)하고,
operator가 기존 manifest의 lifecycle(approve/suspend/resume/retire)을 구동한다.
operator가 approve/suspend/retire와 예약된 resume 실행을 구동한다. resume 예약과
semantic update 예약은 D011에 따라 owner governance가 담당한다.

**2. Enum-append storage rationale.** `PolicyStatus`에 PROPOSED(4)와 RETIRED(5)를
SUSPENDED(3) 뒤에 APPEND한다. 기존 numeric value(UNKNOWN=0, UNREGULATED=1,
Expand Down Expand Up @@ -565,3 +568,92 @@ token의 `declaredBy`/`approvedBy`는 factory 주소이며, attribution은 facto
`test/unit/registry/TokenPolicyRegistry.t.sol`
- `test/integration/EmergencyPause.t.sol`, `test/integration/IntegrationBase.sol`,
`test/integration/Surveillance.t.sol`

## D011 — 위험 중단은 즉시, 재개와 Manifest 의미 변경은 timelock으로 분리한다

Date: 2026-07-22

### Context

ADR-007은 모든 Router가 공유하는 central pause state, 즉시 containment, 지연된
compliance relaxation, Manifest semantic version/history 보존을 요구한다. 기존
reference stack은 venue suspension과 Manifest status만 있어 global/asset pause가
없었고, 재개와 core fact 변경을 즉시 수행할 수 있었다. 배포 후
`TokenPolicyRegistry.owner()`가 Factory가 되므로 owner-only governance 호출을 EOA가
직접 실행할 수도 없었다.

### Decision

1. `OperatorRegistry`를 global/asset/venue pause의 source of truth로 사용하고 Router는
nonce 소비와 compliance evaluation 전에 세 범위를 모두 fail-closed로 검사한다.
2. operator는 pause와 Manifest suspend처럼 위험을 줄이는 동작을 즉시 수행한다.
unpause는 owner가 예약하고 최소 1일 뒤 owner가 실행한다.
3. Manifest resume와 ACTIVE/SUSPENDED semantic update도 owner 예약과 최소 1일
timelock을 요구한다. update activation은 version을 증가시키며 기존 SUSPENDED
상태를 해제하지 않는다.
4. Manifest version, current hash, chained history hash와 pause history hash를
보존하고 actor, old/new, reason, effective time을 append-only event로 남긴다.
5. 배포 후 registry owner인 `CornerStoreFactory`가 resume/update schedule/cancel을
forwarding한다. Factory owner는 production에서 외부 Safe-style governance다.
registry operator는 delay가 지난 동작의 실행과 즉시 tightening을 담당한다.

### Consequences

- 사고 containment는 timelock 없이 가능하지만 재개는 같은 actor가 즉시 우회할 수
없다.
- Factory ownership wiring을 유지하면서 governance 호출이 실제 배포에서도
도달 가능하다.
- chain별 delay, 실제 Safe provider, issuer-level disable과 RecipeBinding migration은
후속 production 설정/feature다.

### Related Files

- `src/registry/OperatorRegistry.sol`
- `src/registry/TokenPolicyRegistry.sol`
- `src/execution/ExecutionRouter.sol`
- `src/factory/CornerStoreFactory.sol`
- `test/integration/EmergencyPause.t.sol`
- `scripts/e2e-anvil.sh`

## D012 — 취득 lot와 거절·감시 상태는 provider-neutral off-chain 계층으로 수렴한다

Date: 2026-07-22

### Context

ADR-008은 acquisition source, person-group state, reject logging과 Router 밖 transfer
감시를 하나의 off-chain compliance data layer로 결정했다. 기존 `Lockup`은 만료나
lineage 상태 없이 단일 timestamp만 읽었고, 실제 provider 계약이 없다는 이유로
현재 문서에는 해당 결정을 여전히 open으로 표시한 곳이 남아 있었다.

### Decision

1. Transfer Agent별 API는 `TransferAgentProvider` adapter 뒤에 둔다. Corner Store
core는 Securitize 전용 undocumented field를 하드코딩하지 않는다.
2. per-lot 입력은 acquisition date, payment completion, source type과 lineage를
검증한다. 단일 holder×asset 온체인 snapshot은 lot 선택을 과대 추정하지 않도록
현재 lot 중 가장 늦은 유효 clock을 사용한다.
3. 온체인 `AttestedAcquisitionSource`에는 clock, observation/expiry, PII-free
source hash와 status만 저장한다. missing, broken lineage, stale과 immature는
`Lockup`에서 서로 다른 reason으로 fail-closed한다.
4. person-group volume/holder state는 execution id로 idempotent하게 commit한다.
동일 id+동일 내용은 no-op, 동일 id+다른 내용은 충돌로 거부한다.
5. rejected attempt와 Router 밖 transfer finding은 off-chain hash-chain audit
record로 보존한다. 이 local SDK는 tamper evidence를 제공하지만 production WORM,
retention 또는 SAR 시스템이라고 주장하지 않는다.

### Consequences

- mock TA로 전체 경계를 테스트할 수 있으나 실제 Securitize compatibility는 공식
API 계약과 provider 인증을 확인하기 전까지 미구현이다.
- 보수적인 latest-lot clock은 안전하지만 일부 mature lot 매도를 과도하게 막을 수
있다. amount-specific lot allocation/FIFO는 provider 계약이 확정될 때 별도
versioned adapter로 추가한다.
- PII, 원본 lot 문서와 감사 원장은 온체인에 저장하지 않는다.

### Related Files

- `docs/decisions/ADR-008-compliance-seam-decisions.md`
- `services/compliance-data/`
- `src/registry/AttestedAcquisitionSource.sol`
- `src/compliance/elements/Lockup.sol`
149 changes: 149 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,12 @@ passing
기본값은 BUIDL-like metadata + Reg D/QP/minimum-investment Manifest다.
- live runner가 backend quote → CLI → Router/RFQAdapter 성공과 revoked-maker
실패를 자동 실행한다.
- `scripts/e2e-anvil.sh --mode rfq`는 AMM·lifecycle·surveillance 설명을 건너뛰고
mock TA profile → Toolkit/CLI → backend-signed quote → protected RFQ settlement
→ revoked-maker rejection만 보여주는 짧은 MVP 시연 경로를 제공한다.
- 기존 Operator dashboard는 `Operator view`(read-only snapshot)와 `RFQ demo`
(local quote request/download/CLI settlement handoff) 모드를 제공하며, browser에
private key나 direct transaction endpoint를 추가하지 않는다.
- backend는 pricing, signing과 nonce 발급만 담당하며 compliance 최종 판단을 하지 않는다.
- production pricing, signer custody, persistent nonce, inventory/risk control과 hosted operation은 명시적으로 범위 밖이다.

Expand All @@ -382,6 +388,7 @@ passing
- `scripts/check.sh`
- `scripts/e2e-anvil.sh --profile buidl-like`
- `scripts/e2e-anvil.sh --profile reg-d`
- `scripts/e2e-anvil.sh --profile buidl-like --mode rfq`
- `git diff --check`

### State
Expand All @@ -395,6 +402,9 @@ passing
- CLI smoke가 `--backend` request path를 검증하고 기존 `RFQFlow.t.sol`이 protected Router settlement의 성공/거부 경로를 검증한다.
- Foundry v1.7.1 clean build에서 `buidl-like`과 `reg-d` 두 profile 모두
통과: 각각 7/7 scenarios, backend-signed quote settlement, revoked-maker 거부.
- RFQ-first dashboard/runbook: `services/operator-dashboard`의 두 모드와 local
HTTP server smoke, `--mode rfq` live E2E를 검증했다. 시연 순서는
`docs/rfq-demo-guide.md`를 기준으로 한다.

## CLI-001 — corner-store Reference CLI

Expand Down Expand Up @@ -639,3 +649,142 @@ passing

문서 정합화와 운영 runbook만 추가한다. contract, API 또는 production 운영 정책은
변경하지 않는다.

## PROD-001 — Production Control Plane

### Behavior

- `OperatorRegistry`가 global, asset, venue pause의 단일 source of truth가 된다.
- operator는 위험을 즉시 중단할 수 있지만 unpause는 owner가 예약한 뒤 최소
timelock이 지난 후에만 실행할 수 있다.
- `ExecutionRouter`는 compliance evaluation과 venue dispatch 전에 global, 양쪽
asset, venue pause를 fail-closed로 검사한다.
- Manifest lifecycle은 현재 version, pending update, full manifest hash와
append-only history hash를 보존한다.
- ACTIVE/SUSPENDED Manifest의 semantic update는 별도 pending proposal로 저장되고
timelock 이후 승인되며, suspended asset을 update만으로 재개할 수 없다.
- lifecycle/pause 변경은 actor, old/new value, reasonCode/reasonHash와 effective time을
event로 남긴다.

### Verification

- `forge fmt --check`
- `forge lint --severity high --deny warnings src`
- `forge test --offline --match-path test/unit/registry/OperatorRegistry.t.sol -vv`
- `forge test --offline --match-path test/unit/registry/TokenPolicyRegistry.t.sol -vv`
- `forge test --offline --match-path test/unit/execution/Router.t.sol -vv`
- `forge test --offline --match-path test/integration/EmergencyPause.t.sol -vv`
- `forge test --offline`
- `scripts/check.sh`

### State

passing

### Notes

- 완료 계획: `docs/exec-plans/completed/PROD-001-production-control-plane.md`
- governance owner는 외부 Safe-style multisig를 전제로 하며 컨트랙트 내부에
n-of-m signer 로직을 구현하지 않는다.
- issuer disable, production multisig provider와 chain별 timelock 값은 후속 운영
설정이며, 현재 manifest에 issuer identity가 없으므로 asset pause로 fail-closed한다.

## DATA-001 — Compliance Data Layer Foundation

### Behavior

- ADR-008의 Transfer Agent 경계를 provider-neutral TypeScript SDK로 제공한다.
- per-lot acquisition 입력은 lineage, 완납일과 freshness를 검증하고 보수적인
holder×asset snapshot으로 컴파일한다.
- `Lockup`은 operator-attested snapshot의 상태와 만료를 fail-closed로 검사한다.
- 거절 시도와 router 밖 transfer finding은 PII 없이 hash-chain audit trail에
append할 수 있다.
- person-group 단위 volume/holder state는 execution idempotency를 보장한다.
- 실제 Securitize API, WORM storage와 production surveillance hosting은 adapter
교체 지점으로 남기며 구현되었다고 주장하지 않는다.

### Verification

- `forge fmt --check`
- `forge test --offline --match-path test/unit/compliance/AcquisitionSource.t.sol -vv`
- `forge test --offline --match-path test/unit/compliance/Elements.t.sol -vv`
- `cd services/compliance-data && npm test`
- `scripts/check.sh`

### State

passing

### Notes

- 완료 계획: `docs/exec-plans/completed/DATA-001-compliance-data-layer.md`
- 실제 Securitize/TA field mapping은 공식 API 계약이 제공될 때 별도 provider
adapter로 구현한다.
- 단일 holder×asset snapshot은 모든 현재 lot 중 가장 늦은 clock을 사용하므로
amount-specific lot allocation 전까지 일부 mature lot 매도를 보수적으로 막을 수 있다.

### Related Files

- `services/compliance-data/`
- `src/registry/AttestedAcquisitionSource.sol`
- `src/compliance/elements/Lockup.sol`
- `test/unit/compliance/AcquisitionSource.t.sol`

## MANIFEST-002 — RecipeBinding Manifest Migration

### Behavior

- 자산 Manifest는 고정 `issuanceRecipeId + fundRecipeId` 대신 bounded
`RecipeBinding[]`를 registry에 저장한다.
- `REQUIRED_BLOCKING` Recipe는 AND, 같은 `pathGroupId`의 `PATH_OPTION`은 OR,
서로 다른 path group은 AND로 평가한다.
- `FLAG_ONLY` 실패는 거래를 막지 않고 `ComplianceDecision.flagsBitmap`과 Router
event로 노출한다.
- binding 변경은 full manifest hash 변경과 기존 timelock/version/history를 거친다.
- binding 수, recipe/version, path group과 duplicate 입력은 등록 시 fail-closed로
검증한다.

### Verification

- RecipeBinding registry/lifecycle unit tests
- REQUIRED/PATH/FLAG engine regression tests
- pair-side, stateful commit와 Router event integration tests
- `scripts/check.sh`
- `buidl-like` / `reg-d` live E2E

### State

passing

### Notes

- 완료 계획: `docs/exec-plans/completed/MANIFEST-002-recipe-binding-migration.md`
- canonical `bytes32 recipeKey` alias와 per-element enforcement override compiler는
별도 versioned refinement로 남긴다.

## AMM-001 — Canonical Uniswap v3 Pool E2E

### Behavior

- vendored `@uniswap/v3-core` artifact로 canonical factory/pool을 배포한다.
- CREATE2 예상 주소와 factory가 생성한 pool 주소가 일치해야 한다.
- 실제 pool을 verified ERC-3643 holder, venue와 adapter allowlist에 등록한다.
- 초기화·유동성 공급 후 `ExecutionRouter → UniswapV3Adapter → pool` exact-input
swap이 실제 callback과 ERC-3643 transfer를 거쳐 성공한다.
- 미등록 pool/callback은 계속 fail-closed이고 Router/Adapter는 잔액을 보유하지 않는다.

### Verification

- canonical factory/pool deployment와 CREATE2 preflight integration test
- protected buy/sell, compliance rejection와 callback authorization tests
- `scripts/check.sh`

### State

passing

### Notes

- 완료 계획: `docs/exec-plans/completed/AMM-001-real-uniswap-v3-e2e.md`
- vendored Solidity source를 제품 `src/`로 복사하지 않는다.
- production fee-tier 승인, LP 운영 정책과 unified deploy CLI는 별도 후속 범위다.
Loading
Loading