From 060a83e6da7f0968a73a023a95b7878cdcb728dc Mon Sep 17 00:00:00 2001 From: ScriptedBro Date: Mon, 30 Mar 2026 08:26:11 +0100 Subject: [PATCH 1/3] feat(contracts): add signer nonce invalidation mechanism --- contracts/credence_delegation/src/lib.rs | 25 +++++ contracts/credence_delegation/src/nonce.rs | 31 +++++++ .../src/test_domain_separation.rs | 92 ++++++++++++++++++- 3 files changed, 146 insertions(+), 2 deletions(-) diff --git a/contracts/credence_delegation/src/lib.rs b/contracts/credence_delegation/src/lib.rs index bfc09d5..311faa5 100644 --- a/contracts/credence_delegation/src/lib.rs +++ b/contracts/credence_delegation/src/lib.rs @@ -65,6 +65,8 @@ enum DataKey { #[contract] pub struct CredenceDelegation; +const MAX_NONCE_INVALIDATION_SPAN: u64 = 10_000; + #[contractimpl] impl CredenceDelegation { /// Initialize the contract with an admin address. @@ -280,6 +282,29 @@ impl CredenceDelegation { nonce::get_nonce(&e, &identity) } + /// Invalidate a bounded range of nonces for compromised-key recovery. + /// + /// Advances nonce to `new_nonce`, invalidating all payloads signed with + /// nonces in `[current_nonce, new_nonce)`. + /// + /// Security properties: + /// - Only `identity` can invalidate its own nonce stream. + /// - Nonce remains strictly monotonic (`new_nonce` must be greater). + /// - Range size is capped to keep gas predictable. + pub fn invalidate_nonce_range(e: Env, identity: Address, new_nonce: u64) { + identity.require_auth(); + let (from_nonce, to_nonce) = nonce::invalidate_nonce_range( + &e, + &identity, + new_nonce, + MAX_NONCE_INVALIDATION_SPAN, + ); + e.events().publish( + (Symbol::new(&e, "nonce_invalidated"), identity), + (from_nonce, to_nonce), + ); + } + // ----------------------------------------------------------------------- // Pausable pass-throughs // ----------------------------------------------------------------------- diff --git a/contracts/credence_delegation/src/nonce.rs b/contracts/credence_delegation/src/nonce.rs index d7a6008..2e4e008 100644 --- a/contracts/credence_delegation/src/nonce.rs +++ b/contracts/credence_delegation/src/nonce.rs @@ -32,3 +32,34 @@ pub fn consume_nonce(e: &Env, identity: &Address, expected_nonce: u64) { .instance() .set(&DataKey::Nonce(identity.clone()), &next); } + +/// Advances nonce to `new_nonce`, invalidating the half-open range +/// `[current_nonce, new_nonce)`. +/// +/// This allows compromised-key recovery by skipping potentially leaked, +/// pre-signed delegated payloads without submitting each nonce one-by-one. +/// +/// # Panics +/// Panics if `new_nonce <= current_nonce` or if the span exceeds `max_span`. +pub fn invalidate_nonce_range( + e: &Env, + identity: &Address, + new_nonce: u64, + max_span: u64, +) -> (u64, u64) { + let current = get_nonce(e, identity); + if new_nonce <= current { + panic!("new nonce must be greater than current nonce"); + } + let span = new_nonce + .checked_sub(current) + .expect("nonce underflow during invalidation"); + if span > max_span { + panic!("nonce invalidation exceeds max batch size"); + } + + e.storage() + .instance() + .set(&DataKey::Nonce(identity.clone()), &new_nonce); + (current, new_nonce) +} diff --git a/contracts/credence_delegation/src/test_domain_separation.rs b/contracts/credence_delegation/src/test_domain_separation.rs index efab4c4..5a2bd8f 100644 --- a/contracts/credence_delegation/src/test_domain_separation.rs +++ b/contracts/credence_delegation/src/test_domain_separation.rs @@ -10,7 +10,7 @@ //! 5. Cross-method replay: a revoke payload cannot be reused as a delegate payload. use super::*; -use soroban_sdk::testutils::{Address as _, Ledger as _}; +use soroban_sdk::testutils::Address as _; use soroban_sdk::Env; // --------------------------------------------------------------------------- @@ -29,7 +29,7 @@ fn setup() -> (Env, CredenceDelegationClient<'static>, Address) { /// Build a valid `DelegatedActionPayload` for the given parameters. fn make_payload( - e: &Env, + _e: &Env, domain: DomainTag, owner: &Address, target: &Address, @@ -344,6 +344,94 @@ fn wrong_target_in_payload_rejected() { // Happy path: full delegated round-trip (delegate → revoke) // --------------------------------------------------------------------------- +#[test] +fn partial_nonce_invalidation_skips_range_and_allows_next_nonce() { + let (e, client, contract_id) = setup(); + let owner = Address::generate(&e); + let delegate = Address::generate(&e); + let expiry = e.ledger().timestamp() + 86_400; + + // Consume nonces 0 and 1 normally. + let p0 = make_payload(&e, DomainTag::Delegate, &owner, &delegate, &contract_id, 0); + client.execute_delegated_delegate( + &owner, + &delegate, + &DelegationType::Attestation, + &expiry, + &p0, + ); + let p1 = make_payload(&e, DomainTag::Delegate, &owner, &delegate, &contract_id, 1); + client.execute_delegated_delegate( + &owner, + &delegate, + &DelegationType::Management, + &expiry, + &p1, + ); + assert_eq!(client.get_nonce(&owner), 2); + + // Invalidate [2, 4): nonce 2 and 3 become unusable. + client.invalidate_nonce_range(&owner, &4); + assert_eq!(client.get_nonce(&owner), 4); + + // Fresh nonce 4 must still be usable. + let p4 = make_payload(&e, DomainTag::Delegate, &owner, &delegate, &contract_id, 4); + client.execute_delegated_delegate( + &owner, + &delegate, + &DelegationType::Attestation, + &expiry, + &p4, + ); + assert_eq!(client.get_nonce(&owner), 5); +} + +#[test] +#[should_panic(expected = "invalid nonce")] +fn full_nonce_invalidation_rejects_previously_valid_payload() { + let (e, client, contract_id) = setup(); + let owner = Address::generate(&e); + let delegate = Address::generate(&e); + let expiry = e.ledger().timestamp() + 86_400; + + // This payload is valid at nonce 0 before invalidation. + let stale_payload = make_payload(&e, DomainTag::Delegate, &owner, &delegate, &contract_id, 0); + + // Invalidate a full early range [0, 10). + client.invalidate_nonce_range(&owner, &10); + assert_eq!(client.get_nonce(&owner), 10); + + // Previously valid payload must now fail. + client.execute_delegated_delegate( + &owner, + &delegate, + &DelegationType::Attestation, + &expiry, + &stale_payload, + ); +} + +#[test] +#[should_panic(expected = "nonce invalidation exceeds max batch size")] +fn nonce_invalidation_range_bound_enforced() { + let (e, client, _) = setup(); + let owner = Address::generate(&e); + + // MAX_NONCE_INVALIDATION_SPAN is 10_000, so 10_001 must fail from nonce 0. + client.invalidate_nonce_range(&owner, &10_001); +} + +#[test] +#[should_panic(expected = "new nonce must be greater than current nonce")] +fn nonce_invalidation_must_be_monotonic() { + let (e, client, _) = setup(); + let owner = Address::generate(&e); + + client.invalidate_nonce_range(&owner, &1); + // Reusing the same target is non-monotonic and must fail. + client.invalidate_nonce_range(&owner, &1); +} + #[test] fn happy_path_delegated_delegate_then_revoke() { let (e, client, contract_id) = setup(); From a351ca0bc26ee8d347cffaca13a70cf4cee3a438 Mon Sep 17 00:00:00 2001 From: ScriptedBro Date: Mon, 30 Mar 2026 08:35:25 +0100 Subject: [PATCH 2/3] fix: resolve cargo fmt --check failures --- .../admin/src/test_ownership_transfer.rs | 46 ++++++------------- contracts/credence_bond/src/events.rs | 8 +++- contracts/credence_bond/src/lib.rs | 26 +++++------ contracts/credence_bond/src/nonce.rs | 2 +- contracts/credence_bond/src/slashing.rs | 4 +- .../credence_bond/src/test_grace_period.rs | 2 +- contracts/credence_delegation/src/lib.rs | 8 +--- .../src/test_domain_separation.rs | 8 +--- 8 files changed, 38 insertions(+), 66 deletions(-) diff --git a/contracts/admin/src/test_ownership_transfer.rs b/contracts/admin/src/test_ownership_transfer.rs index d56ea2d..2716a34 100644 --- a/contracts/admin/src/test_ownership_transfer.rs +++ b/contracts/admin/src/test_ownership_transfer.rs @@ -48,9 +48,7 @@ mod ownership_transfer_tests { let env = Env::default(); let (contract_address, super_admin) = setup_contract(&env); - let owner = env.as_contract(&contract_address, || { - AdminContract::get_owner(env.clone()) - }); + let owner = env.as_contract(&contract_address, || AdminContract::get_owner(env.clone())); assert_eq!(owner, super_admin); } @@ -70,8 +68,7 @@ mod ownership_transfer_tests { #[test] fn test_transfer_ownership_sets_pending_owner() { let env = Env::default(); - let (contract_address, super_admin_1, super_admin_2) = - setup_multiple_super_admins(&env); + let (contract_address, super_admin_1, super_admin_2) = setup_multiple_super_admins(&env); env.mock_all_auths(); env.as_contract(&contract_address, || { @@ -92,8 +89,7 @@ mod ownership_transfer_tests { #[test] fn test_ownership_remains_with_current_owner_before_accept() { let env = Env::default(); - let (contract_address, super_admin_1, super_admin_2) = - setup_multiple_super_admins(&env); + let (contract_address, super_admin_1, super_admin_2) = setup_multiple_super_admins(&env); env.mock_all_auths(); env.as_contract(&contract_address, || { @@ -104,9 +100,7 @@ mod ownership_transfer_tests { ); }); - let owner = env.as_contract(&contract_address, || { - AdminContract::get_owner(env.clone()) - }); + let owner = env.as_contract(&contract_address, || AdminContract::get_owner(env.clone())); // Owner should still be super_admin_1 until accept_ownership is called assert_eq!(owner, super_admin_1); @@ -115,8 +109,7 @@ mod ownership_transfer_tests { #[test] fn test_accept_ownership_transfers_control() { let env = Env::default(); - let (contract_address, super_admin_1, super_admin_2) = - setup_multiple_super_admins(&env); + let (contract_address, super_admin_1, super_admin_2) = setup_multiple_super_admins(&env); env.mock_all_auths(); env.as_contract(&contract_address, || { @@ -128,9 +121,7 @@ mod ownership_transfer_tests { AdminContract::accept_ownership(env.clone(), super_admin_2.clone()); }); - let owner = env.as_contract(&contract_address, || { - AdminContract::get_owner(env.clone()) - }); + let owner = env.as_contract(&contract_address, || AdminContract::get_owner(env.clone())); assert_eq!(owner, super_admin_2); } @@ -138,8 +129,7 @@ mod ownership_transfer_tests { #[test] fn test_pending_owner_cleared_after_acceptance() { let env = Env::default(); - let (contract_address, super_admin_1, super_admin_2) = - setup_multiple_super_admins(&env); + let (contract_address, super_admin_1, super_admin_2) = setup_multiple_super_admins(&env); env.mock_all_auths(); env.as_contract(&contract_address, || { @@ -162,8 +152,7 @@ mod ownership_transfer_tests { #[should_panic(expected = "only current owner can transfer ownership")] fn test_transfer_ownership_rejects_non_owner() { let env = Env::default(); - let (contract_address, super_admin_1, super_admin_2) = - setup_multiple_super_admins(&env); + let (contract_address, super_admin_1, super_admin_2) = setup_multiple_super_admins(&env); let unauthorized_address = Address::generate(&env); @@ -243,11 +232,7 @@ mod ownership_transfer_tests { env.mock_all_auths(); env.as_contract(&contract_address, || { - AdminContract::transfer_ownership( - env.clone(), - super_admin.clone(), - non_admin.clone(), - ); + AdminContract::transfer_ownership(env.clone(), super_admin.clone(), non_admin.clone()); }); } @@ -281,8 +266,7 @@ mod ownership_transfer_tests { #[should_panic(expected = "new owner must be active")] fn test_transfer_ownership_rejects_inactive_admin() { let env = Env::default(); - let (contract_address, super_admin_1, super_admin_2) = - setup_multiple_super_admins(&env); + let (contract_address, super_admin_1, super_admin_2) = setup_multiple_super_admins(&env); env.mock_all_auths(); env.as_contract(&contract_address, || { @@ -363,9 +347,8 @@ mod ownership_transfer_tests { AdminContract::accept_ownership(env.clone(), super_admin_3.clone()); }); - let final_owner = env.as_contract(&contract_address, || { - AdminContract::get_owner(env.clone()) - }); + let final_owner = + env.as_contract(&contract_address, || AdminContract::get_owner(env.clone())); assert_eq!(final_owner, super_admin_3); } @@ -416,9 +399,8 @@ mod ownership_transfer_tests { AdminContract::accept_ownership(env.clone(), super_admin_3.clone()); }); - let final_owner = env.as_contract(&contract_address, || { - AdminContract::get_owner(env.clone()) - }); + let final_owner = + env.as_contract(&contract_address, || AdminContract::get_owner(env.clone())); assert_eq!(final_owner, super_admin_3); } diff --git a/contracts/credence_bond/src/events.rs b/contracts/credence_bond/src/events.rs index c6119a9..def1939 100644 --- a/contracts/credence_bond/src/events.rs +++ b/contracts/credence_bond/src/events.rs @@ -99,7 +99,11 @@ pub fn emit_claims_processed( _processed_claims: &soroban_sdk::Vec, ) { let topics = (Symbol::new(e, "claims_processed"), user.clone()); - let data = (result.processed_count, result.total_amount, result.claim_types.clone()); + let data = ( + result.processed_count, + result.total_amount, + result.claim_types.clone(), + ); e.events().publish(topics, data); } @@ -116,4 +120,4 @@ pub fn emit_claims_expired(e: &Env, user: &Address, expired_count: u32, expired_ let topics = (Symbol::new(e, "claims_expired"), user.clone()); let data = (expired_count, expired_amount); e.events().publish(topics, data); -} \ No newline at end of file +} diff --git a/contracts/credence_bond/src/lib.rs b/contracts/credence_bond/src/lib.rs index 7354fea..b87df6c 100644 --- a/contracts/credence_bond/src/lib.rs +++ b/contracts/credence_bond/src/lib.rs @@ -531,7 +531,7 @@ impl CredenceBond { let base_reward = 1000i128; // Base reward for attestation let weight_bonus = (weight as i128) * 100; // Bonus based on weight let total_reward = base_reward + weight_bonus; - + claims::add_pending_claim( &e, &attester, @@ -728,23 +728,23 @@ impl CredenceBond { penalty_bps, ); early_exit_penalty::emit_penalty_event(&e, &bond.identity, amount, penalty, &treasury); - + // Calculate net amount and transfer to user let net_amount = amount.checked_sub(penalty).expect("penalty exceeds amount"); token_integration::transfer_from_contract(&e, &bond.identity, net_amount); - - // Instead of transferring penalty to treasury immediately, + + // Instead of transferring penalty to treasury immediately, // add a potential penalty refund claim for good behavior if penalty > 0 { // Transfer penalty to treasury (still push-based for treasury) token_integration::transfer_from_contract(&e, &treasury, penalty); - + // Add a potential penalty refund claim (50% of penalty can be refunded for good behavior) let refund_amount = penalty / 2; if refund_amount > 0 { // Get next penalty ID for tracking let penalty_id = get_next_penalty_id(&e); - + claims::add_pending_claim( &e, &bond.identity, @@ -755,7 +755,7 @@ impl CredenceBond { ); } } - + let old_tier = tiered_bond::get_tier_for_amount(bond.bonded_amount); bond.bonded_amount = bond.bonded_amount.checked_sub(amount).expect("underflow"); if bond.slashed_amount > bond.bonded_amount { @@ -1400,11 +1400,7 @@ impl CredenceBond { } /// Process a limited number of claims for the caller - pub fn claim_rewards_batch( - e: Env, - user: Address, - max_claims: u32, - ) -> claims::ClaimResult { + pub fn claim_rewards_batch(e: Env, user: Address, max_claims: u32) -> claims::ClaimResult { claims::process_claims(&e, &user, soroban_sdk::Vec::new(&e), max_claims) } @@ -1507,6 +1503,8 @@ mod test_fees; #[cfg(test)] mod test_governance_approval; #[cfg(test)] +mod test_grace_period; +#[cfg(test)] mod test_helpers; #[cfg(test)] mod test_increase_bond; @@ -1537,6 +1535,4 @@ mod test_weighted_attestation; #[cfg(test)] mod test_withdraw_bond; #[cfg(test)] -mod test_grace_window; // new test module from your commit -#[cfg(test)] -mod token_integration_test; \ No newline at end of file +mod token_integration_test; diff --git a/contracts/credence_bond/src/nonce.rs b/contracts/credence_bond/src/nonce.rs index 14f3606..6827ebe 100644 --- a/contracts/credence_bond/src/nonce.rs +++ b/contracts/credence_bond/src/nonce.rs @@ -102,4 +102,4 @@ pub fn validate_and_consume( require_not_expired(e, deadline); require_domain_match(e, expected_contract); consume_nonce(e, identity, nonce); -} \ No newline at end of file +} diff --git a/contracts/credence_bond/src/slashing.rs b/contracts/credence_bond/src/slashing.rs index d1cb430..0cff990 100644 --- a/contracts/credence_bond/src/slashing.rs +++ b/contracts/credence_bond/src/slashing.rs @@ -120,7 +120,7 @@ pub fn slash_bond(e: &Env, admin: &Address, amount: i128) -> crate::IdentityBond } else { amount }; - + bond.slashed_amount = if new_slashed > bond.bonded_amount { bond.bonded_amount } else { @@ -133,7 +133,7 @@ pub fn slash_bond(e: &Env, admin: &Address, amount: i128) -> crate::IdentityBond if reward_amount > 0 { // Get next source ID for tracking let source_id = get_next_slash_id(e); - + crate::claims::add_pending_claim( e, admin, diff --git a/contracts/credence_bond/src/test_grace_period.rs b/contracts/credence_bond/src/test_grace_period.rs index 0ed5277..ed6cc4d 100644 --- a/contracts/credence_bond/src/test_grace_period.rs +++ b/contracts/credence_bond/src/test_grace_period.rs @@ -230,4 +230,4 @@ fn non_admin_cannot_set_grace_window() { let (client, _admin, attester, _contract_id) = setup(&e); // attester is not admin — must panic client.set_grace_window(&attester, &30u64); -} \ No newline at end of file +} diff --git a/contracts/credence_delegation/src/lib.rs b/contracts/credence_delegation/src/lib.rs index 311faa5..34d0a72 100644 --- a/contracts/credence_delegation/src/lib.rs +++ b/contracts/credence_delegation/src/lib.rs @@ -293,12 +293,8 @@ impl CredenceDelegation { /// - Range size is capped to keep gas predictable. pub fn invalidate_nonce_range(e: Env, identity: Address, new_nonce: u64) { identity.require_auth(); - let (from_nonce, to_nonce) = nonce::invalidate_nonce_range( - &e, - &identity, - new_nonce, - MAX_NONCE_INVALIDATION_SPAN, - ); + let (from_nonce, to_nonce) = + nonce::invalidate_nonce_range(&e, &identity, new_nonce, MAX_NONCE_INVALIDATION_SPAN); e.events().publish( (Symbol::new(&e, "nonce_invalidated"), identity), (from_nonce, to_nonce), diff --git a/contracts/credence_delegation/src/test_domain_separation.rs b/contracts/credence_delegation/src/test_domain_separation.rs index 5a2bd8f..95b11b3 100644 --- a/contracts/credence_delegation/src/test_domain_separation.rs +++ b/contracts/credence_delegation/src/test_domain_separation.rs @@ -361,13 +361,7 @@ fn partial_nonce_invalidation_skips_range_and_allows_next_nonce() { &p0, ); let p1 = make_payload(&e, DomainTag::Delegate, &owner, &delegate, &contract_id, 1); - client.execute_delegated_delegate( - &owner, - &delegate, - &DelegationType::Management, - &expiry, - &p1, - ); + client.execute_delegated_delegate(&owner, &delegate, &DelegationType::Management, &expiry, &p1); assert_eq!(client.get_nonce(&owner), 2); // Invalidate [2, 4): nonce 2 and 3 become unusable. From 260ab8c59d0bfb4cc962f5bd729c14cb9f8129c6 Mon Sep 17 00:00:00 2001 From: ScriptedBro Date: Mon, 30 Mar 2026 09:01:22 +0100 Subject: [PATCH 3/3] fix: resolve CI compile errors in bond crates --- contracts/credence_bond/src/claims.rs | 65 +++++++++++++----------- contracts/credence_bond/src/lib.rs | 17 ++++--- contracts/credence_bond/src/nonce.rs | 15 ++++++ contracts/fixed_duration_bond/src/lib.rs | 6 +-- 4 files changed, 61 insertions(+), 42 deletions(-) diff --git a/contracts/credence_bond/src/claims.rs b/contracts/credence_bond/src/claims.rs index cb083de..f0cfaaa 100644 --- a/contracts/credence_bond/src/claims.rs +++ b/contracts/credence_bond/src/claims.rs @@ -10,8 +10,8 @@ //! - Comprehensive event emission //! - Gas-optimized claim operations -use soroban_sdk::{contracttype, Address, Env, Map, Symbol, Vec}; use crate::{events, DataKey}; +use soroban_sdk::{contracttype, Address, Env, Map, Symbol, Vec}; /// Maximum number of claims that can be processed in a single batch const MAX_BATCH_CLAIMS: u32 = 50; @@ -68,17 +68,17 @@ pub struct ClaimResult { /// Storage keys for claims impl DataKey { /// Pending claims for a user: DataKey::PendingClaims(user) -> Vec - pub const fn pending_claims(user: &Address) -> DataKey { + pub fn pending_claims(user: &Address) -> DataKey { DataKey::PendingClaims(user.clone()) } - + /// Total claimable amount for a user: DataKey::ClaimableAmount(user) -> i128 - pub const fn claimable_amount(user: &Address) -> DataKey { + pub fn claimable_amount(user: &Address) -> DataKey { DataKey::ClaimableAmount(user.clone()) } - + /// Claim history counter: DataKey::ClaimCounter -> u64 - pub const fn claim_counter() -> DataKey { + pub fn claim_counter() -> DataKey { DataKey::ClaimCounter } } @@ -109,7 +109,7 @@ pub fn add_pending_claim( let now = e.ledger().timestamp(); let expires_at = now + DEFAULT_CLAIM_EXPIRY; - + let claim = PendingClaim { claim_type, amount, @@ -125,25 +125,25 @@ pub fn add_pending_claim( .persistent() .get(&DataKey::PendingClaims(user.clone())) .unwrap_or(Vec::new(e)); - + claims.push_back(claim.clone()); - + // Update storage e.storage() .persistent() .set(&DataKey::PendingClaims(user.clone()), &claims); - + // Update total claimable amount let current_total: i128 = e .storage() .persistent() .get(&DataKey::ClaimableAmount(user.clone())) .unwrap_or(0); - + let new_total = current_total .checked_add(amount) .expect("claimable amount overflow"); - + e.storage() .persistent() .set(&DataKey::ClaimableAmount(user.clone()), &new_total); @@ -206,7 +206,7 @@ pub fn process_claims( let now = e.ledger().timestamp(); let mut claims = get_pending_claims(e, user); - + if claims.is_empty() { panic!("no pending claims"); } @@ -227,7 +227,11 @@ pub fn process_claims( let mut remaining_claims = Vec::new(e); let mut total_amount = 0i128; let mut processed_types = Vec::new(e); - let limit = if max_claims == 0 { MAX_BATCH_CLAIMS } else { max_claims.min(MAX_BATCH_CLAIMS) }; + let limit = if max_claims == 0 { + MAX_BATCH_CLAIMS + } else { + max_claims.min(MAX_BATCH_CLAIMS) + }; // Process claims for i in 0..claims.len() { @@ -240,24 +244,24 @@ pub fn process_claims( } let claim = claims.get(i).unwrap(); - + // Skip expired claims if claim.expires_at > 0 && now > claim.expires_at { continue; } - + // Skip if not in filter if filter_types && !type_set.contains_key(claim.claim_type) { remaining_claims.push_back(claim); continue; } - + // Process this claim processed_claims.push_back(claim.clone()); total_amount = total_amount .checked_add(claim.amount) .expect("claim total overflow"); - + // Track unique claim types let mut type_exists = false; for j in 0..processed_types.len() { @@ -287,11 +291,11 @@ pub fn process_claims( e.storage() .persistent() .set(&DataKey::PendingClaims(user.clone()), &remaining_claims); - + let remaining_amount = get_claimable_amount(e, user) .checked_sub(total_amount) .expect("claimable amount underflow"); - + e.storage() .persistent() .set(&DataKey::ClaimableAmount(user.clone()), &remaining_amount); @@ -302,12 +306,11 @@ pub fn process_claims( let token: Address = e .storage() .instance() - .get(&DataKey::Token) + .get(&DataKey::BondToken) .expect("token not configured"); - + let contract = e.current_contract_address(); - soroban_sdk::token::TokenClient::new(e, &token) - .transfer(&contract, user, &total_amount); + soroban_sdk::token::TokenClient::new(e, &token).transfer(&contract, user, &total_amount); } let result = ClaimResult { @@ -333,7 +336,7 @@ pub fn process_claims( pub fn cleanup_expired_claims(e: &Env, user: &Address) -> u32 { let now = e.ledger().timestamp(); let claims = get_pending_claims(e, user); - + if claims.is_empty() { return 0; } @@ -344,7 +347,7 @@ pub fn cleanup_expired_claims(e: &Env, user: &Address) -> u32 { for i in 0..claims.len() { let claim = claims.get(i).unwrap(); - + if claim.expires_at > 0 && now > claim.expires_at { expired_amount = expired_amount .checked_add(claim.amount) @@ -368,11 +371,11 @@ pub fn cleanup_expired_claims(e: &Env, user: &Address) -> u32 { e.storage() .persistent() .set(&DataKey::PendingClaims(user.clone()), &valid_claims); - + let remaining_amount = get_claimable_amount(e, user) .checked_sub(expired_amount) .expect("claimable amount underflow"); - + e.storage() .persistent() .set(&DataKey::ClaimableAmount(user.clone()), &remaining_amount); @@ -396,12 +399,12 @@ pub fn cleanup_expired_claims(e: &Env, user: &Address) -> u32 { pub fn get_claims_summary(e: &Env, user: &Address) -> Map { let claims = get_pending_claims(e, user); let mut summary = Map::new(e); - + for i in 0..claims.len() { let claim = claims.get(i).unwrap(); let current = summary.get(claim.claim_type).unwrap_or(0); summary.set(claim.claim_type, current + claim.amount); } - + summary -} \ No newline at end of file +} diff --git a/contracts/credence_bond/src/lib.rs b/contracts/credence_bond/src/lib.rs index b87df6c..7bbe864 100644 --- a/contracts/credence_bond/src/lib.rs +++ b/contracts/credence_bond/src/lib.rs @@ -7,6 +7,8 @@ use soroban_sdk::{ pub mod access_control; mod batch; +mod claims; +mod cooldown; pub mod early_exit_penalty; mod emergency; mod events; @@ -36,8 +38,6 @@ use crate::access_control::{ add_verifier_role, is_verifier, remove_verifier_role, require_verifier, }; -use soroban_sdk::token::TokenClient; - pub use batch::{BatchBondParams, BatchBondResult}; pub use evidence::{Evidence, EvidenceType}; @@ -64,17 +64,15 @@ pub struct IdentityBond { pub notice_period_duration: u64, } -// Re-export batch types -pub use batch::{BatchBondParams, BatchBondResult}; - #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Attestation { pub id: u64, - pub attester: Address, - pub subject: Address, + pub verifier: Address, + pub identity: Address, pub attestation_data: String, pub timestamp: u64, + pub weight: u32, pub revoked: bool, } @@ -124,6 +122,9 @@ pub enum DataKey { PauseProposal(u64), PauseApproval(u64, Address), PauseApprovalCount(u64), + PendingClaims(Address), + ClaimableAmount(Address), + ClaimCounter, BondToken, GraceWindow, // FIX 1: added for configurable post-expiry grace window } @@ -743,7 +744,7 @@ impl CredenceBond { let refund_amount = penalty / 2; if refund_amount > 0 { // Get next penalty ID for tracking - let penalty_id = get_next_penalty_id(&e); + let penalty_id = Self::get_next_penalty_id(&e); claims::add_pending_claim( &e, diff --git a/contracts/credence_bond/src/nonce.rs b/contracts/credence_bond/src/nonce.rs index 6827ebe..2a3afab 100644 --- a/contracts/credence_bond/src/nonce.rs +++ b/contracts/credence_bond/src/nonce.rs @@ -103,3 +103,18 @@ pub fn validate_and_consume( require_domain_match(e, expected_contract); consume_nonce(e, identity, nonce); } + +/// Backward-compatible wrapper for call sites that pass an explicit grace value. +/// +/// Grace is read from storage by `require_not_expired`, so the `_grace` argument +/// is intentionally ignored. +pub fn validate_and_consume_with_grace( + e: &Env, + identity: &Address, + expected_contract: &Address, + deadline: u64, + nonce: u64, + _grace: u64, +) { + validate_and_consume(e, identity, expected_contract, deadline, nonce); +} diff --git a/contracts/fixed_duration_bond/src/lib.rs b/contracts/fixed_duration_bond/src/lib.rs index 0d40c1a..60ec4b1 100644 --- a/contracts/fixed_duration_bond/src/lib.rs +++ b/contracts/fixed_duration_bond/src/lib.rs @@ -17,7 +17,7 @@ mod errors; mod types; -use credence_math::{add_i128, split_bps}; +use credence_math::{add_i128, mul_i128, split_bps}; use errors::*; use types::{DataKey, FeeConfig, FixedBond, OracleSafety}; @@ -203,7 +203,7 @@ impl FixedDurationBond { .get(&DataKey::OracleSafety(asset)) .unwrap_or_else(|| panic!("{}", ERR_ORACLE_SAFETY_NOT_SET)); validate_oracle_answer(oracle_answer, &safety); - checked_mul_i128(amount, oracle_answer, ERR_VALUATION_OVERFLOW) + mul_i128(amount, oracle_answer, ERR_VALUATION_OVERFLOW) } // ── Bond lifecycle ───────────────────────────────────────────────────── @@ -263,7 +263,7 @@ impl FixedDurationBond { .instance() .get(&DataKey::AccruedFees) .unwrap_or(0); - let new_fees = checked_add_i128(prev_fees, fee, ERR_FEE_ACCRUE_OVERFLOW); + let new_fees = add_i128(prev_fees, fee, ERR_FEE_ACCRUE_OVERFLOW); e.storage().instance().set(&DataKey::AccruedFees, &new_fees); net } else {