Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
41 changes: 40 additions & 1 deletion docs/family-wallet-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Lower numeric value is higher privilege.
| Add member (legacy overwrite path) | `add_family_member` | Owner or Admin | Role cannot be `Owner`; overwrites existing member record; limit forced to `0` |
| Remove member | `remove_family_member` | Owner only | Cannot remove owner |
| Update per-member spending limit | `update_spending_limit` | Owner or Admin | Member must exist; new limit must be `>= 0`; returns `Result` |
| Configure multisig | `configure_multisig` | Owner or Admin | `threshold > 0`; `threshold <= signers.len()`; all signers must already be family members; spending limit must be `>= 0` |
| Configure multisig | `configure_multisig` | Owner or Admin | `Result<bool, Error>` return; validates: `signers.len() > 0`; `MIN_THRESHOLD <= threshold <= MAX_THRESHOLD`; `threshold <= signers.len()`; all signers must be family members; spending limit must be `>= 0`; blocked when paused |
| Propose transaction | `propose_transaction` and wrappers (`withdraw`, `propose_*`) | `Member` or higher | Caller must be family member; blocked when paused |
| Sign transaction | `sign_transaction` | `Member` or higher | Must be in configured signer list for tx type; no duplicate signature; not expired |
| Emergency config and mode | `configure_emergency`, `set_emergency_mode` | Owner or Admin | Emergency max amount `> 0`; min balance `>= 0` |
Expand All @@ -61,6 +61,9 @@ Lower numeric value is higher privilege.
| `SIGNATURE_EXPIRATION` | `86400` seconds | Pending multisig transaction expiry (24h) |
| `MAX_BATCH_MEMBERS` | `30` | Maximum add/remove batch size |
| `MAX_ACCESS_AUDIT_ENTRIES` | `100` | Access audit ring size (last 100 retained) |
| `MAX_SIGNERS` | `100` | Maximum number of authorized signers per multisig config |
| `MIN_THRESHOLD` | `1` | Minimum valid threshold value |
| `MAX_THRESHOLD` | `100` | Maximum valid threshold value |
| `INSTANCE_BUMP_AMOUNT` | `518400` ledgers | Active-instance TTL extension target |
| `ARCHIVE_BUMP_AMOUNT` | `2592000` ledgers | Archive TTL extension target |

Expand Down Expand Up @@ -227,3 +230,39 @@ These are important as-implemented behaviors:
- `archive_old_transactions` archives all `EXEC_TXS` entries currently present; `before_timestamp` is written into archived metadata but not used as a filter.
- `SplitConfigChange` and `PolicyCancellation` transaction execution paths currently complete without cross-contract side effects.
- Token-transfer execution from `sign_transaction` path calls `proposer.require_auth()` for transfer types, so proposer authorization is required at execution time.

## Error Codes

The contract uses a comprehensive error code system for validation failures:

| Code | Value | Condition |
|---|---|---|
| `Unauthorized` | 1 | Caller lacks required role/permission |
| `InvalidThreshold` | 2 | Threshold exceeds number of signers |
| `InvalidSigner` | 3 | Signer validation failure |
| `TransactionNotFound` | 4 | Pending transaction not found |
| `TransactionExpired` | 5 | Pending transaction has expired |
| `InsufficientSignatures` | 6 | Not enough signatures collected |
| `DuplicateSignature` | 7 | Signer already signed this transaction |
| `InvalidTransactionType` | 8 | Unknown transaction type |
| `InvalidAmount` | 9 | Amount validation failure |
| `InvalidRole` | 10 | Role validation failure |
| `MemberNotFound` | 11 | Family member not found |
| `TransactionAlreadyExecuted` | 12 | Transaction was already executed |
| `InvalidSpendingLimit` | 13 | Spending limit must be >= 0 |
| `ThresholdBelowMinimum` | 14 | Threshold < MIN_THRESHOLD (1) |
| `ThresholdAboveMaximum` | 15 | Threshold > MAX_THRESHOLD (100) |
| `SignersListEmpty` | 16 | Signers list is empty |
| `SignerNotMember` | 17 | Signer is not a family member |

### Multisig Threshold Bounds Security

The `configure_multisig` function enforces strict bounds validation to prevent invalid execution policy states:

- **Minimum threshold**: `MIN_THRESHOLD = 1` - At least one signature required
- **Maximum threshold**: `MAX_THRESHOLD = 100` - Prevents unreasonably high requirements
- **Consistency check**: `threshold <= signers.len()` - Threshold cannot exceed available signers
- **Signer validation**: All signers must be existing family members
- **Empty list rejection**: At least one signer required

These bounds prevent configuration errors that could lock the wallet (threshold > signers) or render it unusable (empty signers or zero threshold).
27 changes: 23 additions & 4 deletions family_wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ pub struct AccessAuditEntry {
const CONTRACT_VERSION: u32 = 1;
const MAX_ACCESS_AUDIT_ENTRIES: u32 = 100;
const MAX_BATCH_MEMBERS: u32 = 30;
const MAX_SIGNERS: u32 = 100;
const MIN_THRESHOLD: u32 = 1;
const MAX_THRESHOLD: u32 = 100;

#[contracttype]
#[derive(Clone)]
Expand Down Expand Up @@ -172,6 +175,10 @@ pub enum Error {
MemberNotFound = 11,
TransactionAlreadyExecuted = 12,
InvalidSpendingLimit = 13,
ThresholdBelowMinimum = 14,
ThresholdAboveMaximum = 15,
SignersListEmpty = 16,
SignerNotMember = 17,
}

#[contractimpl]
Expand Down Expand Up @@ -449,15 +456,27 @@ impl FamilyWallet {
panic!("Only Owner or Admin can configure multi-sig");
}

// Validate threshold
let signer_count = signers.len();
if threshold == 0 || threshold > signer_count {
panic!("Invalid threshold");

if signer_count == 0 {
panic!("Signers list cannot be empty");
}

if threshold < MIN_THRESHOLD {
panic!("Threshold must be at least {}", MIN_THRESHOLD);
}

if threshold > MAX_THRESHOLD {
panic!("Threshold cannot exceed {}", MAX_THRESHOLD);
}

if threshold > signer_count {
panic!("Threshold cannot exceed number of signers");
}

for signer in signers.iter() {
if members.get(signer.clone()).is_none() {
panic!("Signer must be a family member");
panic!("Signer is not a family member");
}
}

Expand Down
Loading
Loading