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
25 changes: 21 additions & 4 deletions contracts/niffyinsure/src/claim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ pub fn file_claim(
approve_votes: 0,
reject_votes: 0,
filed_at: now,
appeal_open_deadline_ledger: 0,
appeals_count: 0,
appeal_deadline_ledger: 0,
appeal_approve_votes: 0,
appeal_reject_votes: 0,
};

storage::set_claim(env, &claim);
Expand Down Expand Up @@ -283,10 +288,16 @@ pub fn vote_on_claim(
// Auto-finalize on majority.
let total = snapshot.len();
let majority = total / 2 + 1;
let newly_rejected;
if claim.approve_votes >= majority {
claim.status = ClaimStatus::Approved;
newly_rejected = false;
} else if claim.reject_votes >= majority {
claim.status = ClaimStatus::Rejected;
claim.appeal_open_deadline_ledger = now.saturating_add(ledger::APPEAL_OPEN_WINDOW_LEDGERS);
newly_rejected = true;
} else {
newly_rejected = false;
}

let newly_rejected = claim.status == ClaimStatus::Rejected;
Expand Down Expand Up @@ -329,12 +340,16 @@ pub fn finalize_claim(env: &Env, claim_id: u64) -> Result<ClaimStatus, Error> {
return Err(Error::VotingWindowStillOpen);
}

claim.status = if claim.approve_votes > claim.reject_votes {
ClaimStatus::Approved
let newly_rejected;
if claim.approve_votes > claim.reject_votes {
claim.status = ClaimStatus::Approved;
newly_rejected = false;
} else {
// Tie or reject plurality → Rejected (insurer wins tie).
ClaimStatus::Rejected
};
claim.status = ClaimStatus::Rejected;
claim.appeal_open_deadline_ledger = now.saturating_add(ledger::APPEAL_OPEN_WINDOW_LEDGERS);
newly_rejected = true;
}

let newly_rejected = claim.status == ClaimStatus::Rejected;

Expand Down Expand Up @@ -505,6 +520,8 @@ fn payout(env: &Env, claim: &Claim) -> Result<(), Error> {
Ok(())
}

// ── Public read helpers ───────────────────────────────────────────────────────

pub fn get_claim(env: &Env, claim_id: u64) -> Result<Claim, Error> {
storage::get_claim(env, claim_id).ok_or(Error::ClaimNotFound)
}
Expand Down
12 changes: 12 additions & 0 deletions contracts/niffyinsure/src/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ pub const RATE_LIMIT_WINDOW_LEDGERS: u32 = LEDGERS_PER_DAY; // 17_280
/// Quote validity: how many ledgers a `generate_premium` result stays valid.
pub const QUOTE_TTL_LEDGERS: u32 = 100;

/// Appeal open window: how many ledgers after rejection a claimant may open an appeal.
/// ~3 days. Anchored at the ledger that produced the Rejected status.
pub const APPEAL_OPEN_WINDOW_LEDGERS: u32 = 3 * LEDGERS_PER_DAY; // 51_840

/// Appeal vote window: how many ledgers voters have to vote on an appeal.
/// ~7 days (same duration as the base claim vote window).
pub const APPEAL_VOTE_WINDOW_LEDGERS: u32 = 7 * LEDGERS_PER_DAY; // 120_960

/// Hard cap on appeals per claim. Prevents infinite ping-pong.
/// Claimants get exactly one appeal after a Rejected outcome.
pub const MAX_APPEALS_PER_CLAIM: u32 = 1;

// ── Core window helpers ───────────────────────────────────────────────────────

/// Returns `true` if `now` falls in the half-open interval `[start, end)`.
Expand Down
18 changes: 18 additions & 0 deletions contracts/niffyinsure/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub enum DataKey {
ClaimVoters(u64),
/// Last ledger at which `holder` filed a claim (rate-limit anchor).
LastClaimLedger(Address),
/// (claim_id, voter_address) -> VoteOption for appeal round; immutable after first write.
AppealVote(u64, Address),
}

// ── Instance bump ─────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -453,3 +455,19 @@ pub fn get_last_claim_ledger(env: &Env, holder: &Address) -> Option<u32> {
.persistent()
.get(&DataKey::LastClaimLedger(holder.clone()))
}

// ── Appeal vote (persistent) ──────────────────────────────────────────────────

pub fn set_appeal_vote(env: &Env, claim_id: u64, voter: &Address, vote: &VoteOption) {
let key = DataKey::AppealVote(claim_id, voter.clone());
env.storage().persistent().set(&key, vote);
env.storage()
.persistent()
.extend_ttl(&key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND_TO);
}

pub fn get_appeal_vote(env: &Env, claim_id: u64, voter: &Address) -> Option<VoteOption> {
env.storage()
.persistent()
.get(&DataKey::AppealVote(claim_id, voter.clone()))
}
60 changes: 54 additions & 6 deletions contracts/niffyinsure/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,28 @@ pub const STRIKE_DEACTIVATION_THRESHOLD: u32 = 3;
// Conversion: 1 ledger ≈ 5 s on Stellar Mainnet (Protocol 20+).
// See: https://developers.stellar.org/docs/learn/fundamentals/stellar-consensus-protocol
pub use crate::ledger::{
LEDGERS_PER_DAY, LEDGERS_PER_HOUR, LEDGERS_PER_MIN, LEDGERS_PER_WEEK, POLICY_DURATION_LEDGERS,
APPEAL_OPEN_WINDOW_LEDGERS, APPEAL_VOTE_WINDOW_LEDGERS, LEDGERS_PER_DAY, LEDGERS_PER_HOUR,
LEDGERS_PER_MIN, LEDGERS_PER_WEEK, MAX_APPEALS_PER_CLAIM, POLICY_DURATION_LEDGERS,
QUOTE_TTL_LEDGERS, RATE_LIMIT_WINDOW_LEDGERS, RENEWAL_WINDOW_LEDGERS, SECS_PER_LEDGER,
VOTE_WINDOW_LEDGERS,
};

// ── Strike / rejection constants ──────────────────────────────────────────────

/// Number of rejected claims that automatically deactivates a policy.
///
/// This is a **compile-time constant**, not a runtime admin parameter. Admin
/// cannot flip it post-deployment, which prevents governance gaming where a
/// large voter bloc rejects claims to deactivate rival policies.
///
/// **Legal review:** Before changing this value, consult legal counsel on
/// whether automatic policy cancellation triggers regulatory requirements
/// (e.g., notice periods, appeal rights).
///
/// **Appeal interaction:** Deactivation triggered by reaching this threshold
/// can be reversed by a successful appeal that decrements strikes back below it.
pub const STRIKE_DEACTIVATION_THRESHOLD: u32 = 3;

// ── Enums ─────────────────────────────────────────────────────────────────────

#[contracttype]
Expand Down Expand Up @@ -76,10 +93,19 @@ pub enum CoverageType {

/// Claim lifecycle state machine.
///
/// Transitions:
/// Processing → Approved (majority approve vote or deadline plurality)
/// Processing → Rejected (majority reject vote or deadline plurality/tie)
/// Approved → Paid (admin calls process_claim)
/// Base-flow transitions:
/// Processing → Approved (majority approve vote or deadline plurality)
/// Processing → Rejected (majority reject vote or deadline plurality/tie)
/// Approved → Paid (admin calls process_claim)
///
/// Appeal-flow transitions (requires Rejected status + open appeal window):
/// Rejected → UnderAppeal (claimant calls open_appeal within window)
/// UnderAppeal → AppealApproved (majority approve appeal vote or deadline)
/// UnderAppeal → AppealRejected (majority reject appeal vote or deadline)
/// AppealApproved → Paid (admin calls process_claim — same as Approved)
///
/// Terminal states (no further transitions): Paid, Rejected (after appeal window
/// closes), AppealApproved (→ Paid only), AppealRejected.
#[contracttype]
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ClaimStatus {
Expand All @@ -88,13 +114,23 @@ pub enum ClaimStatus {
Approved,
Paid,
Rejected,
/// Claimant has opened an appeal; fresh vote round in progress.
UnderAppeal,
/// Appeal vote resolved in claimant's favour; awaits admin payout.
AppealApproved,
/// Appeal vote rejected; claim is permanently closed.
AppealRejected,
}

impl ClaimStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
ClaimStatus::Approved | ClaimStatus::Paid | ClaimStatus::Rejected
ClaimStatus::Approved
| ClaimStatus::Paid
| ClaimStatus::Rejected
| ClaimStatus::AppealApproved
| ClaimStatus::AppealRejected
)
}
}
Expand Down Expand Up @@ -234,6 +270,18 @@ pub struct Claim {
pub reject_votes: u32,
/// Ledger sequence at which this claim was filed (voting window anchor).
pub filed_at: u32,
// ── Appeal fields ────────────────────────────────────────────────────────
/// Ledger by which `open_appeal` must be called (0 if never rejected).
/// Set to `rejected_at + APPEAL_OPEN_WINDOW_LEDGERS` when status → Rejected.
pub appeal_open_deadline_ledger: u32,
/// How many appeals have been opened for this claim (cap = MAX_APPEALS_PER_CLAIM).
pub appeals_count: u32,
/// Voting deadline for the current appeal round (0 if no appeal open).
pub appeal_deadline_ledger: u32,
/// Approve votes cast in the current appeal round.
pub appeal_approve_votes: u32,
/// Reject votes cast in the current appeal round.
pub appeal_reject_votes: u32,
}

#[contracttype]
Expand Down
12 changes: 12 additions & 0 deletions contracts/niffyinsure/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ pub enum Error {
VotingWindowStillOpen = 40,
NotEligibleVoter = 41,
RateLimitExceeded = 42,
/// Appeal open window has passed; claimant can no longer appeal this claim.
AppealWindowClosed = 43,
/// An appeal is already open for this claim.
AppealAlreadyOpen = 44,
/// Claim has reached the maximum allowed appeals per claim.
MaxAppealsReached = 45,
/// Claim is not in Rejected status; cannot open an appeal.
ClaimNotRejected = 46,
/// No appeal is currently open; cannot vote on or finalize appeal.
AppealNotOpen = 47,
/// Appeal voting window is still open; cannot finalize appeal yet.
AppealWindowStillOpen = 48,
}

pub fn check_policy(policy: &Policy) -> Result<(), Error> {
Expand Down
Loading