security(oracle): price_times_dt overflow still bricks pair after #465 skip #1224
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#1224
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
Follow-up to closed #465. The pair TWAP path can still permanently brick a pool and lock LP capital after a single unprivileged reserve-skewing swap.
#465 replaced panicking
Decimal::from_ratiowithDecimal::checked_from_ratioand skips the observation only when the reserve ratio cannot be represented as aDecimal(ratio aboveDecimal::MAX). That guard does not cover the next multiply:price_times_dtindex-commondoesUint128(price.atomics()) * dt. A ratio just belowDecimal::MAXconstructs aDecimal, passes the skip, then overflowsu128.oracle_update(...)?sits at the top of every reserve-mutating execute path, so the overflow becomes a hard revert.This is a live funds-at-risk / liveness defect on any pair whose oracle still uses that multiply, not a hypothetical design note. It is not a steal-from-pool drain: the attacker does not receive other LPs’ tokens. Impact is permanent lock of the pair’s reserves until a governance wasm migration. Do not treat this as a duplicate of #465 (that ticket’s AC was “no
from_ratiopanic when ratio exceedsDecimal::MAX,” and those tests still pass while this hole remains).Impact (Immunefi-style)
oracle_updateprevents any laterRESERVESwrite. The ratio cannot rebalance anddtonly grows, so the overflow does not heal.dex-commonTWAP helper. Factory / router inherit the brick because they call the pair.Affected surface
smartcontracts/packages/dex-common/src/oracle.rs(price_times_dt)price.atomics() * dtinu128smartcontracts/contracts/pair/src/contract.rs(oracle_update)#465skip only onchecked_from_ratioErrexecute_swap/execute_provide_liquidity/execute_withdraw_liquidityoracle_update(...)?before reserve writes; no other writer ofRESERVESInvariant that is broken: oracle observation must never be able to freeze reserve mutation. A missed TWAP sample is acceptable; a bricked pair is not.
Arithmetic bound (for implementers, not a public attack recipe):
Decimalstores 18 fractional digits in au128.Decimal::MAX.atomics()isu128::MAX(~3.40e38). A constructible price whose atomics are ~1e38 overflowsatomics * dtfor anydtof a few seconds. Terra Classic block times easily exceed that. Ratios in that band are below the#465skip threshold (checked_from_ratiosucceeds).Constraints / guardrails
checked_from_ratioskip for ratios that cannot be aDecimal. Add a second fail-open path forprice × dtoverflow (skip observation or widen), never a?that aborts execute.Okwithout recording) over clamping toDecimal::MAXunless a spec owner documents why a clamp cannot bias TWAP. Same policy as #465.Uint256) is acceptable if it preserves existing TWAP query encoding. Do not silently change observation cardinality, window, or query JSON.MAX_PAIR_ASSET_DECIMALSor on AMMkwidening (#464 is a different product). 6-dec thin pools can still hit a representable-but-overflowing ratio.oracle_observe_singlestill usingfrom_ratio, noted on #465) are out of scope unless they can abort a state transition. Do not expand this ticket into a full oracle rewrite.ready.Relevant files
smartcontracts/packages/dex-common/src/oracle.rsprice_times_dt; add checked/widened multiply + testssmartcontracts/contracts/pair/src/contract.rsoracle_updateskip vs?; call sites on swap / provide / withdraworacle_overflow_tests(existing#465suite)Decimal::MAX×dtcasesmartcontracts/testspair integration (oracle / withdraw / swap)Recommended direction
price_times_dt(and any siblingprice * dtin the same helper) checked or 256-bit. On overflow: skip this observation the same way#465skips an unrepresentable ratio. Never returnErrthatexecute_*propagates.price.atomics()cannot be multiplied by the currentdt(including largedtafter idle).dtgrowth must not be a second brick.dex-commonunit tests: (a) small price × largedtstill records when the product fits; (b) near-max representableDecimal× modestdtdoes not error.Decimalrange but outsideatomics * dtfordt ≥a few seconds, then asserts swap, provide, and withdraw succeed (oracle may skip). Existingextreme_ratio_degrades_gracefully_instead_of_panickingremains the#465case.Acceptance criteria
price_times_dt(or its replacement) cannot overflow-abort. Overflow → skip observation or a widened product that stillOks.Decimalbut whoseatomics * dtoverflowsu128,execute_swap,execute_provide_liquidity, andexecute_withdraw_liquiditydo not revert on oracle arithmetic. LPs can withdraw.#465behavior preserved: ratio that failschecked_from_ratiostill skips; noDecimal::from_ratiopanic on the execute path.normal_ratio_still_records_observationor equivalent stays green).cargo testfordex-commonoracle helpers +cl8y-dex-pairoracle overflow / observation tests +make test-contracts(or the repo’s documented contract suite) pass.Verification (non-exploitative)
Do not publish a step-by-step mainnet or CW20-hook attack. Verify in unit tests:
Decimalwith atomics large enough thatatomics * dtexceedsu128::MAXfor a smalldt(seconds). Assert the helper/oracle update returnsOkand does not record a wrapping value.checked_from_ratiofailure line, advance block time sodtis non-zero, invokeoracle_updatethen each execute entry. Expect success, notprice x dt overflow(or equivalentContractError).oracle_overflow_tests(Uint128::MAX / 1-unit extreme ratio) still skip without panic.cargo test -p cl8y-dex-tests oracleremains green.Already-bricked live pairs are an ops/governance migrate, not this ticket’s code path. This issue only restores forward liveness in wasm.
First-pass model recommendation
Recommendation: grok-high
Rationale: Security / funds-lock on CosmWasm pair + shared
dex-commonTWAP math. Founder-required surface (contracts, wasm, migrate-class recovery). Cross-cutting: helper multiply,oracle_updatecontrol flow, three execute entrypoints, and observation semantics. Composer is disallowed for security and for contract/wasm work even if the edit were a few lines. Verify with the focused oracle overflow tests above plus the existing#465suite — not a live-chain attack.256 bit preferred
Wasm from #1323 is on main (
c17e71d3).make verify-issue-1322passed before merge, and Woodpeckerci/woodpecker/pr/woodpeckersucceeded on the updated head. The live pair is still the old code. Columbus-5 migrate to cw2 1.18.0, keepingOBSERVATIONS, is #1324.Verified on main at
f3dce2d4withmake verify-issue-1224: all 17 checks passed, covering Uint256 price×dt and accumulation, legacy u128 JSON zero-extension, pair and Charts windows crossing 2^128, and preservation of the #465/#1231 ratio-skip behavior.O1322-1–O1322-8 are documented and cross-linked in twap-oracle, the security audit, testing, integrator docs, and the third-party agent skill. The merged implementation and shared verification are tracked in #1322. Columbus-5 migration and live-pair smoke verification remain in #1324.