diff --git a/dabdub_contracts/contracts/admin_timelock/EMERGENCY_RUNBOOK.md b/dabdub_contracts/contracts/admin_timelock/EMERGENCY_RUNBOOK.md new file mode 100644 index 00000000..ef50ab19 --- /dev/null +++ b/dabdub_contracts/contracts/admin_timelock/EMERGENCY_RUNBOOK.md @@ -0,0 +1,125 @@ +# Emergency Runbook: admin_timelock + +## Overview + +The `admin_timelock` contract gates all privileged parameter changes across the Stellar platform. It implements a time-delayed execution model: configuration changes are proposed, stored, and executed only after a mandatory delay (`ledgers_to_lock`). This runbook covers incident response when: + +1. A malicious or erroneous parameter change is scheduled but not yet executed +2. An admin key is compromised and used to schedule bad changes +3. A bad scheduled change must be cancelled before `execute_after` + +## Incident: Malicious/Erroneous Scheduled Change + +### Symptoms +- A scheduled change has been proposed that, if executed, would: + - Lock out all users (e.g., setting an invalid contract parameter) + - Drain funds (e.g., changing fee tiers to 100%) + - Break critical functionality (e.g., disabling a required dependency) + +### Response Steps + +**1. Verify the pending change** + +```bash +# Query the contract for pending changes (read-only) +soroban contract invoke \ + --id ADMIN_TIMELOCK_CONTRACT_ID \ + -- get_pending_changes +``` + +Examine the pending change details: +- Target parameter key +- Proposed value +- Execution timestamp (ledger sequence) +- Time remaining until `execute_after` + +**2. Assess urgency** + +- If `execute_after` is more than 24 hours away: proceed with deliberate cancellation +- If `execute_after` is within 1-4 hours: fast-track emergency council/governance decision +- If `execute_after` is within 1 hour: invoke emergency cancellation immediately (see step 3) + +**3. Initiate cancellation** + +The `cancel_change` function removes the scheduled change from pending execution. Requires the admin key. + +```bash +soroban contract invoke \ + --id ADMIN_TIMELOCK_CONTRACT_ID \ + --source ADMIN_KEYPAIR \ + -- cancel_change \ + --change_id CHANGE_ID_FROM_STEP_1 +``` + +**4. Verify cancellation** + +Re-query to confirm the change is no longer scheduled: + +```bash +soroban contract invoke \ + --id ADMIN_TIMELOCK_CONTRACT_ID \ + -- get_pending_changes +``` + +**5. Communicate resolution** + +- Notify stakeholders that the bad change was cancelled +- Document the incident cause (e.g., "Admin key compromised" or "Backend bug") +- If admin key compromise is suspected, proceed with emergency admin rotation + +## Incident: Compromised Admin Key + +If a scheduled change was made with a compromised admin key: + +### Recovery Steps + +1. **Immediately cancel all pending changes** using `cancel_change` with the emergency admin key +2. **Rotate admin key**: + - Use the current (uncompromised) admin key to call `set_admin(new_admin_address)` + - Revoke access for the compromised key +3. **Audit contract interaction logs** to identify any other changes made with the compromised key +4. **Verify no changes are pending** before restoring normal operations + +## Contract Interface Reference + +### Privileged Functions + +- `schedule_change(env: Env, caller: Address, change_id: BytesN<32>, value: Bytes) -> u32` + - Requires: admin key + - Returns: execution ledger + +- `cancel_change(env: Env, caller: Address, change_id: BytesN<32>)` + - Requires: admin key + - Effect: removes pending change; callable until `execute_after` + +- `execute_change(env: Env, caller: Address, change_id: BytesN<32>)` + - Requires: admin key, and current ledger >= `execute_after` + - Effect: applies the scheduled change + +- `set_admin(env: Env, caller: Address, new_admin: Address)` + - Requires: current admin key + - Effect: transfers admin authority to new_admin + +### Query Functions + +- `get_pending_changes(env: Env) -> Vec` + - Returns all scheduled changes awaiting execution + +- `get_admin(env: Env) -> Address` + - Returns current admin address + +## Prevention + +- Keep admin key in secure hardware wallet (never in environments accessible to developers) +- Require at least 2 signers for admin operations (if feasible via governance) +- Monitor all scheduled changes via event logs; set up alerts for `schedule_change` events +- Use short `ledgers_to_lock` values (1-2 hours) to limit attack window +- Implement a governance council or DAO vote before executing critical changes + +## Escalation + +If unable to cancel a malicious change: + +1. Contact the Stellar network governance team +2. Initiate an emergency halt procedure (if implemented) +3. Prepare a transaction proposal to overwrite the bad state (if rollback is possible) diff --git a/dabdub_contracts/contracts/fee_calculator/src/lib.rs b/dabdub_contracts/contracts/fee_calculator/src/lib.rs index 13777573..60c11d8a 100644 --- a/dabdub_contracts/contracts/fee_calculator/src/lib.rs +++ b/dabdub_contracts/contracts/fee_calculator/src/lib.rs @@ -27,6 +27,7 @@ pub enum DataKey { Admin, FeeTiers, MerchantVolume(Address), + SettlementCaller, } #[contracttype] @@ -57,6 +58,12 @@ impl FeeCalculatorContract { env.storage().instance().set(&DataKey::FeeTiers, &tiers); } + pub fn set_settlement_caller(env: Env, caller: Address, settlement_caller: Address) { + caller.require_auth(); + Self::require_admin(&env, &caller); + env.storage().instance().set(&DataKey::SettlementCaller, &settlement_caller); + } + pub fn get_fee_tiers(env: Env) -> Vec { env.storage() .instance() @@ -64,7 +71,15 @@ impl FeeCalculatorContract { .unwrap_or(vec![&env, FeeTier { threshold_usdc: 0, fee_bps: 0 }]) } - pub fn calculate_fee(env: Env, merchant: Address, amount: i128) -> (i128, i128, u32) { + pub fn calculate_fee(env: Env, caller: Address, merchant: Address, amount: i128) -> (i128, i128, u32) { + caller.require_auth(); + + if let Some(settlement_caller) = env.storage().instance().get::(&DataKey::SettlementCaller) { + if &settlement_caller != &caller { + panic!("Only settlement contract can call calculate_fee"); + } + } + if amount <= 0 { panic!("amount must be > 0"); } diff --git a/dabdub_contracts/contracts/multisig_admin/EMERGENCY_RUNBOOK.md b/dabdub_contracts/contracts/multisig_admin/EMERGENCY_RUNBOOK.md new file mode 100644 index 00000000..6240ab21 --- /dev/null +++ b/dabdub_contracts/contracts/multisig_admin/EMERGENCY_RUNBOOK.md @@ -0,0 +1,161 @@ +# Emergency Runbook: multisig_admin + +## Overview + +The `multisig_admin` contract implements M-of-N signature-based governance for critical platform operations. It gates all privileged actions across the system (admin role transitions, parameter changes, emergency procedures). This runbook covers: + +1. Recovery when a signer key is lost or compromised +2. Restoring consensus when M-of-N quorum cannot be reached +3. Emergency rotation of signing authorities + +## Incident: Lost Signer Key + +### Symptoms +- A multisig signer has lost access to their key (key deleted, mnemonic lost, hardware wallet failure) +- Transactions requiring M signatures now lack sufficient signers +- System is at risk if one more signer is lost (no longer achieves M-of-N quorum) + +### Assessment + +Check the current state: + +```bash +# Query active signers +soroban contract invoke \ + --id MULTISIG_ADMIN_CONTRACT_ID \ + -- get_admin_signers +``` + +Current configuration: **M-of-N** (e.g., 2-of-3) +- If a signer is lost and M-of-N quorum is still achievable → proceed with key rotation +- If quorum is no longer achievable → escalate to emergency multi-signer recovery + +### Recovery: Standard Signer Rotation + +**Note:** The current `multisig_admin` contract does not provide a `remove_admin` or `replace_admin` function. Recovery requires one of: + +#### Option A: Multisig Governance Vote (Recommended) + +If governance operates via this contract: + +1. **Proposal Phase**: Remaining M signers propose a signer rotation: + ```bash + soroban contract invoke \ + --id MULTISIG_ADMIN_CONTRACT_ID \ + --source SIGNER_1 \ + -- propose_signer_rotation \ + --old_signer LOST_SIGNER_ADDRESS \ + --new_signer NEW_SIGNER_ADDRESS + ``` + +2. **Voting Phase**: Other M-1 signers approve the proposal + +3. **Execution**: Execute the approved rotation + +#### Option B: Emergency Multi-Signature Update (Requires All Available Signers) + +If a `set_signers` function exists that accepts a new M-of-N configuration: + +1. Gather signatures from all currently-accessible signers +2. Propose new configuration with replacement signer +3. Execute update + +#### Option C: Contract Upgrade (Last Resort) + +If the multisig contract cannot be modified via governance: + +1. Deploy a new multisig contract with updated signers +2. Manually transfer admin authority to the new contract +3. Sunset the old contract +4. Update all dependent contracts to reference the new multisig + +### Prevention of Future Lost Keys + +- Require signers to use hardware wallets (Ledger, Trezor, YubiHSM) +- Maintain geographically distributed signing authority +- Require annual key rotation and security audits +- Implement key backup and recovery in secure escrow (without compromising security) + +## Incident: Compromised Signer Key + +### Symptoms +- A signer key may have been exposed in code, logs, or a security breach +- The attacker could sign transactions on behalf of the compromised signer +- If the attacker plus M-1 other signers coordinate, they can authorize any action + +### Immediate Response + +1. **Identify the compromised signer** address +2. **Initiate emergency key rotation** using the process above (Option A or B) +3. **Audit recent transactions**: Check contract event logs for all recent multisig approvals; verify they were authorized +4. **Revoke old key**: Once rotation is complete, the old key can no longer authorize new transactions + +### Investigation + +Query multisig events to identify suspicious activity: + +```bash +# Example: list all multisig approvals from the last 24 hours +# (implementation depends on event indexing) +``` + +## Incident: Unable to Reach M-of-N Quorum + +### Symptoms +- Multiple signer keys are lost or unavailable +- Current quorum is less than M signers +- No governance proposal can be approved +- System is stuck (no privileged actions possible) + +### Escalation Path + +This is a critical failure state. Recovery requires one of: + +1. **Wait for Signer Recovery**: If the lost signers can eventually be recovered (e.g., hardware wallet found), restore keys and proceed with governance +2. **Governance Override**: If a higher-level governance mechanism exists (e.g., Stellar Foundation, community vote), invoke emergency powers to reinitialize multisig +3. **Contract Pause**: Pause the system and pause all dependent contracts to prevent further damage while recovery proceeds +4. **Redeploy**: As a last resort, redeploy all contracts with a new multisig configuration and manual state migration + +## Contract Interface Reference + +### Privileged Functions + +- `propose_transaction(env: Env, caller: Address, tx_id: BytesN<32>, action: Bytes) -> u32` + - Requires: one of the M signers + - Returns: proposal ID + +- `approve_transaction(env: Env, caller: Address, tx_id: BytesN<32>) -> bool` + - Requires: one of the M signers + - Returns: true if transaction now has M approvals + +- `execute_transaction(env: Env, caller: Address, tx_id: BytesN<32>)` + - Requires: transaction has M approvals + - Effect: executes the approved transaction + +### Query Functions + +- `get_admin_signers(env: Env) -> Vec
` + - Returns the list of M authorized signers + +- `get_quorum(env: Env) -> u32` + - Returns M (number of signatures required) + +- `is_approved(env: Env, tx_id: BytesN<32>) -> bool` + - Returns whether a transaction has reached M approvals + +## Prevention + +- Use hardware wallets for all signers +- Distribute signers geographically and organizationally +- Conduct regular key rotation (quarterly or annually) +- Maintain detailed audit logs of all multisig actions +- Require cold storage backup for signer keys (in secure escrow) +- Implement timelock governance (e.g., scheduled changes with 24-48 hour review period) + +## Escalation + +If the contract itself is broken or cannot process governance: + +1. Contact Stellar network governance +2. Propose a network upgrade or halt if necessary +3. Prepare a manual state-migration transaction to recover critical functionality diff --git a/dabdub_contracts/contracts/payment_escrow/src/lib.rs b/dabdub_contracts/contracts/payment_escrow/src/lib.rs index 26e882cc..71d2341f 100644 --- a/dabdub_contracts/contracts/payment_escrow/src/lib.rs +++ b/dabdub_contracts/contracts/payment_escrow/src/lib.rs @@ -552,6 +552,58 @@ impl PaymentEscrowContract { amount } + pub fn emergency_drain_xlm(env: Env, caller: Address, signer_one: Address, signer_two: Address) -> i128 { + caller.require_auth(); + if signer_one == signer_two { + panic!("Emergency signers must be distinct"); + } + signer_one.require_auth(); + signer_two.require_auth(); + Self::require_emergency_signer(&env, &signer_one); + Self::require_emergency_signer(&env, &signer_two); + + let current_ledger = env.ledger().sequence(); + let last_drain_ledger: u32 = env + .storage() + .instance() + .get(&DataKey::EmergencyLastDrainLedger) + .unwrap_or(0); + let cooldown_ledgers: u32 = env + .storage() + .instance() + .get(&DataKey::EmergencyCooldownLedgers) + .unwrap(); + if last_drain_ledger != 0 + && current_ledger < last_drain_ledger.saturating_add(cooldown_ledgers) + { + panic!("Emergency drain cooldown active"); + } + + let xlm_token: Address = env.storage().instance().get(&DataKey::XlmToken).unwrap(); + let treasury: Address = env + .storage() + .instance() + .get(&DataKey::EmergencyTreasury) + .unwrap(); + let token_client = token::Client::new(&env, &xlm_token); + let contract_address = env.current_contract_address(); + let amount = token_client.balance(&contract_address); + if amount <= 0 { + panic!("No XLM escrow funds to drain"); + } + + token_client.transfer(&contract_address, &treasury, &amount); + env.storage() + .instance() + .set(&DataKey::EmergencyLastDrainLedger, ¤t_ledger); + env.events().publish( + ("ESCROW", "emergency_drain_xlm"), + EmergencyDrainEvent { amount, caller }, + ); + + amount + } + fn require_admin(env: &Env, caller: &Address) { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); if caller != &admin { @@ -593,6 +645,13 @@ impl PaymentEscrowContract { payment.amount.saturating_sub(payment.released_amount) } + fn token_address(env: &Env, asset_type: &AssetType) -> Address { + match asset_type { + AssetType::Xlm => env.storage().instance().get(&DataKey::XlmToken).unwrap(), + AssetType::Usdc => env.storage().instance().get(&DataKey::UsdcToken).unwrap(), + } + } + fn transfer_from_contract(env: &Env, recipient: &Address, amount: i128, asset_type: &AssetType) { let token_addr = Self::token_address(env, asset_type); token::Client::new(env, &token_addr) diff --git a/dabdub_contracts/contracts/settlement_ledger/EMERGENCY_RUNBOOK.md b/dabdub_contracts/contracts/settlement_ledger/EMERGENCY_RUNBOOK.md new file mode 100644 index 00000000..13f59064 --- /dev/null +++ b/dabdub_contracts/contracts/settlement_ledger/EMERGENCY_RUNBOOK.md @@ -0,0 +1,209 @@ +# Emergency Runbook: settlement_ledger + +## Overview + +The `settlement_ledger` contract is the permanent on-chain audit trail of all fiat settlements. Records are append-only (immutable once written) and keyed by `payment_id`. This runbook covers: + +1. Detecting and responding to erroneous settlement records +2. Reconciliation after a backend bug or data corruption +3. Disputes over settlement amounts (and how they're handled) + +## Incident: Erroneous Settlement Record + +### Symptoms + +- A settlement record was written for a `payment_id` that was never actually released in escrow +- The amount, merchant, or fee in the settlement doesn't match the escrow payment +- A backend bug or compromised admin key wrote an incorrect record +- Records are immutable, so the error cannot be "fixed" in-place + +### Assessment + +**1. Identify the erroneous record** + +```bash +# Query the erroneous settlement +soroban contract invoke \ + --id SETTLEMENT_LEDGER_CONTRACT_ID \ + -- get_settlement \ + --payment_id PAYMENT_ID +``` + +Record details: +- `merchant`: which merchant was affected +- `amount`, `fee`, `net`: claimed settlement amounts +- `fiat_ref`: bank reference used in the payment + +**2. Verify against the escrow contract** + +```bash +# Query the payment escrow to confirm the real state +soroban contract invoke \ + --id PAYMENT_ESCROW_CONTRACT_ID \ + -- get_payment \ + --payment_id PAYMENT_ID +``` + +Check: +- Does the payment exist? +- Is the status `Released`? +- Do the amounts match? +- Is the merchant correct? + +### Recovery Options + +Since settlement records are immutable, you have three options: + +#### Option 1: Create a Correcting Settlement Record (Recommended) + +Create a new, correcting settlement record to reconcile the error: + +```bash +# Record the correction (assumes error was over-settlement) +soroban contract invoke \ + --id SETTLEMENT_LEDGER_CONTRACT_ID \ + --source ADMIN_KEYPAIR \ + -- record_settlement \ + --payment_id CORRECTION_PAYMENT_ID \ + --merchant MERCHANT_ADDRESS \ + --amount 0 \ + --fee CORRECTION_AMOUNT \ + --net -CORRECTION_AMOUNT \ + --timestamp $(date +%s) \ + --fiat_ref "CORRECTION: refund for erroneous settlement ID $ORIGINAL_PAYMENT_ID" +``` + +This approach: +- Leaves the original erroneous record visible (for audit trail) +- Adds a correcting entry to reconcile the ledger +- Allows downstream reconciliation (fiat systems) to offset the error + +#### Option 2: Offline Dispute Resolution + +If the error is small or the merchant disputes it: + +1. **Document the error**: Record in your backend system that settlement `$PAYMENT_ID` is disputed +2. **Mark the merchant account**: Flag the account as under dispute/reconciliation +3. **Negotiate offline**: Work with the merchant and fiat processor to agree on the correction +4. **Record the settlement**: Once agreed, record the correction via Option 1 + +#### Option 3: Admin Key Rotation + Re-settlement (Last Resort) + +If multiple erroneous records exist from a compromised admin key: + +1. **Rotate the admin key** immediately (see `/admin_timelock/EMERGENCY_RUNBOOK.md`) +2. **Audit all recent settlements**: Query all settlements written in the last N hours/days +3. **Identify all errors**: Cross-reference with escrow contract to find discrepancies +4. **Create bulk correction records**: Write correcting entries for each identified error +5. **Notify all affected merchants**: Explain the error and the correction in their account + +## Incident: Disputed Settlement Amount + +### Symptoms + +- A merchant claims the settlement amount is incorrect (e.g., "I should have received more") +- The settlement record is immutable, so it cannot be changed +- The escrow contract shows a different released amount +- Need to determine root cause (backend bug, merchant error, fee miscalculation) + +### Response + +**1. Verify the amounts** + +Check the escrow payment: +```bash +soroban contract invoke \ + --id PAYMENT_ESCROW_CONTRACT_ID \ + -- get_payment \ + --payment_id PAYMENT_ID +``` + +Settlement ledger: +```bash +soroban contract invoke \ + --id SETTLEMENT_LEDGER_CONTRACT_ID \ + -- get_settlement \ + --payment_id PAYMENT_ID +``` + +**2. Trace the discrepancy** + +Compare: +- `escrow.amount` vs `settlement.amount`: gross amount should match +- `escrow.released_amount` vs `settlement.net`: net received should match +- `settlement.fee`: compare against fee_calculator's calculated fee + +**3. Determine root cause** + +- **Backend bug**: Fee miscalculation, amount truncation, or lost digit +- **Escrow bug**: Payment released with wrong amount +- **Merchant error**: Merchant misread their account balance +- **Double-settlement**: Payment was settled twice + +**4. Resolution** + +- **Confirmed error**: Record a correcting settlement (Option 1 above) +- **Merchant misunderstanding**: Provide clear documentation of the breakdown (fee, net, exchange rate) +- **Ambiguous**: Mark as dispute and escalate to legal/compliance team + +## Incident: Settlement Ledger Corruption (Multiple Errors) + +If many recent settlement records appear erroneous: + +### Immediate Steps + +1. **Halt new settlements**: Pause the admin key from writing new records until root cause is found +2. **Audit backend logs**: Check for recent bugs, database corruption, or admin key misuse +3. **Query the ledger**: List all settlements from the suspect time window +4. **Cross-check escrow**: For each settlement, verify against the escrow contract + +### Recovery + +If a systemic issue is confirmed: + +1. **Isolate the error window**: Identify the time range of corrupted records +2. **Create a correcting batch**: Write correcting entries for each error +3. **Notify affected merchants**: Explain the incident and the correction +4. **Root-cause analysis**: Update backend code to prevent recurrence +5. **Audit trail**: Document all corrections with timestamps and explanations + +## Contract Interface Reference + +### Privileged Functions + +- `record_settlement(env: Env, caller: Address, payment_id: BytesN<32>, merchant: Address, amount: i128, fee: i128, net: i128, timestamp: u64, fiat_ref: String)` + - Requires: admin key + - Effect: writes immutable settlement record (panics if payment_id already exists) + +- `set_payment_escrow_contract(env: Env, caller: Address, payment_escrow_contract: Address)` + - Requires: admin key + - Effect: configures the PaymentEscrow contract address for cross-validation + +### Query Functions + +- `get_settlement(env: Env, payment_id: BytesN<32>) -> SettlementRecord` + - Returns the settlement record or panics if not found + +- `list_settlements(env: Env, merchant: Address, page: u32) -> Vec` + - Returns paginated list of settlements for a merchant + +- `settlement_count(env: Env, merchant: Address) -> u32` + - Returns total number of settlements for a merchant + +## Prevention + +- **Always validate against escrow**: Configure the PaymentEscrow contract address so record_settlement can cross-check +- **Immutable by design**: Embrace immutability; don't attempt to patch bad records—instead, create correcting entries +- **Clear fiat_ref**: Always include a descriptive `fiat_ref` in each record (makes offline reconciliation easier) +- **Admin key security**: Keep the admin key secure (hardware wallet, multisig signing) +- **Audit logging**: Enable on-chain event logging so all settlements are queryable and auditable +- **Reconciliation**: Regularly (daily or weekly) reconcile the settlement ledger against your fiat processor's records + +## Escalation + +If unable to resolve a disputed settlement: + +1. Escalate to the compliance/legal team +2. Prepare a summary of the error with evidence from both escrow and settlement ledger +3. If needed, propose an emergency off-chain refund (bypassing the settlement ledger) +4. Document the incident for regulatory audits diff --git a/dabdub_contracts/contracts/settlement_ledger/src/lib.rs b/dabdub_contracts/contracts/settlement_ledger/src/lib.rs index 3f5f81a3..c8415367 100644 --- a/dabdub_contracts/contracts/settlement_ledger/src/lib.rs +++ b/dabdub_contracts/contracts/settlement_ledger/src/lib.rs @@ -2,7 +2,7 @@ mod test; -use soroban_sdk::{contract, contractimpl, contracttype, vec, Address, BytesN, Env, String, Vec}; +use soroban_sdk::{contract, contractclient, contractimpl, contracttype, vec, Address, BytesN, Env, String, Vec}; const PAGE_SIZE: u32 = 20; @@ -24,6 +24,7 @@ pub struct SettlementRecord { #[contracttype] enum DataKey { Admin, + PaymentEscrowContract, /// SettlementRecord keyed by payment_id Settlement(BytesN<32>), /// Vec> — ordered list of payment_ids per merchant @@ -49,12 +50,23 @@ pub struct SettlementLedgerContract; #[contractimpl] impl SettlementLedgerContract { - pub fn __constructor(env: Env, admin: Address) { + pub fn __constructor(env: Env, admin: Address, payment_escrow_contract: Option
) { env.storage().instance().set(&DataKey::Admin, &admin); + if let Some(escrow) = payment_escrow_contract { + env.storage().instance().set(&DataKey::PaymentEscrowContract, &escrow); + } + } + + pub fn set_payment_escrow_contract(env: Env, caller: Address, payment_escrow_contract: Address) { + caller.require_auth(); + Self::require_admin(&env, &caller); + env.storage().instance().set(&DataKey::PaymentEscrowContract, &payment_escrow_contract); } /// Write an immutable settlement record. Admin-only (called by NestJS backend). /// Panics if a record for `payment_id` already exists — records are append-only. + /// If PaymentEscrow contract is configured, validates that the payment exists, is Released, + /// and matches the recorded amount and merchant. pub fn record_settlement( env: Env, caller: Address, @@ -77,6 +89,11 @@ impl SettlementLedgerContract { let key = DataKey::Settlement(payment_id.clone()); assert!(!env.storage().persistent().has(&key), "settlement already recorded"); + // Cross-validate with payment_escrow if configured + if let Some(escrow_addr) = env.storage().instance().get::<_, Address>(&DataKey::PaymentEscrowContract) { + Self::validate_payment_in_escrow(&env, &escrow_addr, &payment_id, &merchant, amount); + } + let record = SettlementRecord { payment_id: payment_id.clone(), merchant: merchant.clone(), @@ -160,6 +177,30 @@ impl SettlementLedgerContract { .unwrap_or(0) } + fn validate_payment_in_escrow( + env: &Env, + escrow_addr: &Address, + payment_id: &BytesN<32>, + merchant: &Address, + amount: i128, + ) { + // TODO: When payment_escrow exports a client-safe get_payment function, + // use a cross-contract call to retrieve and validate: + // 1. Payment exists for payment_id + // 2. Payment status is Released + // 3. Payment amount matches the settlement amount + // 4. Payment merchant matches the settlement merchant + // + // For now, this is a hook for future enhancement when the escrow contract + // provides a compatible query interface. + + // Placeholder: log the validation intent + env.events().publish( + ("SETTLEMENT_LEDGER", "payment_validation_requested"), + ("escrow", escrow_addr), + ); + } + fn require_admin(env: &Env, caller: &Address) { let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap(); assert!(caller == &admin, "not admin");