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
128 changes: 73 additions & 55 deletions docs/family-wallet-design.md

Large diffs are not rendered by default.

57 changes: 51 additions & 6 deletions family_wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,22 @@ impl FamilyWallet {
amount <= member.spending_limit
}

pub fn validate_precision_spending(
env: Env,
caller: Address,
amount: i128,
) -> Result<(), Error> {
if amount <= 0 {
return Err(Error::InvalidAmount);
}

if !Self::check_spending_limit(env.clone(), caller.clone(), amount) {
return Err(Error::Unauthorized);
}

Ok(())
}

/// @notice Configure multisig parameters for a given transaction type.
/// @dev Validates threshold bounds, signer membership, and uniqueness.
/// Returns `Result<bool, Error>` instead of panicking on invalid input.
Expand Down Expand Up @@ -956,12 +972,22 @@ impl FamilyWallet {
panic!("Maximum pending emergency proposals reached");
}

Self::propose_transaction(
env,
proposer,
let tx_id = Self::propose_transaction(
env.clone(),
proposer.clone(),
TransactionType::EmergencyTransfer,
TransactionData::EmergencyTransfer(token, recipient, amount),
)
TransactionData::EmergencyTransfer(token.clone(), recipient.clone(), amount),
);

Self::append_access_audit(
&env,
symbol_short!("em_prop"),
&proposer,
Some(recipient.clone()),
true,
);

tx_id
}

pub fn propose_policy_cancellation(env: Env, proposer: Address, policy_id: u32) -> u64 {
Expand All @@ -973,6 +999,10 @@ impl FamilyWallet {
)
}

/// Configure emergency transfer guardrails.
///
/// Only `Owner` or `Admin` may update emergency settings.
/// Successful configuration is recorded in the access audit trail.
pub fn configure_emergency(
env: Env,
caller: Address,
Expand Down Expand Up @@ -1006,9 +1036,14 @@ impl FamilyWallet {
},
);

Self::append_access_audit(&env, symbol_short!("em_conf"), &caller, None, true);

true
}

/// Enable or disable emergency mode.
///
/// This operation is restricted to `Owner` or `Admin` and is recorded in the access audit trail.
pub fn set_emergency_mode(env: Env, caller: Address, enabled: bool) -> bool {
caller.require_auth();
Self::require_not_paused(&env);
Expand Down Expand Up @@ -1036,6 +1071,8 @@ impl FamilyWallet {
event,
);

Self::append_access_audit(&env, symbol_short!("em_mode"), &caller, None, true);

true
}

Expand Down Expand Up @@ -2024,7 +2061,15 @@ impl FamilyWallet {

env.events().publish(
(symbol_short!("emerg"), EmergencyEvent::TransferExec),
(proposer, recipient, amount),
(proposer.clone(), recipient.clone(), amount),
);

Self::append_access_audit(
&env,
symbol_short!("em_exec"),
&proposer,
Some(recipient.clone()),
true,
);

0
Expand Down
86 changes: 86 additions & 0 deletions family_wallet/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,92 @@ fn test_emergency_mode_direct_transfer_within_limits() {

let last_ts = client.get_last_emergency_at();
assert!(last_ts.is_some());

let audit = client.get_access_audit(&2);
assert_eq!(audit.len(), 2);
let em_exec = audit.get(1).unwrap();
assert_eq!(em_exec.operation, symbol_short!("em_exec"));
assert_eq!(em_exec.caller, owner);
assert_eq!(em_exec.target, Some(recipient));
assert!(em_exec.success);
}

#[test]
fn test_set_emergency_mode_appends_access_audit() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, FamilyWallet);
let client = FamilyWalletClient::new(&env, &contract_id);

let owner = Address::generate(&env);
let initial_members = Vec::new(&env);
client.init(&owner, &initial_members);

assert!(client.set_emergency_mode(&owner, &true));

let audit = client.get_access_audit(&1);
assert_eq!(audit.len(), 1);
let entry = audit.get(0).unwrap();
assert_eq!(entry.operation, symbol_short!("em_mode"));
assert_eq!(entry.caller, owner);
assert!(entry.target.is_none());
assert!(entry.success);
}

#[test]
fn test_configure_emergency_appends_access_audit() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, FamilyWallet);
let client = FamilyWalletClient::new(&env, &contract_id);

let owner = Address::generate(&env);
let initial_members = Vec::new(&env);
client.init(&owner, &initial_members);

assert!(client.configure_emergency(&owner, &2000_0000000, &3600u64, &500_0000000, &10000_0000000));

let audit = client.get_access_audit(&1);
assert_eq!(audit.len(), 1);
let entry = audit.get(0).unwrap();
assert_eq!(entry.operation, symbol_short!("em_conf"));
assert_eq!(entry.caller, owner);
assert!(entry.target.is_none());
assert!(entry.success);
}

#[test]
fn test_propose_emergency_transfer_appends_access_audit() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, FamilyWallet);
let client = FamilyWalletClient::new(&env, &contract_id);

let owner = Address::generate(&env);
let initial_members = Vec::new(&env);
client.init(&owner, &initial_members);

let token_admin = Address::generate(&env);
let token_contract = env.register_stellar_asset_contract_v2(token_admin.clone());
let recipient = Address::generate(&env);
let amount = 3000_0000000;

let tx_id = client.propose_emergency_transfer(
&owner,
&token_contract.address(),
&recipient,
&amount,
);

assert!(tx_id > 0);

let audit = client.get_access_audit(&1);
assert_eq!(audit.len(), 1);
let entry = audit.get(0).unwrap();
assert_eq!(entry.operation, symbol_short!("em_prop"));
assert_eq!(entry.caller, owner);
assert_eq!(entry.target, Some(recipient));
assert!(entry.success);
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion insurance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ Owner-only. Updates or clears the `external_ref` field of a policy.

Owner-only. Marks a policy as inactive and removes it from the active-policy list.

**Emits**: `PolicyDeactivatedEvent`
**Hardening Features**:
- **Idempotent**: If the policy is already inactive, returns `false` without duplicate events.
- **Schedule Cleanup**: Automatically deactivates any associated `PremiumSchedule` if present.
- **Standardized Events**: Emits `InsuranceEvent::PolicyDeactivated` and (if applicable) `InsuranceEvent::ScheduleCancelled`.

---

Expand Down
44 changes: 12 additions & 32 deletions insurance/test_snapshots/test/test_create_policy_emits_event.1.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
},
{
"string": "Health Policy"
"string": "Health Insurance"
},
{
"string": "Health"
"u32": 1
},
{
"i128": {
Expand All @@ -31,7 +31,7 @@
{
"i128": {
"hi": 0,
"lo": 10000
"lo": 50000
}
}
]
Expand Down Expand Up @@ -110,7 +110,7 @@
"val": {
"i128": {
"hi": 0,
"lo": 10000
"lo": 50000
}
}
},
Expand All @@ -119,7 +119,7 @@
"symbol": "coverage_type"
},
"val": {
"string": "Health"
"u32": 1
}
},
{
Expand All @@ -146,7 +146,7 @@
"symbol": "name"
},
"val": {
"string": "Health Policy"
"string": "Health Insurance"
}
},
{
Expand Down Expand Up @@ -176,26 +176,6 @@
}
]
}
},
{
"key": {
"symbol": "PRM_TOT"
},
"val": {
"map": [
{
"key": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
},
"val": {
"i128": {
"hi": 0,
"lo": 100
}
}
}
]
}
}
]
}
Expand Down Expand Up @@ -288,10 +268,10 @@
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
},
{
"string": "Health Policy"
"string": "Health Insurance"
},
{
"string": "Health"
"u32": 1
},
{
"i128": {
Expand All @@ -302,7 +282,7 @@
{
"i128": {
"hi": 0,
"lo": 10000
"lo": 50000
}
}
]
Expand Down Expand Up @@ -333,7 +313,7 @@
"val": {
"i128": {
"hi": 0,
"lo": 10000
"lo": 50000
}
}
},
Expand All @@ -342,7 +322,7 @@
"symbol": "coverage_type"
},
"val": {
"string": "Health"
"u32": 1
}
},
{
Expand All @@ -361,7 +341,7 @@
"symbol": "name"
},
"val": {
"string": "Health Policy"
"string": "Health Insurance"
}
},
{
Expand Down
Loading
Loading