Skip to content

Commit 85e2154

Browse files
committed
Record the manifest lifecycle decision (CMP-002)
1 parent f0ca28e commit 85e2154

3 files changed

Lines changed: 188 additions & 21 deletions

File tree

DECISIONS.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,109 @@ criteria remain approval-gated").
379379
`test/integration/RegD506cElements.t.sol`
380380
- `test/unit/compliance/Recipes.t.sol`, `test/unit/compliance/Engine.t.sol`
381381
- `docs/ROADMAP.md`
382+
383+
## D009 — Manifest lifecycle를 validated state machine로 만들고 engine을 default-deny로 닫는다
384+
385+
Date: 2026-07-04
386+
387+
### Context
388+
389+
MVP-v2 §5는 asset Manifest에 explicit operator approval step을 포함한 full
390+
lifecycle을 요구한다. 기존 `TokenPolicyRegistry`는 임의 상태를 덮어쓰는 raw
391+
`setStatus`만 제공했고(승인 단계 없음), `ComplianceEngine.evaluate`는 SUSPENDED와
392+
UNKNOWN만 명시적으로 reject한 뒤 나머지를 `_evaluateActivePair`로 흘려보냈다. 이
393+
구조에 PROPOSED/RETIRED 상태를 추가하면 두 개의 fail-open 구멍이 생긴다: (a)
394+
PROPOSED/RETIRED 자산이 UNREGULATED와 pair되면 element가 0개 수집되어 `allowed =
395+
true`, (b) PROPOSED/RETIRED가 ACTIVE와 pair되면 ACTIVE side만 검증되고 거래가
396+
통과한다.
397+
398+
### Decision
399+
400+
**1. Lifecycle state machine (states/transitions).** Manifest는 아래 transition만
401+
허용하는 명시적 state machine을 따른다. 그 외 모든 (state, action)은 dedicated
402+
custom error `Errors.InvalidManifestTransition`으로 revert한다.
403+
404+
| From | Action | To | Gate |
405+
| --- | --- | --- | --- |
406+
| UNKNOWN 또는 RETIRED | `registerManifest` | PROPOSED | onlyOwner |
407+
| PROPOSED | `approveManifest` | ACTIVE | onlyOperator (issuance recipe 필수) |
408+
| ACTIVE | `suspendManifest(reasonCode)` | SUSPENDED | onlyOperator |
409+
| SUSPENDED | `resumeManifest` | ACTIVE | onlyOperator |
410+
| ACTIVE 또는 SUSPENDED | `retireManifest(reasonCode)` | RETIRED (terminal) | onlyOperator |
411+
| UNKNOWN | `setUnregulated` | UNREGULATED | onlyOwner |
412+
| UNREGULATED | `clearUnregulated` | UNKNOWN | onlyOwner |
413+
414+
`registerManifest`는 caller가 넣은 `m.status`를 무시하고 항상 PROPOSED로 착지하며
415+
`declaredBy = msg.sender`를 기록한다. `approveManifest``approvedBy = msg.sender`
416+
기록하고 `issuanceRecipeId == 0`이면 registry-level completeness floor로 revert한다.
417+
RETIRED는 terminal이며 재발행은 RETIRED→register→approve 경로로만 가능하다. gating
418+
model: owner가 자산을 classify/declare(register/setUnregulated/clearUnregulated)하고,
419+
operator가 기존 manifest의 lifecycle(approve/suspend/resume/retire)을 구동한다.
420+
421+
**2. Enum-append storage rationale.** `PolicyStatus`에 PROPOSED(4)와 RETIRED(5)를
422+
SUSPENDED(3) 뒤에 APPEND한다. 기존 numeric value(UNKNOWN=0, UNREGULATED=1,
423+
ACTIVE=2, SUSPENDED=3)는 storage layout과 enum↔uint cast에서 load-bearing이므로
424+
절대 재정렬하지 않는다. 그 결과 enum의 numeric order는 lifecycle graph의 semantic
425+
order와 일치하지 않는다(type 파일에 명시적 주석). UNKNOWN=0을 유지하는 것은 absent
426+
manifest가 fail-closed default가 되도록 하는 핵심이다.
427+
428+
**3. `setStatus` 제거.** raw `setStatus` overwrite는 제거한다(pre-production
429+
breaking change; 모든 caller/test/factory를 이 feature에서 갱신). validation 없는
430+
상태 덮어쓰기는 approval step과 transition guard를 우회하므로 유지할 수 없다.
431+
432+
**4. Engine default-deny 재구조화 (positive allowlist).** `evaluate`의 enumerated
433+
bad-status 검사(SUSPENDED/UNKNOWN reject)를 side별 positive allowlist로 교체한다:
434+
각 side는 UNREGULATED 또는 ACTIVE여야 하며, 그 외(UNKNOWN/SUSPENDED/PROPOSED/
435+
RETIRED와 미래에 추가될 모든 member)는 fail-closed한다. bad state를 열거하는 대신
436+
permitted state를 열거하는 이유: reject list에 새 상태를 추가하는 것을 잊으면
437+
그대로 fail-open이 되지만, allowlist는 새로 추가된 상태를 default로 거부하므로
438+
"append-only 안전"하다. both-UNREGULATED fast-path pass-through는 유지한다.
439+
`commit`은 router가 `evaluate`를 먼저 호출하고 reject 시 revert하므로 rejected
440+
pair에서 도달 불가능하다 — mirror guard가 필요 없고, in-file 주석으로 이유를
441+
남긴다(재구조화하지 않음).
442+
443+
**5. `clearUnregulated` correction path.** 잘못 UNREGULATED로 tag된 token을
444+
UNKNOWN(clean slate)으로 되돌려 이후 regulated manifest로 register할 수 있게 한다.
445+
onlyOwner이며 UNREGULATED가 아닌 상태에서 호출하면 `InvalidManifestTransition`.
446+
setUnregulated와 대칭인 governance classification 호출이다.
447+
448+
**6. `declaredBy = msg.sender` semantics와 factory consequence.** register는
449+
`declaredBy = msg.sender`, approve는 `approvedBy = msg.sender`를 기록한다.
450+
`CornerStoreFactory.registerRWAToken`은 register→approve를 한 governed call에서
451+
연속 실행하며 factory가 registry의 owner이자 approving operator다(배포 시
452+
governance가 registry ownership을 factory로 이전). 그 결과 이 경로로 onboarding된
453+
token의 `declaredBy`/`approvedBy`는 factory 주소이며, attribution은 factory
454+
경계에서 멈춘다.
455+
456+
### Alternatives Considered
457+
458+
- **Engine에서 reject list(bad state 열거) 유지**: PolicyStatus에 member를 추가할
459+
때마다 reject list 갱신을 잊으면 fail-open이 되므로 제외. positive allowlist는
460+
구조적으로 fail-closed default를 보장한다.
461+
- **enum member를 semantic order로 재정렬**: storage layout과 enum↔uint cast가
462+
깨지므로 제외. append-only만 허용한다.
463+
- **`setStatus`를 deprecated로 유지**: validation 우회 경로가 남으므로 제외
464+
(pre-production이라 breaking removal 비용이 낮다).
465+
- **PROPOSED/RETIRED에 UNKNOWN/SUSPENDED와 다른 별도 reason-code family 부여**:
466+
기존 `_rejectPolicy`가 이미 `uint32(status)`를 reason code에 인코딩하므로 side의
467+
실제 status를 그대로 넘기면 distinct code가 자동으로 나온다 — 추가 scheme 불필요.
468+
469+
### Consequences
470+
471+
- 모든 onboarding은 propose→approve 2단계를 거친다(factory는 한 call에서 collapse).
472+
- ACTIVE manifest의 issuance recipe re-point는 retire→register→approve 경로를
473+
써야 한다(ACTIVE 위 re-register는 illegal).
474+
- engine은 미래에 PolicyStatus member가 추가되어도 default로 fail-closed한다.
475+
- fixture는 register→approve(및 UNREGULATED는 setUnregulated)로 통일된다.
476+
477+
### Related Files
478+
479+
- `src/types/ComplianceTypes.sol` (enum append + 주석)
480+
- `src/registry/TokenPolicyRegistry.sol`,
481+
`src/interfaces/compliance/ITokenPolicyRegistry.sol`
482+
- `src/compliance/ComplianceEngine.sol` (evaluate default-deny gate)
483+
- `src/factory/CornerStoreFactory.sol` (register+approve, natspec)
484+
- `test/unit/compliance/Engine.t.sol`,
485+
`test/unit/registry/TokenPolicyRegistry.t.sol`
486+
- `test/integration/EmergencyPause.t.sol`, `test/integration/IntegrationBase.sol`,
487+
`test/integration/Surveillance.t.sol`

FEATURES.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,52 @@ passing
203203
정렬, production data source 연결, acquisition/lot data source와 holding-period
204204
활성화 조건 결정.
205205
- Non-goals: production legal 활성화, direction-aware element application.
206+
207+
## CMP-002 — Manifest Lifecycle & Operator Approval Flow
208+
209+
### Behavior
210+
211+
- Manifest는 validated state machine을 따른다: UNKNOWN --register--> PROPOSED
212+
--approve--> ACTIVE, ACTIVE <--resume--/--suspend--> SUSPENDED,
213+
{ACTIVE, SUSPENDED} --retire--> RETIRED(terminal), UNKNOWN --setUnregulated-->
214+
UNREGULATED --clearUnregulated--> UNKNOWN. 그 외 모든 transition은
215+
`InvalidManifestTransition`으로 revert한다.
216+
- `registerManifest`는 caller-supplied status를 무시하고 항상 PROPOSED로 착지하며
217+
`declaredBy = msg.sender`를 기록한다. `approveManifest``approvedBy`를 기록하고
218+
issuance recipe가 비어 있으면 revert한다. raw `setStatus`는 제거되었다.
219+
- owner가 자산을 classify(register/setUnregulated/clearUnregulated)하고 operator가
220+
기존 manifest의 lifecycle(approve/suspend/resume/retire)을 구동한다.
221+
- `ComplianceEngine.evaluate`는 side별 positive allowlist(UNREGULATED 또는 ACTIVE만
222+
허용)로 default-deny한다. UNKNOWN/SUSPENDED/PROPOSED/RETIRED와 미래 member는
223+
fail-closed하며, both-UNREGULATED fast-path는 유지된다.
224+
- `CornerStoreFactory.registerRWAToken`은 register→approve를 한 governed call에서
225+
실행하고 token은 ACTIVE로 끝난다(`declaredBy`/`approvedBy` = factory).
226+
227+
### Verification
228+
229+
- `forge test --offline`(전체 227/227, pre-task 212 + 신규 15).
230+
- Engine unit test(default-deny fail-closed): `test_proposed_against_unregulated_fails_closed`,
231+
`test_retired_against_unregulated_fails_closed`, `test_proposed_against_active_fails_closed`,
232+
`test_retired_against_active_fails_closed_both_orderings`(양방향 ordering).
233+
- Registry unit test(clearUnregulated + onlyOwner-vs-onlyOperator):
234+
`test_clearUnregulated_from_UNREGULATED`, `test_clearUnregulated_then_register_ok`,
235+
`test_clearUnregulated_reverts_when_UNKNOWN`, `test_clearUnregulated_reverts_when_PROPOSED`,
236+
`test_clearUnregulated_reverts_when_ACTIVE`, `test_clearUnregulated_reverts_for_non_owner`,
237+
`test_clearUnregulated_reverts_for_operator`, `test_setUnregulated_reverts_for_operator`.
238+
- 통합 test `test/integration/EmergencyPause.t.sol`(router end-to-end):
239+
`test_proposedPolicy_failsClosed`, `test_retiredPolicy_failsClosed`,
240+
`test_suspendThenResume_tradesAgain`.
241+
- 기존 registry lifecycle state-machine unit test(Task 1, 25 tests)는 유지.
242+
- `forge fmt`.
243+
244+
### State
245+
246+
passing
247+
248+
### Notes
249+
250+
- 정책 결정: D009(lifecycle state machine, enum-append, setStatus 제거, engine
251+
positive-allowlist default-deny, clearUnregulated correction path, declaredBy=
252+
msg.sender와 factory consequence).
253+
- Non-goals: direction-aware element application, production onboarding governance
254+
key management.

PROGRESS.md

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ source of truth로 사용한다.
2222
- `RFQ-002 — RFQ v2 Hardening`
2323
- `CMP-001 — Reg D 506(c) 9-element Recipe`(illustrative element library + recipe
2424
9-element 확장, version 2)
25+
- `CMP-002 — Manifest Lifecycle & Operator Approval Flow`(validated state machine,
26+
setStatus 제거, engine positive-allowlist default-deny, clearUnregulated,
27+
factory register→approve)
2528
- multi-venue 아키텍처와 책임 문서 작성
2629
- Corner Store용 Uniswap v3 최소 배포 profile 분리와 테스트
2730
- ExecutionRouter/VenueRegistry/VenueSelector와 AMM reference adapter skeleton
@@ -38,42 +41,51 @@ source of truth로 사용한다.
3841

3942
## Next
4043

41-
1. Manifest lifecycle/operator approval flow를 구현한다. (Element library와 Reg D
42-
506(c) Recipe 9-element 확장은 CMP-001로 완료.)
43-
2. ungated legacy mock element(A-01 sanctions, A-03 accredited, QP)를 새 element와
44+
1. ungated legacy mock element(A-01 sanctions, A-03 accredited, QP)를 새 element와
4445
동일하게 operator-gate로 정렬한다 — CMP-001 deferred follow-up.
45-
3. RFQ integration-test 시나리오(router-path maker-approval/cancellation coverage)를
46+
2. RFQ integration-test 시나리오(router-path maker-approval/cancellation coverage)를
4647
추가한다 — RFQ-002에서 deferred된 follow-up.
47-
4. acquisition/lot data source와 holding-period Recipe 활성화 조건을 결정한다
48+
3. acquisition/lot data source와 holding-period Recipe 활성화 조건을 결정한다
4849
(C-01 Lockup은 현재 fixture-only mock acquisition source).
49-
5. live Anvil deployment/E2E를 추가한다.
50-
6. Order Book은 matching/custody/surveillance 모델 결정 후 구현한다.
51-
7. CI hardening(static analysis 등)을 강화한다.
50+
4. live Anvil deployment/E2E를 추가한다.
51+
5. Order Book은 matching/custody/surveillance 모델 결정 후 구현한다.
52+
6. CI hardening(static analysis 등)을 강화한다.
5253

5354
## Last Session Summary
5455

55-
- CMP-001 (Reg D 506(c) 9-element Recipe)을 landing했다.
56+
- CMP-002 (Manifest Lifecycle & Operator Approval Flow)을 landing했다. Task 1
57+
(registry state machine)에 이어 Task 2에서 engine fail-open을 닫고 factory/seam
58+
finalization, 통합 시나리오, bookkeeping을 완료했다.
5659
- 변경한 파일:
57-
- product: `src/compliance/recipes/RegD506cRecipe.sol`(2→9 element, version 2)
58-
- test: `test/unit/compliance/Recipes.t.sol`, `test/unit/compliance/Engine.t.sol`,
59-
`test/integration/IntegrationBase.sol`, `test/integration/SwapFlow.t.sol`,
60-
신규 `test/integration/RegD506cElements.t.sol`
61-
- bookkeeping: `DECISIONS.md`(D008), `FEATURES.md`(CMP-001), `PROGRESS.md`
62-
- 6개 새 illustrative element(A-02/A-04/A-05/B-01/B-02/E-01)는 앞선 리뷰된
63-
브랜치에서 landed; 이번 세션은 recipe 확장 + fixture wiring + bookkeeping.
60+
- product: `src/compliance/ComplianceEngine.sol`(evaluate positive-allowlist
61+
default-deny + commit 주석), `src/registry/TokenPolicyRegistry.sol` +
62+
`src/interfaces/compliance/ITokenPolicyRegistry.sol`(clearUnregulated),
63+
`src/factory/CornerStoreFactory.sol`(register→approve natspec)
64+
- test: `test/unit/compliance/Engine.t.sol`(신규 4 default-deny),
65+
`test/unit/registry/TokenPolicyRegistry.t.sol`(신규 8 clearUnregulated+auth),
66+
`test/integration/EmergencyPause.t.sol`(신규 3 router E2E),
67+
`test/integration/IntegrationBase.sol`/`Surveillance.t.sol`(seam 정리)
68+
- bookkeeping: `DECISIONS.md`(D009), `FEATURES.md`(CMP-002), `PROGRESS.md`
69+
- Task 1(앞선 커밋): `src/types/ComplianceTypes.sol`(enum append),
70+
`src/libraries/Errors.sol`(InvalidManifestTransition), registry state machine
71+
- TDD: engine 4개 default-deny 테스트가 먼저 RED로 오늘의 fail-open(PROPOSED/
72+
RETIRED × UNREGULATED/ACTIVE가 allowed=true)을 증명한 뒤 positive-allowlist로
73+
GREEN 전환.
6474
- 실행한 명령:
6575
- `forge fmt`
6676
- `forge test --offline`
6777
- 통과한 검증:
68-
- `forge test --offline` 195/195(pre-task 189 + 신규 6).
69-
- recipe unit test로 9-element/version 2/always-applicable 확인.
70-
- element family별 rejection(A-02 disallowed/unset, A-04, A-05, B-01)은 정확한
71-
reasonCode로 assert.
78+
- `forge test --offline` 227/227(pre-task 212 + 신규 15).
79+
- engine default-deny(양방향 ordering 포함), registry clearUnregulated +
80+
onlyOwner-vs-onlyOperator auth, 통합 PROPOSED/RETIRED reject + suspend→resume
81+
재거래.
7282
- 남은 리스크:
7383
- ungated legacy mock element(A-01/A-03/QP)와 새 operator-gated element 사이
74-
hardening divergence — follow-up으로 정렬.
84+
hardening divergence — follow-up으로 정렬(CMP-001 deferred).
7585
- C-01 Lockup은 fixture-only mock acquisition source에 의존; production
7686
acquisition/lot data source와 holding-period 활성화 default는 미결정.
7787
- production data source(OFAC/ONCHAINID/ERC-165/EDGAR) 연결과 legal 활성화는
7888
approval-gated로 열려 있다.
7989
- engine은 direction-aware가 아니다(기존 문서화된 concern).
90+
- production onboarding governance key management(factory ownership/multisig)는
91+
미결정.

0 commit comments

Comments
 (0)