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
15 changes: 8 additions & 7 deletions PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,23 @@ source of truth로 사용한다.
`RFQMakerNotApproved`), maker-initiated nonce-scoped idempotent cancellation
(`cancelQuoteNonce`/`cancelQuoteNonces`, `RFQQuoteCancelled`), venueType binding
fix, `docs/rfq-threat-model.md` 위협 모델과 D007 결정 기록
- 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

- 없음

## Next

1. ungated legacy mock element(A-01 sanctions, A-03 accredited, QP)를 새 element와
동일하게 operator-gate로 정렬한다 — CMP-001 deferred follow-up.
2. RFQ integration-test 시나리오(router-path maker-approval/cancellation coverage)를
1. RFQ integration-test 시나리오(router-path maker-approval/cancellation coverage)를
추가한다 — RFQ-002에서 deferred된 follow-up.
3. acquisition/lot data source와 holding-period Recipe 활성화 조건을 결정한다
2. acquisition/lot data source와 holding-period Recipe 활성화 조건을 결정한다
(C-01 Lockup은 현재 fixture-only mock acquisition source).
4. live Anvil deployment/E2E를 추가한다.
5. Order Book은 matching/custody/surveillance 모델 결정 후 구현한다.
6. CI hardening(static analysis 등)을 강화한다.
3. live Anvil deployment/E2E를 추가한다.
4. Order Book은 matching/custody/surveillance 모델 결정 후 구현한다.
5. CI hardening(static analysis 등)을 강화한다.

## Last Session Summary

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