Skip to content
Merged
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
20 changes: 10 additions & 10 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ source of truth로 사용한다.
`RFQMakerNotApproved`), maker-initiated nonce-scoped idempotent cancellation
(`cancelQuoteNonce`/`cancelQuoteNonces`, `RFQQuoteCancelled`), venueType binding
fix, `docs/rfq-threat-model.md` 위협 모델과 D008 결정 기록
- legacy mock element(Sanctions A-01, AccreditedInvestor A-03, QualifiedPurchaser
A-13)의 attestation setter를 `Governed`/`onlyOperator` + 이벤트로 정렬해 CMP-001
이후 hardening divergence를 닫았다(Lockup C-01은 settable mutator가 없어 변경 없음).

## Blocked

Expand All @@ -45,28 +48,25 @@ source of truth로 사용한다.

1. MVP RFQ demo backend milestone/user flow를 별도 문서·feature로 구체화한다.
PR #29/#30이 머지되면 기존 live-Anvil E2E/CLI 경로를 재사용한다.
2. pending RFQ/E2E/CLI/BUIDL PR stack(#26/#27/#28/#29/#30/#35)이 머지된 뒤
2. pending RFQ/E2E/CLI/BUIDL PR stack(#28/#29/#30/#35)이 머지된 뒤
roadmap과 feature 상태를 재조정한다.
3. ungated legacy mock element(A-01 sanctions, A-03 accredited, QP)를 새 element와
동일하게 operator-gate로 정렬한다 — CMP-001 deferred follow-up.
4. 남은 RFQ production policy를 별도 feature로 분리한다: custody, partial fill,
3. 남은 RFQ production policy를 별도 feature로 분리한다: custody, partial fill,
production dealer/operator 책임.
5. acquisition/lot data source와 holding-period Recipe 활성화 조건을 결정한다
4. acquisition/lot data source와 holding-period Recipe 활성화 조건을 결정한다
(C-01 Lockup은 현재 fixture-only mock acquisition source).
6. Order Book은 matching/custody/surveillance 모델 결정 후 구현한다.
5. Order Book은 matching/custody/surveillance 모델 결정 후 구현한다.

## Last Session Summary

- #26 merge preparation includes CMP-002 (Manifest Lifecycle & Operator Approval Flow) on top of current main.
- CMP-002 closes the manifest lifecycle state machine and engine default-deny path after CMP-001.
- RFQ-002, CMP-001, and RFQ-SDK records are preserved during this stacked merge reconciliation.
- #27 merge preparation includes legacy element gating on top of current main.
- Legacy mock element setters for A-01/A-03/A-13 are now operator-gated, closing the CMP-001 hardening divergence.
- RFQ-002, CMP-001, CMP-002, and RFQ-SDK records are preserved during this stacked merge reconciliation.
- 실행한 검증:
- original PR CI/checks passed before retarget
- conflict reconciliation checked with `git diff --check`
- 남은 리스크:
- MVP HTTP/CLI backend는 아직 구현하지 않았다.
- production signer custody, persistent nonce store, pricing, inventory/risk는 integrator/operator 책임이다.
- ungated legacy mock element(A-01/A-03/QP)와 새 operator-gated element 사이 hardening divergence는 follow-up으로 정렬한다.
- C-01 Lockup은 fixture-only mock acquisition source에 의존한다.

## RFQ-002 Merge Note
Expand Down
8 changes: 6 additions & 2 deletions src/compliance/elements/AccreditedInvestor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.17;

import {BaseElement} from "./BaseElement.sol";
import {Governed} from "../../auth/Governed.sol";
import {
ElementMetadata,
ElementCategory,
Expand All @@ -14,11 +15,13 @@ import {ReasonCodes} from "../../libraries/ReasonCodes.sol";

/// @dev A-03-v1 Accredited investor attestation (mock). Settable per-user flag
/// stands in for a real claim (existence + issuer + expiry, Pattern B).
contract AccreditedInvestor is BaseElement {
contract AccreditedInvestor is BaseElement, Governed {
bytes32 internal constant ELEMENT_ID = "A-03-v1";

mapping(address => bool) public accredited;

event AccreditedInvestorSet(address indexed investor, bool isAccredited);

constructor()
BaseElement(ElementMetadata({
elementId: ELEMENT_ID,
Expand All @@ -31,8 +34,9 @@ contract AccreditedInvestor is BaseElement {
}))
{}

function setAccredited(address user, bool isAccredited) external {
function setAccredited(address user, bool isAccredited) external onlyOperator {
accredited[user] = isAccredited;
emit AccreditedInvestorSet(user, isAccredited);
}

function check(address user, address, address, uint256, bytes calldata)
Expand Down
8 changes: 6 additions & 2 deletions src/compliance/elements/QualifiedPurchaser.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.17;

import {BaseElement} from "./BaseElement.sol";
import {Governed} from "../../auth/Governed.sol";
import {
ElementMetadata,
ElementCategory,
Expand All @@ -14,11 +15,13 @@ import {ReasonCodes} from "../../libraries/ReasonCodes.sol";

/// @dev A-13-v1 Qualified purchaser attestation (mock). Activated conditionally
/// by the 3(c)(7) fund recipe; settable per-user flag stands in for a claim.
contract QualifiedPurchaser is BaseElement {
contract QualifiedPurchaser is BaseElement, Governed {
bytes32 internal constant ELEMENT_ID = "A-13-v1";

mapping(address => bool) public qp;

event QualifiedPurchaserSet(address indexed investor, bool isQp);

constructor()
BaseElement(ElementMetadata({
elementId: ELEMENT_ID,
Expand All @@ -31,8 +34,9 @@ contract QualifiedPurchaser is BaseElement {
}))
{}

function setQp(address user, bool isQp) external {
function setQp(address user, bool isQp) external onlyOperator {
qp[user] = isQp;
emit QualifiedPurchaserSet(user, isQp);
}

function check(address user, address, address, uint256, bytes calldata)
Expand Down
8 changes: 6 additions & 2 deletions src/compliance/elements/Sanctions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.17;

import {BaseElement} from "./BaseElement.sol";
import {Governed} from "../../auth/Governed.sol";
import {
ElementMetadata,
ElementCategory,
Expand All @@ -14,11 +15,13 @@ import {ReasonCodes} from "../../libraries/ReasonCodes.sol";

/// @dev A-01-v1 Sanctions screen (mock). Blocks listed users at the trade gate.
/// Real list management is out of scope — a settable mapping stands in.
contract Sanctions is BaseElement {
contract Sanctions is BaseElement, Governed {
bytes32 internal constant ELEMENT_ID = "A-01-v1";

mapping(address => bool) public blocked;

event SanctionsBlockedSet(address indexed account, bool blocked);

constructor()
BaseElement(ElementMetadata({
elementId: ELEMENT_ID,
Expand All @@ -31,8 +34,9 @@ contract Sanctions is BaseElement {
}))
{}

function setBlocked(address user, bool isBlocked) external {
function setBlocked(address user, bool isBlocked) external onlyOperator {
blocked[user] = isBlocked;
emit SanctionsBlockedSet(user, isBlocked);
}

function check(address user, address, address, uint256, bytes calldata)
Expand Down
53 changes: 53 additions & 0 deletions test/unit/compliance/Elements.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Statefulness
} from "../../../src/types/ComplianceTypes.sol";
import {Events} from "../../../src/libraries/Events.sol";
import {Errors} from "../../../src/libraries/Errors.sol";

contract MockAcquisitionSource is IAcquisitionSource {
mapping(bytes32 => uint64) internal _at;
Expand All @@ -31,8 +32,15 @@ contract MockAcquisitionSource is IAcquisitionSource {
}

contract ElementsTest is Test {
// Mirrors each legacy element's *Set event signature for vm.expectEmit matching
// (solc 0.8.17 does not support qualified `ContractName.EventName` emit syntax).
event SanctionsBlockedSet(address indexed account, bool blocked);
event AccreditedInvestorSet(address indexed investor, bool isAccredited);
event QualifiedPurchaserSet(address indexed investor, bool isQp);

address internal user = address(0xA11CE);
address internal asset = address(0xBEEF);
address internal stranger = address(0xDEAD);

function test_sanctions_pass_and_fail_and_metadata() public {
Sanctions s = new Sanctions();
Expand All @@ -54,6 +62,21 @@ contract ElementsTest is Test {
assertEq(uint256(m.statefulness), uint256(Statefulness.STATELESS));
}

function test_sanctions_setBlocked_reverts_for_non_operator() public {
Sanctions s = new Sanctions();
vm.prank(stranger);
vm.expectRevert(Errors.NotAuthorized.selector);
s.setBlocked(user, true);
}

function test_sanctions_setBlocked_updates_state_and_emits() public {
Sanctions s = new Sanctions();
vm.expectEmit(true, false, false, true);
emit SanctionsBlockedSet(user, true);
s.setBlocked(user, true);
assertTrue(s.blocked(user));
}

function test_accredited_pass_fail_metadata() public {
AccreditedInvestor a = new AccreditedInvestor();
(bool passed,) = a.check(user, address(0), asset, 0, "");
Expand All @@ -70,6 +93,21 @@ contract ElementsTest is Test {
assertEq(uint256(m.temporal), uint256(TemporalNature.ONE_TIME));
}

function test_accredited_setAccredited_reverts_for_non_operator() public {
AccreditedInvestor a = new AccreditedInvestor();
vm.prank(stranger);
vm.expectRevert(Errors.NotAuthorized.selector);
a.setAccredited(user, true);
}

function test_accredited_setAccredited_updates_state_and_emits() public {
AccreditedInvestor a = new AccreditedInvestor();
vm.expectEmit(true, false, false, true);
emit AccreditedInvestorSet(user, true);
a.setAccredited(user, true);
assertTrue(a.accredited(user));
}

function test_qp_pass_fail_metadata() public {
QualifiedPurchaser q = new QualifiedPurchaser();
(bool passed,) = q.check(user, address(0), asset, 0, "");
Expand All @@ -84,6 +122,21 @@ contract ElementsTest is Test {
assertEq(uint256(m.decidability), uint256(Decidability.ATTESTATION_BASED));
}

function test_qp_setQp_reverts_for_non_operator() public {
QualifiedPurchaser q = new QualifiedPurchaser();
vm.prank(stranger);
vm.expectRevert(Errors.NotAuthorized.selector);
q.setQp(user, true);
}

function test_qp_setQp_updates_state_and_emits() public {
QualifiedPurchaser q = new QualifiedPurchaser();
vm.expectEmit(true, false, false, true);
emit QualifiedPurchaserSet(user, true);
q.setQp(user, true);
assertTrue(q.qp(user));
}

function test_lockup_blocks_until_elapsed() public {
MockAcquisitionSource src = new MockAcquisitionSource();
uint64 lockupSeconds = 100;
Expand Down
Loading