Skip to content

Add emergency pause for deposits/withdrawals under incident response #561

Description

@Oluwaseyi89

Summary

Add emergency pause for deposits/withdrawals under incident response — buffer_pool has no circuit-breaker switch, so a compromised carbon-asset contract or governance key cannot be contained without redeploying or abandoning the pool.

Social Media Link

Let's collaborate on Discord. And ensure to star our repo.

Problem Statement

Confirmed in stellar-core/carbon-asset-factory/contracts/buffer_pool/src/lib.rs, stellar-core/carbon-asset-factory/contracts/buffer_pool/src/storage.rs, and stellar-core/carbon-asset-factory/contracts/buffer_pool/src/errors.rs:

  1. No pause flag exists in storage: storage.rs defines ADMIN, GOVERNANCE, CARBON_CONTRACT, REPLENISH_PCT, TVL, and CUSTODY symbols — there is no PAUSED key or equivalent boolean gate.

  2. deposit() has no pause check: lib.rs:51-85 validates caller identity (caller != admin && caller != carbon_contract) and duplicate custody, but nothing would stop a deposit from proceeding during an active incident.

  3. withdraw_to_replace() has no pause check: lib.rs:89-131 performs a cross-contract transfer call unconditionally once governance auth passes — there is no way to freeze this path while investigating a suspected exploit in the linked carbon_asset_contract.

  4. auto_deposit() has no pause check: lib.rs:133-181 is invoked by the carbon_asset contract on every qualifying mint (per the modulo-based sampling logic) and cannot be temporarily disabled without changing set_replenishment_rate to 0, which the code explicitly rejects — set_replenishment_rate requires 1..=10000 (lib.rs:214-220) and initialize rejects initial_percentage == 0 (lib.rs:32-34), so there is no supported way to fully halt auto-deposits even temporarily.

  5. No admin/governance-gated toggle function exists at all: the only state-mutating admin/governance functions are set_governance_address and set_replenishment_rate (lib.rs:183-225) — neither is designed as, or reusable as, an incident-response kill switch.

  6. No Error variant for a paused-state rejection: whatever this contract's errors.rs defines (Error::Unauthorized, Error::AlreadyExists, Error::ZeroPercentage, Error::InvalidPercentage, Error::TokenNotFound, per usages seen in lib.rs), there is no Error::ContractPaused to reject calls cleanly during a freeze.

  7. No event exists for a pause/unpause transition: events.rs provides emit_deposit_event, emit_withdraw_event, emit_auto_deposit_event, and emit_duplicate_auto_deposit_event, but nothing to signal an incident-response action to off-chain monitors.

  8. get_total_value_locked, get_custody_record, and is_token_in_pool are read-only and unaffected, so a pause implementation must be careful to gate only state-mutating entry points (deposit, withdraw_to_replace, auto_deposit) and not the view functions.

Required Changes

  1. Add a PAUSED: Symbol constant and get_paused(env) -> bool / set_paused(env, paused: &bool) storage helpers to storage.rs, defaulting to false when unset.

  2. Add Error::ContractPaused to errors.rs.

  3. Add pause(env, caller: Address) -> Result<(), Error> restricted to governance (matching the existing governance-gated pattern used by set_governance_address/set_replenishment_rate), setting PAUSED to true.

  4. Add unpause(env, caller: Address) -> Result<(), Error> restricted to governance, setting PAUSED to false.

  5. Add a pause check as the first guard in deposit(), withdraw_to_replace(), and auto_deposit(), returning Err(Error::ContractPaused) immediately when paused — before any auth or storage checks, to minimize wasted budget during an incident.

  6. Add PauseEvent { paused_by: Address } and UnpauseEvent { unpaused_by: Address } to events.rs and publish them from the new functions.

  7. Add is_paused(env) -> bool as a public view function.

  8. Ensure auto_deposit's silent-false-return behavior for the zero-percentage case (lib.rs:150-152) is not confused with the new paused-rejection path — paused must be a distinct Err, not a silent Ok(false), so callers (the carbon_asset contract) can distinguish "no auto-deposit triggered" from "pool is frozen."

  9. Add unit tests: pause() by non-governance fails; pause() by governance succeeds and subsequent deposit/withdraw_to_replace/auto_deposit calls all return ContractPaused; unpause() restores normal operation; view functions (get_total_value_locked, get_custody_record, is_token_in_pool) remain callable while paused.

Acceptance Criteria

  1. deposit(), withdraw_to_replace(), and auto_deposit() all reject with ContractPaused when the pool is paused.
  2. Only the governance address can call pause() or unpause().
  3. PauseEvent and UnpauseEvent are emitted on each transition.
  4. is_paused() accurately reflects current state.
  5. View-only functions remain callable regardless of pause state.
  6. Pausing does not affect already-stored custody records or TVL accounting.
  7. Existing deposit/withdraw/auto-deposit tests continue to pass when the pool is not paused.
  8. Test coverage exists for pause/unpause authorization and for each gated function's rejection while paused.

Directory to Work on:

stellar-core/carbon-asset-factory/

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    RustThis issue is to be implemented with Rust programming language.SorobanThis issue is to be implemented with Soroban SDK

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions