Skip to content

Commit e09b00e

Browse files
authored
Merge pull request InsurNiffy#1063 from abdegenius/feature/843-policy-terms-hash
feat: to close InsurNiffy#843 policy term hash
2 parents 2b073e9 + d12ebe6 commit e09b00e

8 files changed

Lines changed: 504 additions & 15 deletions

File tree

contracts/niffyinsure/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,7 @@ impl NiffyInsure {
10981098
opts.expected_nonce,
10991099
opts.metadata_uri,
11001100
opts.region_code,
1101+
opts.terms_hash,
11011102
)
11021103
}
11031104

@@ -1427,7 +1428,7 @@ impl NiffyInsure {
14271428
}
14281429

14291430
/// Admin-only: set the governance cooldown window in ledgers after parameter changes.
1430-
pub fn admin_set_governance_cooldown_ledgers(env: Env, new_ledgers: u32) -> Result<(), AdminError> {
1431+
pub fn admin_set_gov_cooldown_ledgers(env: Env, new_ledgers: u32) -> Result<(), AdminError> {
14311432
admin::set_governance_cooldown_ledgers(&env, new_ledgers)
14321433
}
14331434

@@ -2130,6 +2131,9 @@ impl NiffyInsure {
21302131
) {
21312132
use crate::types::{Policy, PolicyType, RegionTier, TerminationReason};
21322133
let token = storage::get_token(&env);
2134+
// Non-zero terms_hash sentinel for test policies.
2135+
let mut hash_bytes = [0u8; 32];
2136+
hash_bytes[0] = 1;
21332137
let policy = Policy {
21342138
holder: holder.clone(),
21352139
policy_id,
@@ -2148,6 +2152,7 @@ impl NiffyInsure {
21482152
terminated_by_admin: false,
21492153
strike_count: 0,
21502154
metadata_uri: String::from_str(&env, "ipfs://test-policy-metadata"),
2155+
terms_hash: soroban_sdk::BytesN::from_array(&env, &hash_bytes),
21512156
};
21522157
let key = storage::DataKey::Policy(holder.clone(), policy_id);
21532158
env.storage().persistent().set(&key, &policy);

contracts/niffyinsure/src/policy.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
validate::{self, Error},
88
};
99
pub use ledger::QUOTE_TTL_LEDGERS;
10-
use soroban_sdk::{contracterror, contractevent, contracttype, Address, Env, String};
10+
use soroban_sdk::{contracterror, contractevent, contracttype, Address, BytesN, Env, String};
1111

1212
/// Current event schema version.
1313
pub const POLICY_EVENT_VERSION: u32 = 1;
@@ -66,6 +66,8 @@ pub enum PolicyError {
6666
InvalidDeductible = 123,
6767
/// Treasury balance is insufficient to cover projected claim obligations.
6868
InsufficientSolvency = 124,
69+
/// Terms hash is all-zero (uninitialized). A non-zero SHA-256 digest is required at bind time.
70+
InvalidTermsHash = 125,
6971
}
7072

7173
#[contracttype]
@@ -92,6 +94,9 @@ pub struct PolicyInitiated {
9294
pub deductible: Option<i128>,
9395
pub start_ledger: u32,
9496
pub end_ledger: u32,
97+
/// SHA-256 hash of the insurance terms document bound at policy initiation.
98+
/// Non-zero; uniquely identifies the exact terms version in effect.
99+
pub terms_hash: BytesN<32>,
95100
}
96101

97102
/// Emitted when a protocol fee is collected from a premium payment.
@@ -388,6 +393,7 @@ pub fn initiate_policy(
388393
expected_nonce: Option<u64>,
389394
metadata_uri: String,
390395
region_code: Option<String>,
396+
terms_hash: BytesN<32>,
391397
) -> Result<Policy, PolicyError> {
392398
// Check granular pause: policy binding should be blocked if bind_paused
393399
storage::assert_bind_not_paused(env);
@@ -446,6 +452,10 @@ pub fn initiate_policy(
446452
if base_amount <= 0 {
447453
return Err(PolicyError::InvalidCoverage);
448454
}
455+
// Terms hash must be non-zero: all-zero digest is rejected as uninitialized.
456+
if terms_hash == BytesN::from_array(env, &[0u8; 32]) {
457+
return Err(PolicyError::InvalidTermsHash);
458+
}
449459
if !check_solvency_ratio(env, &asset, base_amount) {
450460
return Err(PolicyError::InsufficientSolvency);
451461
}
@@ -541,6 +551,7 @@ pub fn initiate_policy(
541551
terminated_by_admin: false,
542552
strike_count: 0,
543553
metadata_uri,
554+
terms_hash: terms_hash.clone(),
544555
};
545556

546557
validate::check_policy(&policy).map_err(|_| PolicyError::PolicyValidation)?;
@@ -570,6 +581,7 @@ pub fn initiate_policy(
570581
deductible: deductible_stored,
571582
start_ledger: current_ledger,
572583
end_ledger,
584+
terms_hash,
573585
}
574586
.publish(env);
575587

contracts/niffyinsure/src/policy_lifecycle.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
types::{ClaimStatus, Policy, PolicyType, RegionTier, TerminationReason},
66
validate,
77
};
8-
use soroban_sdk::{contracterror, contractevent, Address, Env, String};
8+
use soroban_sdk::{contracterror, contractevent, Address, BytesN, Env, String};
99

1010
#[contracterror]
1111
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
@@ -76,6 +76,12 @@ pub fn initiate_policy(
7676
terminated_by_admin: false,
7777
strike_count: 0,
7878
metadata_uri: String::from_str(env, ""),
79+
// policy_lifecycle bindings are legacy/internal; use a non-zero sentinel.
80+
terms_hash: BytesN::from_array(env, &{
81+
let mut b = [0u8; 32];
82+
b[0] = 1;
83+
b
84+
}),
7985
};
8086

8187
validate::check_policy(&policy).map_err(|e| match e {

contracts/niffyinsure/src/types.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,16 +437,22 @@ pub struct InitiatePolicyOptions {
437437
pub metadata_uri: String,
438438
/// Optional region code validated against the admin-managed region registry.
439439
pub region_code: Option<String>,
440+
/// SHA-256 hash of the insurance terms document in effect at bind time.
441+
/// Must be non-zero (all-zero digest is rejected as uninitialized).
442+
pub terms_hash: BytesN<32>,
440443
}
441444

442445
impl InitiatePolicyOptions {
443446
pub fn test_defaults(env: &Env) -> Self {
447+
let mut hash_bytes = [0u8; 32];
448+
hash_bytes[0] = 1; // non-zero sentinel for tests
444449
Self {
445450
beneficiary: None,
446451
deductible: None,
447452
expected_nonce: None,
448453
metadata_uri: String::from_str(env, "ipfs://test-policy"),
449454
region_code: None,
455+
terms_hash: BytesN::from_array(env, &hash_bytes),
450456
}
451457
}
452458
}
@@ -652,6 +658,15 @@ pub struct Policy {
652658
/// Off-chain URI to the policy governing document.
653659
/// Must be non-empty at policy creation. Admin can update via `update_policy_metadata_uri`.
654660
pub metadata_uri: String,
661+
/// SHA-256 hash of the insurance terms document in effect at bind time.
662+
///
663+
/// Commits the policy to an exact version of the coverage conditions and exclusions.
664+
/// Stored at `initiate_policy` time and immutable thereafter — no entrypoint modifies it.
665+
/// Must be non-zero (all-zero hash is rejected as an uninitialized value sentinel).
666+
///
667+
/// Off-chain verification: download the terms document at `metadata_uri` and compare its
668+
/// SHA-256 digest against this field to confirm the on-chain commitment matches.
669+
pub terms_hash: BytesN<32>,
655670
}
656671

657672
/// Return value of [`crate::policy::renew_policy`].

contracts/niffyinsure/tests/governance_cooldown.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fn set_governance_cooldown_succeeds_within_bounds() {
4040
let (_env, client, _admin) = setup();
4141
// 100 ledgers is well within the 30-day maximum.
4242
assert!(client
43-
.try_admin_set_governance_cooldown_ledgers(&100u32)
43+
.try_admin_set_gov_cooldown_ledgers(&100u32)
4444
.is_ok());
4545
assert_eq!(client.get_governance_cooldown_ledgers(), 100);
4646
}
@@ -51,15 +51,15 @@ fn set_governance_cooldown_out_of_bounds_fails() {
5151
// 30 days is niffyinsure::admin::MAX_GOVERNANCE_COOLDOWN_LEDGERS.
5252
// Exceed it: 30 * 17_280 + 1
5353
let too_large = 30u32 * 17_280u32 + 1;
54-
let result = client.try_admin_set_governance_cooldown_ledgers(&too_large);
54+
let result = client.try_admin_set_gov_cooldown_ledgers(&too_large);
5555
assert!(result.is_err());
5656
}
5757

5858
#[test]
5959
fn config_change_within_cooldown_reverts() {
6060
let (env, client, _admin) = setup();
6161
// Set a 1_000-ledger cooldown, then immediately try to change quorum.
62-
client.admin_set_governance_cooldown_ledgers(&1_000u32);
62+
client.admin_set_gov_cooldown_ledgers(&1_000u32);
6363

6464
// Immediately try another change – must be blocked.
6565
let result = client.try_admin_set_quorum_bps(&500u32);
@@ -73,7 +73,7 @@ fn config_change_within_cooldown_reverts() {
7373
fn config_change_after_cooldown_succeeds() {
7474
let (env, client, _admin) = setup();
7575
let cooldown = 500u32;
76-
client.admin_set_governance_cooldown_ledgers(&cooldown);
76+
client.admin_set_gov_cooldown_ledgers(&cooldown);
7777

7878
// Advance past the cooldown.
7979
env.ledger().with_mut(|l| {
@@ -88,7 +88,7 @@ fn config_change_after_cooldown_succeeds() {
8888
fn cooldown_enforced_on_vote_duration_change() {
8989
let (env, client, _admin) = setup();
9090
let cooldown = 200u32;
91-
client.admin_set_governance_cooldown_ledgers(&cooldown);
91+
client.admin_set_gov_cooldown_ledgers(&cooldown);
9292

9393
// Within cooldown: should fail.
9494
assert!(
@@ -112,7 +112,7 @@ fn cooldown_enforced_on_vote_duration_change() {
112112
fn cooldown_enforced_on_rolling_claim_cap_change() {
113113
let (env, client, _admin) = setup();
114114
let cooldown = 300u32;
115-
client.admin_set_governance_cooldown_ledgers(&cooldown);
115+
client.admin_set_gov_cooldown_ledgers(&cooldown);
116116

117117
// Within cooldown: should fail.
118118
assert!(client.try_set_rolling_claim_cap(&500_000i128).is_err());
@@ -128,7 +128,7 @@ fn cooldown_enforced_on_rolling_claim_cap_change() {
128128
fn cooldown_enforced_on_grace_period_change() {
129129
let (env, client, _admin) = setup();
130130
let cooldown = 400u32;
131-
client.admin_set_governance_cooldown_ledgers(&cooldown);
131+
client.admin_set_gov_cooldown_ledgers(&cooldown);
132132

133133
// Within cooldown: should fail.
134134
let grace = types::MIN_GRACE_PERIOD_LEDGERS;
@@ -145,15 +145,15 @@ fn cooldown_enforced_on_grace_period_change() {
145145
fn cooldown_set_to_zero_disables_enforcement() {
146146
let (env, client, _admin) = setup();
147147
// First enable, then immediately disable.
148-
client.admin_set_governance_cooldown_ledgers(&500u32);
148+
client.admin_set_gov_cooldown_ledgers(&500u32);
149149

150150
// Advance past cooldown to allow next change.
151151
env.ledger().with_mut(|l| {
152152
l.sequence_number = l.sequence_number.saturating_add(501);
153153
});
154154

155155
// Disable cooldown.
156-
client.admin_set_governance_cooldown_ledgers(&0u32);
156+
client.admin_set_gov_cooldown_ledgers(&0u32);
157157

158158
// Now rapid changes must succeed.
159159
assert!(client.try_admin_set_quorum_bps(&1_000u32).is_ok());

0 commit comments

Comments
 (0)