Skip to content

Commit 4ba3e2f

Browse files
authored
Merge branch 'main' into feat/appeal-window
2 parents b91463f + 3754826 commit 4ba3e2f

27 files changed

Lines changed: 1247 additions & 337 deletions

.claude/settings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(find /home/ljtwp/Desktop/drips/niff -type f -name *.sol -o -name *.rs -o -name *.ts -o -name *.tsx -o -name *.js -o -name *.jsx)",
5+
"Bash(cargo test:*)",
6+
"Bash(cargo fmt:*)",
7+
"Bash(cargo clippy:*)"
8+
]
9+
}
10+
}

contracts/niffyinsure/src/claim.rs

Lines changed: 172 additions & 315 deletions
Large diffs are not rendered by default.

contracts/niffyinsure/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ impl NiffyInsure {
468468
terminated_at_ledger: 0,
469469
termination_reason: TerminationReason::None,
470470
terminated_by_admin: false,
471+
strike_count: 0,
471472
};
472473
env.storage().persistent().set(
473474
&storage::DataKey::Policy(holder.clone(), policy_id),

contracts/niffyinsure/src/policy.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ pub fn initiate_policy(
272272
terminated_at_ledger: 0,
273273
termination_reason: crate::types::TerminationReason::None,
274274
terminated_by_admin: false,
275+
strike_count: 0,
275276
};
276277

277278
validate::check_policy(&policy).map_err(|_| PolicyError::PolicyValidation)?;

contracts/niffyinsure/src/policy_lifecycle.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub fn initiate_policy(
6666
terminated_at_ledger: 0,
6767
termination_reason: TerminationReason::None,
6868
terminated_by_admin: false,
69+
strike_count: 0,
6970
};
7071

7172
validate::check_policy(&policy).map_err(|e| match e {
@@ -212,5 +213,10 @@ fn termination_reason_tag(reason: TerminationReason) -> u32 {
212213
TerminationReason::FraudOrMisrepresentation => 4,
213214
TerminationReason::RegulatoryAction => 5,
214215
TerminationReason::AdminOverride => 6,
216+
// 7 = ExcessiveRejections: set by the claims engine via on_reject,
217+
// not by the policy-lifecycle termination flow. Included here for
218+
// completeness; PolicyTerminated is not normally emitted for this
219+
// reason — PolicyDeactivated (from claim.rs) is the canonical event.
220+
TerminationReason::ExcessiveRejections => 7,
215221
}
216222
}

contracts/niffyinsure/src/types.rs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ pub const IMAGE_URLS_MAX: u32 = 5;
77
pub const REASON_MAX_LEN: u32 = 128;
88
pub const SAFETY_SCORE_MAX: u32 = 100;
99

10+
// ── Rejection side-effect thresholds ─────────────────────────────────────────
11+
//
12+
// GOVERNANCE NOTE: This constant is the only on-chain parameter controlling
13+
// automatic policy deactivation. Changing it requires a contract upgrade and
14+
// cannot be altered by the admin at runtime — removing an avenue for
15+
// admin-only extraction via strike-count manipulation.
16+
//
17+
// LEGAL NOTE: Product/legal must sign off on the strike threshold before
18+
// mainnet deployment. Three rejections is a conservative starting point.
19+
// The threshold intentionally errs toward coverage preservation; false
20+
// positives (legitimate holders de-activated) are harder to recover from
21+
// than false negatives (fraudulent holders retained until human review).
22+
//
23+
// APPEAL INTERACTION: If an appeal window is added later, auto-deactivation
24+
// should be deferred until the appeal deadline passes. Implement by adding a
25+
// `deactivation_pending_until_ledger: u32` field to Policy and skipping the
26+
// `is_active = false` write until that ledger is reached.
27+
pub const STRIKE_DEACTIVATION_THRESHOLD: u32 = 3;
28+
1029
// ── Ledger window constants (re-exported from ledger.rs for ABI visibility) ───
1130
//
1231
// These are the canonical values used by on-chain checks. The frontend and
@@ -124,6 +143,15 @@ pub enum VoteOption {
124143
}
125144

126145
/// Reason for policy termination.
146+
///
147+
/// GOVERNANCE NOTE: `ExcessiveRejections` is set by the claims engine
148+
/// automatically when `strike_count` reaches `STRIKE_DEACTIVATION_THRESHOLD`.
149+
/// All other variants require an explicit holder or admin action.
150+
///
151+
/// CENTRALIZATION RISK: `AdminOverride` allows the admin to terminate any
152+
/// policy for any reason at any time. This is a privileged operation that
153+
/// bypasses normal holder protections. Consider a time-lock or multi-sig
154+
/// requirement before using this variant in production.
127155
#[contracttype]
128156
#[derive(Clone, PartialEq, Eq, Debug)]
129157
pub enum TerminationReason {
@@ -134,9 +162,15 @@ pub enum TerminationReason {
134162
FraudOrMisrepresentation,
135163
RegulatoryAction,
136164
AdminOverride,
137-
/// Policy deactivated automatically by `on_reject` when `strike_count` reached
138-
/// `STRIKE_DEACTIVATION_THRESHOLD`. Not set by admin.
139-
/// **XDR append-safe:** appended at end of enum; existing serialised values unchanged.
165+
/// Automatically set when `Policy.strike_count` reaches
166+
/// `STRIKE_DEACTIVATION_THRESHOLD` consecutive rejections.
167+
/// No admin intervention is required or possible to prevent this;
168+
/// the transition is deterministic and trustless.
169+
///
170+
/// APPEAL NOTE: If an appeal window is introduced, deactivation should be
171+
/// deferred until the appeal window closes. The `PolicyDeactivated` event
172+
/// (emitted in `claim.rs`) is the authoritative signal for indexers; it
173+
/// will carry a `reason_code = 1` identifying this variant.
140174
ExcessiveRejections,
141175
}
142176

@@ -199,11 +233,21 @@ pub struct Policy {
199233
pub terminated_by_admin: bool,
200234
/// Running count of rejected claims against this policy.
201235
///
202-
/// Incremented by `claim::on_reject`. When `strike_count >= STRIKE_DEACTIVATION_THRESHOLD`
203-
/// the policy is automatically deactivated (`is_active = false`) and a `PolicyDeactivated`
204-
/// event is emitted. A successful appeal decrements this counter.
236+
/// Incremented by `claim::on_reject` every time a claim on this policy
237+
/// reaches `ClaimStatus::Rejected` (whether via majority vote or deadline
238+
/// finalization). Never decremented; exists purely for accumulation.
239+
///
240+
/// When `strike_count >= STRIKE_DEACTIVATION_THRESHOLD`, the policy is
241+
/// automatically deactivated (`is_active = false`) and the
242+
/// `PolicyDeactivated` event is emitted. No admin action is required.
243+
///
244+
/// RENEWAL GATE: Any future `renew_policy` implementation MUST check
245+
/// `strike_count` before allowing renewal. A policy with strikes at or
246+
/// near the threshold should be blocked or require admin review.
205247
///
206-
/// **Renewal gate:** any future `renew_policy` implementation MUST gate on this field.
248+
/// DATA VISIBILITY: This field is stored on-chain and permanently
249+
/// readable via `get_policy`. It carries only a count — no allegation
250+
/// narratives, no claimant-identifying data.
207251
pub strike_count: u32,
208252
}
209253

contracts/niffyinsure/test_snapshots/admin_may_bypass_open_claim_guard_when_explicitly_flagged.1.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@
236236
"u32": 1
237237
}
238238
},
239+
{
240+
"key": {
241+
"symbol": "strike_count"
242+
},
243+
"val": {
244+
"u32": 0
245+
}
246+
},
239247
{
240248
"key": {
241249
"symbol": "terminated_at_ledger"

contracts/niffyinsure/test_snapshots/cannot_terminate_policy_under_wrong_holder_address.1.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,14 @@
172172
"u32": 1
173173
}
174174
},
175+
{
176+
"key": {
177+
"symbol": "strike_count"
178+
},
179+
"val": {
180+
"u32": 0
181+
}
182+
},
175183
{
176184
"key": {
177185
"symbol": "terminated_at_ledger"

contracts/niffyinsure/test_snapshots/double_terminate_fails.1.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@
201201
"u32": 1
202202
}
203203
},
204+
{
205+
"key": {
206+
"symbol": "strike_count"
207+
},
208+
"val": {
209+
"u32": 0
210+
}
211+
},
204212
{
205213
"key": {
206214
"symbol": "terminated_at_ledger"

contracts/niffyinsure/test_snapshots/duplicate_vote_is_rejected.1.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,14 @@
467467
"u32": 0
468468
}
469469
},
470+
{
471+
"key": {
472+
"symbol": "strike_count"
473+
},
474+
"val": {
475+
"u32": 0
476+
}
477+
},
470478
{
471479
"key": {
472480
"symbol": "terminated_at_ledger"

0 commit comments

Comments
 (0)