Skip to content

Commit c3e0d1e

Browse files
committed
Gate legacy element attestations behind the operator role
Sanctions (A-01), AccreditedInvestor (A-03), and QualifiedPurchaser (A-13) had ungated, eventless attestation setters, diverging from the Governed/onlyOperator + event pattern used by the six CMP-001 elements (Jurisdiction, UsTaxResident, etc). Extend Governed on all three, gate setBlocked/setAccredited/setQp with onlyOperator, and emit an event per change. Lockup (C-01) has no settable mutator (state is injected via IAcquisitionSource), so it is left untouched. Add per-element outsider-auth-revert and set+emit tests to Elements.t.sol, written RED first against the unmodified elements. Existing fixtures (Engine.t.sol, IntegrationBase.sol, MultiRecipe.t.sol, SwapFlow.t.sol) call these setters from the deploying test contract itself, i.e. as owner, which onlyOperator already admits, so no fixture changes were needed.
1 parent 01bfe98 commit c3e0d1e

5 files changed

Lines changed: 79 additions & 13 deletions

File tree

PROGRESS.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,23 @@ source of truth로 사용한다.
3434
`RFQMakerNotApproved`), maker-initiated nonce-scoped idempotent cancellation
3535
(`cancelQuoteNonce`/`cancelQuoteNonces`, `RFQQuoteCancelled`), venueType binding
3636
fix, `docs/rfq-threat-model.md` 위협 모델과 D007 결정 기록
37+
- legacy mock element(Sanctions A-01, AccreditedInvestor A-03, QualifiedPurchaser
38+
A-13)의 attestation setter를 `Governed`/`onlyOperator` + 이벤트로 정렬해 CMP-001
39+
이후 hardening divergence를 닫았다(Lockup C-01은 settable mutator가 없어 변경 없음).
3740

3841
## Blocked
3942

4043
- 없음
4144

4245
## Next
4346

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

5455
## Last Session Summary
5556

src/compliance/elements/AccreditedInvestor.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity 0.8.17;
33

44
import {BaseElement} from "./BaseElement.sol";
5+
import {Governed} from "../../auth/Governed.sol";
56
import {
67
ElementMetadata,
78
ElementCategory,
@@ -14,11 +15,13 @@ import {ReasonCodes} from "../../libraries/ReasonCodes.sol";
1415

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

2021
mapping(address => bool) public accredited;
2122

23+
event AccreditedInvestorSet(address indexed investor, bool isAccredited);
24+
2225
constructor()
2326
BaseElement(ElementMetadata({
2427
elementId: ELEMENT_ID,
@@ -31,8 +34,9 @@ contract AccreditedInvestor is BaseElement {
3134
}))
3235
{}
3336

34-
function setAccredited(address user, bool isAccredited) external {
37+
function setAccredited(address user, bool isAccredited) external onlyOperator {
3538
accredited[user] = isAccredited;
39+
emit AccreditedInvestorSet(user, isAccredited);
3640
}
3741

3842
function check(address user, address, address, uint256, bytes calldata)

src/compliance/elements/QualifiedPurchaser.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity 0.8.17;
33

44
import {BaseElement} from "./BaseElement.sol";
5+
import {Governed} from "../../auth/Governed.sol";
56
import {
67
ElementMetadata,
78
ElementCategory,
@@ -14,11 +15,13 @@ import {ReasonCodes} from "../../libraries/ReasonCodes.sol";
1415

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

2021
mapping(address => bool) public qp;
2122

23+
event QualifiedPurchaserSet(address indexed investor, bool isQp);
24+
2225
constructor()
2326
BaseElement(ElementMetadata({
2427
elementId: ELEMENT_ID,
@@ -31,8 +34,9 @@ contract QualifiedPurchaser is BaseElement {
3134
}))
3235
{}
3336

34-
function setQp(address user, bool isQp) external {
37+
function setQp(address user, bool isQp) external onlyOperator {
3538
qp[user] = isQp;
39+
emit QualifiedPurchaserSet(user, isQp);
3640
}
3741

3842
function check(address user, address, address, uint256, bytes calldata)

src/compliance/elements/Sanctions.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity 0.8.17;
33

44
import {BaseElement} from "./BaseElement.sol";
5+
import {Governed} from "../../auth/Governed.sol";
56
import {
67
ElementMetadata,
78
ElementCategory,
@@ -14,11 +15,13 @@ import {ReasonCodes} from "../../libraries/ReasonCodes.sol";
1415

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

2021
mapping(address => bool) public blocked;
2122

23+
event SanctionsBlockedSet(address indexed account, bool blocked);
24+
2225
constructor()
2326
BaseElement(ElementMetadata({
2427
elementId: ELEMENT_ID,
@@ -31,8 +34,9 @@ contract Sanctions is BaseElement {
3134
}))
3235
{}
3336

34-
function setBlocked(address user, bool isBlocked) external {
37+
function setBlocked(address user, bool isBlocked) external onlyOperator {
3538
blocked[user] = isBlocked;
39+
emit SanctionsBlockedSet(user, isBlocked);
3640
}
3741

3842
function check(address user, address, address, uint256, bytes calldata)

test/unit/compliance/Elements.t.sol

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
Statefulness
1818
} from "../../../src/types/ComplianceTypes.sol";
1919
import {Events} from "../../../src/libraries/Events.sol";
20+
import {Errors} from "../../../src/libraries/Errors.sol";
2021

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

3334
contract ElementsTest is Test {
35+
// Mirrors each legacy element's *Set event signature for vm.expectEmit matching
36+
// (solc 0.8.17 does not support qualified `ContractName.EventName` emit syntax).
37+
event SanctionsBlockedSet(address indexed account, bool blocked);
38+
event AccreditedInvestorSet(address indexed investor, bool isAccredited);
39+
event QualifiedPurchaserSet(address indexed investor, bool isQp);
40+
3441
address internal user = address(0xA11CE);
3542
address internal asset = address(0xBEEF);
43+
address internal stranger = address(0xDEAD);
3644

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

65+
function test_sanctions_setBlocked_reverts_for_non_operator() public {
66+
Sanctions s = new Sanctions();
67+
vm.prank(stranger);
68+
vm.expectRevert(Errors.NotAuthorized.selector);
69+
s.setBlocked(user, true);
70+
}
71+
72+
function test_sanctions_setBlocked_updates_state_and_emits() public {
73+
Sanctions s = new Sanctions();
74+
vm.expectEmit(true, false, false, true);
75+
emit SanctionsBlockedSet(user, true);
76+
s.setBlocked(user, true);
77+
assertTrue(s.blocked(user));
78+
}
79+
5780
function test_accredited_pass_fail_metadata() public {
5881
AccreditedInvestor a = new AccreditedInvestor();
5982
(bool passed,) = a.check(user, address(0), asset, 0, "");
@@ -70,6 +93,21 @@ contract ElementsTest is Test {
7093
assertEq(uint256(m.temporal), uint256(TemporalNature.ONE_TIME));
7194
}
7295

96+
function test_accredited_setAccredited_reverts_for_non_operator() public {
97+
AccreditedInvestor a = new AccreditedInvestor();
98+
vm.prank(stranger);
99+
vm.expectRevert(Errors.NotAuthorized.selector);
100+
a.setAccredited(user, true);
101+
}
102+
103+
function test_accredited_setAccredited_updates_state_and_emits() public {
104+
AccreditedInvestor a = new AccreditedInvestor();
105+
vm.expectEmit(true, false, false, true);
106+
emit AccreditedInvestorSet(user, true);
107+
a.setAccredited(user, true);
108+
assertTrue(a.accredited(user));
109+
}
110+
73111
function test_qp_pass_fail_metadata() public {
74112
QualifiedPurchaser q = new QualifiedPurchaser();
75113
(bool passed,) = q.check(user, address(0), asset, 0, "");
@@ -84,6 +122,21 @@ contract ElementsTest is Test {
84122
assertEq(uint256(m.decidability), uint256(Decidability.ATTESTATION_BASED));
85123
}
86124

125+
function test_qp_setQp_reverts_for_non_operator() public {
126+
QualifiedPurchaser q = new QualifiedPurchaser();
127+
vm.prank(stranger);
128+
vm.expectRevert(Errors.NotAuthorized.selector);
129+
q.setQp(user, true);
130+
}
131+
132+
function test_qp_setQp_updates_state_and_emits() public {
133+
QualifiedPurchaser q = new QualifiedPurchaser();
134+
vm.expectEmit(true, false, false, true);
135+
emit QualifiedPurchaserSet(user, true);
136+
q.setQp(user, true);
137+
assertTrue(q.qp(user));
138+
}
139+
87140
function test_lockup_blocks_until_elapsed() public {
88141
MockAcquisitionSource src = new MockAcquisitionSource();
89142
uint64 lockupSeconds = 100;

0 commit comments

Comments
 (0)