security(bridge): pause must stop withdraw approve or reset cancel window #188
Labels
No labels
agent:implement
agent:ready
backend
bug
cannot-reproduce
confirmed
desktop
docs
documentation
duplicate
enhancement
feature
frontend
good first issue
help wanted
high-risk
in-review
invalid
mobile
needs-triage
P0-critical
P1-high
P2-medium
P3-low
qa
QA
question
ready
report
responsive
security
security-escalate
smart-contract
solana
tablet
test-pass
ux
wallet-issue
wallet:keplr
wallet:metamask
wallet:station
wallet:walletconnect
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-bridge-monorepo#188
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Incident pause stops execute (and user deposit/submit) but does not stop operator approve on EVM or Terra.
approvedAtis wall-clock. A pause that outlasts the remaining cancel window lets execute succeed immediately on unpause, while cancelers may have treated pause as “nothing is moving.”Solana already rejects
withdraw_approvewhilebridge.paused. EVMwithdrawApprovehas nowhenNotPaused. Terraexecute_withdraw_approveloadsCONFIGand never readsconfig.paused. Unpause on all three chains only flips the flag; it does not freeze or restart in-flight windows.This is not #177 (admin
setCancelWindow/ 15s floor / snapshot at approve). This is not #176 (M-of-N operator approve) or #175 (Terramin_signatures). This is not #179 (recoverAssetnative sweep while paused). Keyword overlap on “pause” /approvedAtis not this bug.Internal review id: S-3. Still in source as of 2026-09-12.
Bundle (same ticket, do not split):
Founder-required contracts. No community autoland. Do not add
ready.Impact (today vs hypothetical)
cancelWindow/withdraw_delay, anyone can execute on unpause with no further canceler opportunity.onlyOperator/ Terra operator-or-admin / Solanabridge.operator. Pause/unpause is owner/admin. The hole is that pause does not stop the operator path or the clock.withdrawApprove+pause/unpause; Terraexecute_withdraw_approve+execute_unpause; Solana unpause clock (approve already gated); cancel must remain possible during pause; tests + security-model / Terra pause docs. Not M-of-N (#176/#175). NotrecoverAsset(#179).Invariant that is broken: admin pause is a full incident brake on the withdraw pipeline. Cancelers get a delay they can meet after the incident, not a window that ticks while they assume the bridge is frozen.
Docs already over-claim this: Terra
execute_pausecomment anddocs/contracts-terraclassic.mdsay pause “stops all transfers” / “all bridge operations.” EVM tests only prove deposit reverts while paused. Solana INV-D2 says paused withdraw paths reject; approve is covered there, the unpause clock is not.Do not publish a mainnet pause-then-approve-then-unpause-execute sequence.
EVM: execute has
whenNotPaused; approve does notpause/unpauseareonlyOwnerwrappers around OpenZeppelin_pause/_unpause. They do not touchpendingWithdraws.depositNative/depositERC20/withdrawSubmitwhenNotPausedwithdrawExecuteUnlock/withdrawExecuteMintwhenNotPausedwithdrawApproveonlyOperator nonReentrantonlywithdrawCancel/withdrawUncancelwithdrawApprovesetsapproved = true,approvedAt = block.timestamp, markswithdrawNonceUsed._validateWithdrawExecutionusesw.approvedAt + cancelWindowagainst currentblock.timestamp. Pause does not stop that clock.test_Pause/test_UnpauseinBridge.t.solonly coverdepositERC20. There is nowithdrawApprovewhile paused.withdrawUncancelalready resetsapprovedAt(comment: “restart cancel window”) — that pattern is the right shape for unpause, but unpause does not use it.Terra: submit/execute check
config.paused; approve does notexecute_withdraw_submit,execute_withdraw_execute_unlock, andexecute_withdraw_execute_mintreturnContractError::BridgePaused.execute_withdraw_approveloadsCONFIGfor the operator/admin check, then:pending.approved = truepending.approved_at = env.block.time.seconds()save_pending_and_sync_index+WITHDRAW_NONCE_USEDNo
config.pausedread.execute_withdraw_cancelalso has no pause check (cancelers must remain able to cancel during an incident).execute_unpauseonly setsconfig.paused = false.Tests:
test_execute_while_paused_rejectedandtest_submit_while_paused_rejectedexist. There is no approve-while-paused test.test_execute_while_paused_rejectedeven approves first, waits, then pauses — it never asks whether approve itself is legal during pause.Solana: approve already paused; unpause still burns the window
withdraw_approve::handlerhasrequire!(!bridge.paused, BridgeError::BridgePaused). Tests includewithdraw_approve rejects when paused. Execute also checks pause.withdraw_canceldoes not (keep that).set_configwritesbridge.pausedwith noapproved_atadjustment. Rows approved before pause still age during pause; unpause then allows execute ifClock + delayalready passed. INV-D2 does not mention the clock.Constraints / guardrails
(srcChain, nonce)replay, or execute-after-window. Execute still requiresapprovedandwhenNotPaused.withdrawCanceltraps cancelers for the entire incident. EVM/Terra/Solana cancel stay ungated (or explicitly gated only if product documents a substitute).withdrawUncancelwhile paused (it starts a newapprovedAt). Do not treat uncancel as an incident bypass.approvedAt/approved_at= now for every approved, not executed, not cancelled row (full new window). Matches the asked behavior; delays honest rows by a full window after every pause.max(approvedAt, lastUnpausedAt)) so remaining time is preserved. O(1) via a bridge-levelpausedAt/pauseElapsedis preferred on Solana (pending PDAs are not cheap to enumerate)._pendingWithdrawHashesiteration /__gap. Terra: do not break INV-TC-AW1 (ACTIVE_WITHDRAW_HASHES). Solana: do not require a full PDA walk if a config-level clock works.Pausable: paused/BridgePaused). Out of scope to redesign the writer, but do not claim pause is a complete brake until the chain rejects approve.ready. No public mainnet incident recipe.Relevant files
packages/contracts-evm/src/Bridge.solwithdrawApprovelackswhenNotPaused;pause/unpausedo not touchapprovedAtpackages/contracts-evm/test/Bridge.t.soltest_Pauseis deposit-onlypackages/contracts-terraclassic/bridge/src/execute/withdraw.rsexecute_withdraw_approveignoresconfig.pausedpackages/contracts-terraclassic/bridge/src/execute/admin.rsexecute_unpauseflag-only; comment claims pause stops all transferspackages/contracts-terraclassic/bridge/tests/test_withdraw_flow.rspackages/contracts-solana/programs/cl8y-bridge/src/instructions/withdraw_approve.rspackages/contracts-solana/programs/cl8y-bridge/src/instructions/set_config.rspackages/contracts-solana/tests/deposit_withdraw.test.tsdocs/security-model.mddocs/contracts-terraclassic.mddocs/SOLANA_BRIDGE_INVARIANTS.mdRecommended direction
whenNotPausedtowithdrawApprove(andwithdrawUncancel). Terra:if config.paused { BridgePaused }at the top ofexecute_withdraw_approve(and uncancel). Leave cancel ungated._pendingWithdrawHashesand TerraACTIVE_WITHDRAW_HASHES; Solana still needs a config-level effective start (max(approved_at, last_unpaused_at)).test_Pauseso it is not deposit-only.Acceptance criteria
withdrawApprovewhile paused reverts (Pausable/whenNotPaused). TerraWithdrawApprovewhile paused returnsBridgePaused. Solana approve-paused tests stay green.approvedAt + delay, unpause: execute still reverts until the frozen remainder or a restarted full window has elapsed.Verification (non-exploitative)
Local Forge / CosmWasm / Anchor tests only. Do not pause a production proxy or publish an incident sequence.
withdrawApprovereverts; unpause → approve succeeds. Deposit/execute paused tests stay green.Pause/WithdrawApprove/WithdrawExecuteUnlock. Addtest_approve_while_paused_rejectednext to the existing submit/execute paused tests.CancelWindow/too early).First-pass model recommendation
Recommendation: grok-high
Rationale: Security class plus founder-required contracts (EVM proxy, Terra wasm, Solana program), pause/auth, and cancel-window protocol state. Composer is disallowed (High/security; contracts/keys; pause clock is a cross-chain state/protocol change, not a local three-file edit). A missed unpause reset or a paused cancel gate either restores immediate execute or traps honest cancelers. Verify with Forge + CosmWasm + Anchor tests, not a live pause.