This document describes the Emergency Key Revocation mechanism implemented in src/admin.rs and integrated into the main contract. This system provides a secure, automated way to revoke compromised administrative or coordinator keys without requiring a full contract upgrade.
If an administrative or coordinator hot-wallet key becomes compromised, the system previously lacked a secure, automated method to strip its permissions. The original process would require:
- A full contract upgrade (slow, complex)
- Manual intervention (prone to delays during emergencies)
- No instant blocking capability
#[contracttype]
#[derive(Clone)]
pub struct EmergencyRevocationProposal {
/// Address to be revoked (compromised key)
pub target: Address,
/// Replacement administrator
pub replacement: Address,
/// Signer that proposed the revocation
pub proposer: Address,
/// Ledger timestamp when proposed
pub proposed_at: u64,
/// Votes from authorized signers
pub votes: Map<Address, ()>,
}EMERGENCY_REVOCATION_KEY: Active emergency revocation proposalREVOKED_SIGNER_KEY: Set of revoked (blocked) addressesREVOCATION_EXPIRY_SECONDS: 7-day expiration window for proposals
New contract errors:
RevokedAddress(23): Address was revoked and cannot signEmergencyRevocationAlreadyActive(24): Emergency revocation already pendingNoActiveEmergencyRevocation(25): No emergency revocation proposal active
Function: propose_emergency_revocation(target, replacement, proposer)
Who Can Call: Admin or any registered signer
Actions:
- Validates caller is authorized (admin or signer)
- Checks no active revocation exists for target
- Creates
EmergencyRevocationProposalwith:- Target address to be revoked
- Replacement admin
- Proposer identity (for audit trail)
- Current ledger timestamp
- Empty votes map
Storage: Proposal stored at EMERGENCY_REVOCATION_KEY
Constraints:
- Only one emergency revocation can be active at a time
- Proposals expire after 7 days if not executed
Function: vote_emergency_revocation(voter)
Who Can Call: Admin or any registered signer (except revoked addresses)
Actions:
- Validates voter is authorized and not revoked
- Checks proposal hasn't expired (7 days)
- Prevents double-voting
- Adds vote to proposal's vote map
- Calculates threshold:
(signers_count + 1) / 2 + 1(simple majority) - If threshold reached:
- Blocks target address in
REVOKED_SIGNER_KEY - Promotes replacement to admin (if not already)
- Removes proposal from storage
- Instantly effective - no waiting period
- Blocks target address in
- If threshold not reached: Updates proposal with new vote count
Returns: Boolean indicating if execution occurred
Threshold Calculation:
- Total eligible voters = number of signers + 1 (admin)
- Threshold = (total / 2) + 1 (strict majority)
- Example: 4 signers + admin = 5 eligible voters → 3 votes needed
Enforcement Points:
-
Signer Registration (
register_signer)- Prevents registering revoked addresses as signers
- Returns
RevokedAddresserror
-
Voting Operations (
vote_emergency_revocation,vote_revocation)- Revoked addresses cannot participate in any voting
- Returns
RevokedAddresserror
-
Query Functions (
is_address_revoked)- Provides boolean status for any address
- Accessible by any contract participant
// Propose emergency revocation
pub fn propose_emergency_revocation(
env: Env,
target: Address, // Address to revoke
replacement: Address, // New admin
proposer: Address, // Must be signer or admin
) -> Result<(), ContractError>
// Vote on emergency revocation
pub fn vote_emergency_revocation(
env: Env,
voter: Address, // Must be signer or admin
) -> Result<bool, ContractError> // Returns true if executed
// Get current proposal
pub fn get_emergency_revocation_proposal(
env: Env,
) -> Option<admin::EmergencyRevocationProposal>
// Get current vote count
pub fn get_emergency_revocation_vote_count(env: Env) -> Option<u32>
// Check if address is revoked
pub fn is_address_revoked(env: Env, addr: Address) -> bool
// Get all revoked addresses (placeholder)
pub fn get_revoked_addresses(env: Env) -> Vec<Address>
// Cancel active proposal (admin only)
pub fn cancel_emergency_revocation(
env: Env,
canceller: Address,
) -> Result<(), ContractError>// Propose emergency revocation
pub fn propose_emergency_revocation(
env: &Env,
target: Address,
replacement: Address,
proposer: Address,
signers: &Map<Address, ()>,
) -> Result<(), ContractError>
// Vote on emergency revocation
pub fn vote_emergency_revocation(
env: &Env,
voter: Address,
signers: &Map<Address, ()>,
revoked_addrs: &mut Map<Address, ()>,
) -> Result<bool, ContractError>
// Get current proposal status
pub fn get_emergency_revocation_proposal(env: &Env) -> Option<EmergencyRevocationProposal>
// Get vote count
pub fn get_emergency_revocation_vote_count(env: &Env) -> Option<u32>
// Cancel proposal
pub fn cancel_emergency_revocation(
env: &Env,
canceller: Address,
) -> Result<(), ContractError>- Simple majority required (>50% of eligible voters)
- Prevents single-point-of-failure revocations
- Includes admin in voter count
- No timelock delay (unlike upgrade mechanism)
- Compromise requires immediate response
- Storage flags updated instantly upon threshold
- Only authorized signers or admin can propose
- Only authorized signers or admin can vote
- Revoked addresses cannot participate in any voting
- Proposer identity stored
- Proposal timestamp recorded
- Vote history preserved until execution
- All via ledger-readable storage
- Proposals expire after 7 days
- Prevents stale emergency votes from lingering
- Automatic cleanup on vote attempt past expiry
- Revoked addresses cannot be re-registered as signers
- Revoked addresses cannot vote on any proposals
- Revoked addresses cannot execute contract functions requiring auth
use soroban_sdk::{Address, Env};
// Setup: Initialize contract with admin
env.initialize(&admin);
// Register signers for multi-sig
env.register_signer(&signer1, &admin);
env.register_signer(&signer2, &admin);
env.register_signer(&signer3, &admin);
// EMERGENCY SCENARIO: signer2's key is compromised
// Step 1: Propose revocation (by signer1)
env.propose_emergency_revocation(
&signer2, // compromised key
&admin, // keep current admin
&signer1, // proposer
)?;
// Step 2: Signers vote to approve (need 2 out of 4 = 3 votes)
env.vote_emergency_revocation(&signer1)?; // Returns false (1/3 votes)
env.vote_emergency_revocation(&signer3)?; // Returns false (2/3 votes)
env.vote_emergency_revocation(&admin)?; // Returns true (3/3 votes - EXECUTED)
// Step 3: Verify revocation
assert!(env.is_address_revoked(&signer2));
assert!(env.is_address_revoked(&signer1).not());
// Step 4: Attempt to register revoked address fails
assert!(env.register_signer(&signer2, &admin).is_err());- Uses
Map<Address, ()>for O(1) revocation lookups - Revoked address set maintained per-contract
- Only stores what's necessary (address presence)
let total_eligible = signers.len() + 1; // +1 for admin
let threshold = total_eligible / 2 + 1; // Integer divisionExamples:
- 1 signer + 1 admin = 2 voters → 2 votes needed (100%)
- 2 signers + 1 admin = 3 voters → 2 votes needed (67%)
- 3 signers + 1 admin = 4 voters → 3 votes needed (75%)
- 4 signers + 1 admin = 5 voters → 3 votes needed (60%)
- Validate voter authorization and not revoked
- Check proposal not expired
- Prevent double-voting
- Record vote
- Count votes
- If threshold reached:
- Add target to revoked addresses
- Promote replacement to admin (if needed)
- Clean up proposal storage
- Return
true
- If threshold not reached:
- Store updated proposal with new vote
- Return
false
-
EmergencyRevocationProposalstruct defined - Error types added (
RevokedAddress,EmergencyRevocationAlreadyActive,NoActiveEmergencyRevocation) - Storage keys defined (
EMERGENCY_REVOCATION_KEY,REVOKED_SIGNER_KEY) - Proposal function implemented in
admin.rs - Voting function implemented in
admin.rs - Helper functions in
admin.rs - Public contract methods in
lib.rs - Revocation checks in sensitive operations
- Error propagation for revoked addresses
- Revocation Cooldown: Add delay before revoked address can be registered again
- Revocation History: Maintain append-only log of all revocations
- Partial Revocation: Revoke specific permissions instead of full address block
- Revocation Appeal: Allow revoked address to propose self-reinstatement
- Time-Delayed Revocation: Allow revocation to take effect after delay (safety check)
- Only authorized addresses can propose revocation
- Threshold calculation is correct for various signer counts
- Double-voting prevention works
- Proposal expiration after 7 days
- Instant execution upon threshold
- Revoked addresses cannot vote
- Revoked addresses cannot be registered as signers
- Replacement admin is correctly promoted
- Revocation status persists across storage
- Multiple concurrent revocation attempts are blocked
- Storage Model: Instance storage for fast access, no TTL concerns
- Auth Model: Uses
require_auth()for each signer - Error Handling: Explicit error types for all failure modes
- Time Source: Stellar ledger timestamp for proposal expiration