Skip to content

fix(globe-wallet): enforce timelock on spend limit increases to protect against compromised keys (issue #83) - #98

Closed
s6pa1rta3n-lab wants to merge 1 commit into
Orbit-Wal:mainfrom
s6pa1rta3n-lab:fix-spend-limit-compromised-key-issue-83
Closed

fix(globe-wallet): enforce timelock on spend limit increases to protect against compromised keys (issue #83)#98
s6pa1rta3n-lab wants to merge 1 commit into
Orbit-Wal:mainfrom
s6pa1rta3n-lab:fix-spend-limit-compromised-key-issue-83

Conversation

@s6pa1rta3n-lab

Copy link
Copy Markdown

Root Cause Analysis

record_spend's doc comment explicitly states its compromised-key threat model:

"require_auth() on user authenticates the caller, not the value — in the compromised-key threat model this function exists to defend against, the attacker can already produce valid user signatures, so require_auth() alone provides no protection here."

However, set_spend_limit previously allowed instant limit increases (including raising the limit to i128::MAX or removing it by setting limit = 0) with only user.require_auth(). Under this threat model, an attacker holding a compromised user key could unilaterally call set_spend_limit(user, asset, i128::MAX) and immediately drain the entire wallet balance past the daily spend cap in a single transaction.

Solution & Design Decision

We implement asymmetric handling for spend limit modifications:

  1. Limit Decreases & Initial Cap Configuration (Instant): Setting an initial cap on an unconstrained wallet (current_limit == 0) or lowering an existing cap (limit <= current_limit && limit > 0) is applied instantly via set_spend_limit. This operation strictly tightens security headroom. Retroactive enforcement is maintained: if today's spend already exceeds the new lower limit, the call is rejected with SpendLimitExceeded.
  2. Limit Increases & Cap Removal (Timelocked): Attempting to directly increase an active cap or set it to 0 (unlimited) via set_spend_limit is rejected with WalletError::SpendLimitIncreaseRequiresProposal. Raising or removing a limit must go through a timelock proposal flow:
    • propose_spend_limit_increase(env, user, asset_code, limit, delay_in_ledgers): Queues the increase proposal with a mandatory timelock delay of at least GlobeWallet::MIN_SPEND_LIMIT_INCREASE_DELAY (~24 hours / 17,280 ledgers).
    • execute_spend_limit_increase(env, user, asset_code): Executes the proposal once the timelock window has elapsed (env.ledger().sequence() >= ready_at).
    • cancel_spend_limit_increase(env, user, asset_code): Allows the legitimate account owner to notice the unauthorized proposal on-chain and cancel it before execution.
    • get_spend_limit_proposal(env, user, asset_code): Returns the in-flight proposal.

Verification & Test Coverage

All tests in the workspace pass (88 tests total). The following tests specifically verify the fix:

  • test_spend_limit_is_self_escalatable_by_the_key_it_defends_against: Proves that a compromised-key holder cannot instantly raise their own limit and spend past the previously-configured cap in one transaction, and cannot bypass the timelock with delay = 0 or execute before the delay elapses.
  • test_spend_limit_increase_happy_path: Proves legitimate limit increases succeed via propose_spend_limit_increase -> wait timelock -> execute_spend_limit_increase.
  • test_spend_limit_increase_rejects_delay_below_minimum: Proves proposals with delay < MIN_SPEND_LIMIT_INCREASE_DELAY are rejected with InvalidTimelockDelay.
  • test_spend_limit_increase_rejects_non_increase: Proves proposals that do not increase the cap are rejected with SpendLimitNotAnIncrease.
  • test_spend_limit_increase_already_pending_fails: Proves second proposals while one is pending are rejected with SpendLimitIncreaseAlreadyPending.
  • test_cancel_spend_limit_increase: Proves proposals can be cancelled and cannot be executed afterward.
  • test_spend_limit_decrease_is_instant_and_unaffected: Proves legitimate limit decreases remain instant.
  • test_raise_spend_then_lower_limit: Verifies retroactive enforcement behavior.

Payout Routing

  • EVM (Base/Arbitrum/Polygon/ETH): 0xF46C9F6d70C50BF81ef3588AB523a90a594a2F89
  • Stellar: GCL6OXAMLD75BMTINA6EMRUDWK5THQUSHMYNLSNBCJAPZJHNYJTUNIBC

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant