Skip to content
Closed
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
125 changes: 125 additions & 0 deletions dabdub_contracts/contracts/admin_timelock/EMERGENCY_RUNBOOK.md
Original file line number Diff line number Diff line change
@@ -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<PendingChange>`
- 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)
17 changes: 16 additions & 1 deletion dabdub_contracts/contracts/fee_calculator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub enum DataKey {
Admin,
FeeTiers,
MerchantVolume(Address),
SettlementCaller,
}

#[contracttype]
Expand Down Expand Up @@ -57,14 +58,28 @@ 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<FeeTier> {
env.storage()
.instance()
.get(&DataKey::FeeTiers)
.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, Address>(&DataKey::SettlementCaller) {
if &settlement_caller != &caller {
panic!("Only settlement contract can call calculate_fee");
}
}

if amount <= 0 {
panic!("amount must be > 0");
}
Expand Down
161 changes: 161 additions & 0 deletions dabdub_contracts/contracts/multisig_admin/EMERGENCY_RUNBOOK.md
Original file line number Diff line number Diff line change
@@ -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<Address>`
- 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
59 changes: 59 additions & 0 deletions dabdub_contracts/contracts/payment_escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, &current_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 {
Expand Down Expand Up @@ -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)
Expand Down
Loading