[PoC] Abstract symbolic multiplication (abst_evm_bvmul) for vault-style verification - #1073
Closed
gustavo-grieco wants to merge 14 commits into
Closed
[PoC] Abstract symbolic multiplication (abst_evm_bvmul) for vault-style verification#1073gustavo-grieco wants to merge 14 commits into
gustavo-grieco wants to merge 14 commits into
Conversation
Extend the arithmetic abstraction to multiplication, so vault-style properties involving symbolic*symbolic products become provable while keeping hevm SOUND. Encoding (src/EVM/SMT/DivModEncoding.hs, src/EVM/SMT.hs): - Route only symbolic*symbolic Mul through an uninterpreted abst_evm_bvmul (concrete/0/1/power-of-two factors stay native). No mul ground truth, so the solver never bit-blasts a symbolic product. - Sound lemmas only: commutativity; 0/1 identities; div x mul link (quotient*divisor <= dividend); div monotonicity; and mul monotonicity guarded by a bound-free no-overflow predicate (high half of the 512-bit product is zero). Bounding (e.g. require(x < 2**128)) is supplied in Solidity, not hardcoded here. Soundness (src/EVM/Solvers.hs): - A QED is a real proof (the lemmas over-approximate real EVM arithmetic). - Because abst_evm_bvmul is uninterpreted, a SAT model may use product values inconsistent with real multiplication, so a counterexample cannot be trusted: when the mul abstraction is active, SAT is downgraded to Unknown. div-only counterexamples (div is exact via its ground truth) are unaffected. Tests (test/test.hs, group "Mul-Abstraction"): mul monotonicity, 0/1 identities, div x mul link, ERC-4626 share-price monotonicity (QED with abstraction, unknown natively); plus soundness cases: unbounded overflow must not yield a bogus QED, the cross-divisor round-trip's spurious model must not be reported as a counterexample, and div/constant-mul counterexamples remain reported. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the set of vault-style properties the multiplication abstraction
proves, beyond plain monotonicity. Both lemmas are sound (true of real EVM
arithmetic) and were each validated to unlock their target via the CLI.
- Division anti-monotonicity in the divisor: y1 <= y2 && y1 != 0 => x/y2 <= x/y1.
Unlocks divisor-ordering ("a cheaper price gives at least as many units").
- mulDiv bound: for (x*y)/z, if y <= z (or x <= z) then the result is <= x
(resp. <= y), guarded by no-overflow of x*z (resp. y*z). Unlocks the common
DeFi pattern, e.g. a fee of feeBps/10000 never exceeds the principal.
A third lemma (multiplication cancellation, m(x,z)<=m(y,z)=>x<=y) was tried and
reverted: it does not help bound-style properties such as (x*s)/s <= x, because
the abstract product is unpinned, so the division result is unbounded and the
no-overflow guard never discharges. Those need the product pinned (i.e.
bit-blasted), which the abstraction deliberately avoids.
Soundness is unchanged: QED remains a real proof, and properties that the
lemmas cannot discharge (or that are false) stay Unknown rather than producing
a bogus proof or counterexample.
Tests (group "Mul-Abstraction"): div-divisor-monotone, muldiv-fee-cap (QED),
and muldiv-fee-uncapped-not-unsound (must stay Unknown).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI (solc 0.8.31) canonicalises `a * b` with the shared operand FIRST (e.g. Mul (SLoad slot) (ReadWord arg)), whereas solc 0.8.2 put it second. mul-monotonicity paired products by a shared *second* operand only, so under the 0.8.31 ordering the lemma was not generated, the abstraction could not prove the property, and the spurious model was (soundly) downgraded to Unknown — making `vault-shares-monotonic` fail with "expected QED, got Unknown". Fix: feed both operand orders into the monotonicity pairing (multiplication is commutative and we already assert it, so the generated term links back to the one in the props). The division lemmas remain order-sensitive, as they must. Verified on the exact failing CI bytecode: now QED (was Unknown); full local regression unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two changes that let realistic vault queries (operand magnitudes ~2^100, e.g. token amounts up to 1e30) prove in seconds instead of timing out: 1. mulNoOverflow now uses the cheap *sufficient* condition `x <= 2^128-1 && y <= 2^128-1` instead of the exact bound-free predicate `extract 511 256 (bvmul (zext x) (zext y)) = 0`. The exact form forced the solver to bit-blast a full 512-bit multiply for every lemma instance, which was the dominant cost: monotonicity over operands bounded by 2^96+ timed out, while 2^64 proved instantly. The sufficient condition is sound (when it holds there is genuinely no overflow) and is the same width-budget idea Halmos uses. All realistic token amounts, share counts and conversion rates are far below 2^128. 2. New const-mul monotonicity lemma (mkConstMulMono): for a literal constant c, `x <= y => c*x <= c*y`. Multiplication by a constant stays a native bvmul, but a symbolic operand times a large constant (e.g. 1e27 in sUSDS rate conversions) is still costly to bit-blast and compare. Since c is concrete we compute the exact no-overflow bound floor((2^256-1)/c) at encode time, so the guard is a single constant comparison. This is what unblocks `value * 1e27 / rate`-style conversions, which div-monotonicity could not close without first ordering the two const-mul dividends. Adds a constmul-div-monotone test mirroring spark-psm convertToAssets(sUSDS). Existing Mul-Abstraction tests are unaffected (the 2^128-bounded ones discharge the cheap guard exactly; soundness tests stay Unknown/Cex). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When an abstract product a*b has a factor that is also used as a divisor elsewhere in the query, synthesize the exact division (a*b)/factor and add it to the div set. The existing sound lemmas then bridge nested divisions: mulDiv-bound gives (a*b)/b <= a, and div-monotonicity carries that bound across a shared divisor. This proves the cross-divisor round-trip convertToAssetValue(convertToShares(x)) <= x, i.e. ((x*ts)/ta)*ta/ts <= x — the stateless core of inflation-attack safety — which single-level lemmas could not discharge (the bridging term (x*ts)/ts was simply absent from the query). Soundness: the synthetic terms are exact EVM divisions and every lemma asserted about them is a true fact, so a QED remains a real proof. Verified that the downgrade-soundness checks (mul-overflow-not-unsound, muldiv-fee-uncapped) still report Unknown and the counterexample tests still report Cex — the added lemmas are consistent (an inconsistency would have turned those into spurious QEDs). The former roundtrip-spurious-cex-suppressed test now proves the property, so it becomes the positive roundtrip-no-inflation test; downgrade soundness stays covered by the two *-not-unsound tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The round-trip (cancellation synthesis) and the const-mul-div monotonicity (large 1e27 constant multiply) are reliably discharged by z3 but not by bitwuzla. Both solvers are sound; this is purely a capability pick. Since withBitwuzlaSolver runs with no SMT timeout, an unprovable-for-bitwuzla query would hang the test rather than fail fast, so prefer the solver verified to close these. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e round-trip Reverts the round-trip and const-mul-div tests from z3 back to bitwuzla. The earlier switch was made when bitwuzla was missing from the local PATH and every query spuriously returned unknown; with bitwuzla actually present it discharges all of these in well under a second (and is the bit-vector solver hevm defaults to). On the checked-arithmetic round-trip bitwuzla is in fact the *stronger* solver — z3 is the one that struggles there. Adds roundtrip-checked-realcode: the inflation-attack round-trip on real-code- shaped Solidity — ordinary checked arithmetic (so each `*` carries the 0.8 overflow guard div(mul(a,b),a)) and the conversions are real functions called as in the contract. Confirms the cancellation synthesis survives the overflow-guard divisions, i.e. the property proves on the code as written, not only on raw assembly ops. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t test - Reword the no-overflow guard comment in generic terms (large operands ~2^100) instead of naming specific token/amount domains. - Drop the spark-specific note from the const-mul-div test comment. - Remove the redundant roundtrip-checked-realcode test: roundtrip-no-inflation already covers the cross-divisor round-trip; the cancellation synthesis still discharges Solidity's overflow-guard divisions (described in the encoding). No functional change to the abstraction; comments and test surface only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multiplication by a literal constant stays a native bvmul, but the surrounding division is abstracted (uninterpreted), so a divide-by-the-same-constant wrapper like `amount * 1e18 / 1e18` (e.g. ERC-4626 precision scaling) is not provably the identity. This lemma pins it: for a division (c*x)/c by the same literal c, assert the result is x, guarded by the exact no-overflow bound floor((2^256-1)/c) computed at encode time (a single constant comparison, like const-mul-mono). Sound: (c*x)/c == x exactly whenever c*x does not overflow, which the guard ensures. The lemma only fires when the divisor literal equals the multiplier literal, so a near-miss like (x*1e18)/(1e18+1) is left uninterpreted (verified: it still yields a counterexample, not a spurious QED). Adds a const-cancel test. This discharges constant-precision wrappers in isolation; it does not by itself close the largest composite queries (e.g. a full previewDeposit/convertToAssets round-trip), where the surrounding structure remains beyond the solver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two sound lemmas that discharge precision-scaling closed forms (exact-value spec-conformance), which the abstraction otherwise leaves uninterpreted: 1. Generalize const cancellation from (c*x)/c == x to (c1*x)/c2 == (c1/c2)*x when the literal divisor c2 divides the literal multiplier c1 (c2 | c1). The same-constant case (c1==c2, k=1) is unchanged; the general case covers lossless scaling between decimals, e.g. amount*1e18/1e6 == amount*1e12. Guarded by the exact no-overflow bound floor((2^256-1)/c1). 2. Nested-division collapse: (A/c1)/c2 == A/(c1*c2) for literals c1,c2 whose product fits in 256 bits. A floor identity, sound unconditionally; collapses chained constant divisions like x*rate/1e9/1e18 to x*rate/1e27. Both verified sound (correct form proves; a wrong constant yields a counterexample, never a spurious QED) and non-regressing. Adds const-cancel-scaled and nested-div-collapse tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror of the generalized const-cancel (which needs c2 | c1): this covers the c1 | c2 direction (multiply by a small constant, divide by a large one), e.g. x*1e6/1e18 == x/1e12 -- precision scaling DOWN (the convert-to-usdc step). Sound because, under the no-overflow guard x <= (2^256-1)/c1, c1*x is exact and floor(c1*x/(c1*k)) = floor(x/k). Adds fraction-reduce and fraction-reduce-cex unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ceilDiv sibling of const-cancel. OpenZeppelin Math.ceilDiv(c1*x, c2) is (c1*x == 0) ? 0 : (c1*x - 1)/c2 + 1, so the abstracted divide is over the (c1*x - 1) dividend. When c2 | c1 the product is always a multiple of c2, so ceil == floor and the whole ceilDiv collapses to (c1/c2)*x. The lemma pins the inner divide: for x >= 1, (c1*x - 1)/c2 == (c1/c2)*x - 1 (sound: c1*x = c2*m*x, floor((c2*m*x - 1)/c2) = m*x - 1), under the encode-time no-overflow bound floor((2^256-1)/c1). Discharges round-up multiply-up swap-out closed forms (e.g. previewSwapExactOut usds->usdc == amountOut*1e12). Adds ceildiv-cancel and ceildiv-cancel-cex unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gustavo-grieco
added a commit
to gustavo-grieco/spark-psm
that referenced
this pull request
Jun 18, 2026
More stateless properties proved against the real PSM3 under the hevm multiplication abstraction (argotorg/hevm#1073): - ProveOriginals: 8 no-value conversion delegates (totalShares==0 branch) to the repo's UNMODIFIED Conversions fuzz tests -- convertToAssetValue/convertToShares identity, and convertToAssets / convertToShares per-asset precision forms (1:1, fraction-reduce, symbolic-divisor); plus previewSwapExactOut usds->usdc exact (== amountOut*1e12), discharged by the new ceilDiv-cancel lemma. - ProveGetters: getSUsdsValue rate-direction monotonicity (both rounding modes) -- nondecreasing in the conversion rate. - ProveSwapPreviews: rate-direction monotonicity for all 8 rate-bearing swap legs (ExactIn output and ExactOut required-input rise/fall with the rate as each leg dictates; rate-as-multiplier via mul-mono+div-mono, rate-as-divisor via divisor-anti-mono). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e div set Two related additions to the multiplication abstraction: 1. Scaled-product telescoping (sound, no-overflow guarded): for products sharing a factor a whose other factors differ by a literal k that the common divisor c divides, floor(a*b/c) == floor(a*(b-k)/c) + a*(k/c). This is the only lemma that relates two DISTINCT abstract products, so it discharges value-change accounting identities such as susds*rate/1e27 - susds == susds*(rate-1e27)/1e27 (rate and rate-1e27 otherwise give unrelated abst_evm_bvmul terms). Sound because a*b = a*(b-k) + a*k and a*k is an exact multiple of c, shifting the floor by exactly a*(k/c). Validated: SMT soundness (m=1 and m=2) and wrong-RHS rejection, plus necessity/efficacy against the existing lemma set. 2. Collapse a nested constant division (A/c1)/c2 into a synthetic single divide A/(c1*c2) added to the div set, so the single-divide lemmas (telescoping, const-cancel, fraction-reduce) match code that splits precision across two divides — e.g. _getSUsdsValue's x*rate/1e9/1e18, whose collapsed x*rate/1e27 is what those lemmas key on. Sound: nested-div-collapse already asserts the two forms equal, so the synthetic divide is exact. Additive (never removes a term), so it cannot introduce a false QED. Tests: scaled-product-telescope (QED) and scaled-product-telescope-not-unsound (wrong difference stays Unknown, never QED) in the Mul-Abstraction group. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gustavo-grieco
added a commit
to gustavo-grieco/spark-psm
that referenced
this pull request
Jun 19, 2026
Add prove_totalAssets_rateIncrease_valueChange: when the rate rises from 1e27 to q, totalAssets() rises by exactly the sUSDS revaluation su*(q-1e27)/1e27, against the real totalAssets() read twice. This is the third assertion of the conversionRate fuzz tests, discharged by the new scaled-product telescoping lemma (argotorg/hevm#1073) now reaching the nested /1e9/1e18 precision split. Together with the two aggregate identities, this covers the full content of the Conversions.t.sol conversionRate fuzz tests. Those tests are not delegated directly: they run three real _deposit()s through MockERC20, whose keccak-mapping balance reads are intractable nested in the abstracted arithmetic (the reason ProveRealPSM3 uses a single-slot mock); the note in ProveOriginals records this. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gustavo-grieco
added a commit
to gustavo-grieco/spark-psm
that referenced
this pull request
Jun 19, 2026
test/symbolic/echidna.yaml runs `echidna --mode verification` over ProveRealPSM3, discharging all 17 stateless conversion properties against the real deployed PSM3 with the arithmetic-abstraction hevm (argotorg/hevm#1073). echidna test/symbolic/ProveRealPSM3.t.sol --contract ProveRealPSM3 \ --config test/symbolic/echidna.yaml Needs the abstraction-enabled echidna (hevm pin bumped + abstractArith forced in the verification path) on PATH; that wiring lands in echidna once the hevm PR merges. symExecMaxExplore is raised from the default of 10 so the deployment + setUp + conversion path explores fully. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the 624-line DivModEncoding into three modules, following the EVM.Expr simplifier idiom (pattern-directed rules + per-constructor emit), so the lemmas live in one reviewable file: - EVM.SMT.AbstractBase: shared SMT primitives, signed helpers, the div/mod taxonomy, term collectors/matchers, and `saturate` (the abstract-term closure that was tangled in mulEncoding's `let`). - EVM.SMT.AbstractLemmas: the lemma catalogue and review surface. A `LemmaInst` sum type, `collectLemmas` (triggers), and `emitLemma` (SMT emission + soundness notes), one banner per lemma. GHC exhaustiveness now forces every constructor to have an emitter, replacing the old silent positional concat. - EVM.SMT.DivModEncoding: slimmed to orchestration (mulEncoding) plus the div/mod ground-truth encoding. Behavior-preserving: emitter bodies are verbatim and the emission order is unchanged, so generated queries are identical. Public API unchanged. Validated: lib + exe build clean (GHC 9.6.6); seven representative lemma properties (const-cancel, scaled const-cancel, frac-reduce, nested-div, mul-mono, mul-div-bound, telescope) prove under --abstract-arith, and a false identity correctly fails, so soundness is preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collaborator
Collaborator
Author
|
Actually I have refactored and consolidated here: #1075 lets close this one |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Stacked on #996 (base branch
poc-div-encoding). Extends the arithmeticabstraction from division to multiplication — the missing piece for
verifying vault-style (ERC-4626) properties, where the hard part is the
nonlinear
q·S ≤ q·Acancellation, not the division itself.Symbolic×symbolic
Mulis encoded as an uninterpreted functionabst_evm_bvmul(concrete /0/1/ power-of-two factors stay native), withno ground truth, so the solver never bit-blasts a symbolic product. Meaning
is given by sound lemmas only:
0/1identities (x*0 == 0,x*1 == x)(x/y)*y <= xcondition
x <= 2^128-1 && y <= 2^128-1(so the product fits in 256 bits).This is the width-budget idea Halmos uses; it replaces the exact bound-free
predicate
extract 511 256 (zext x * zext y) = 0, whose 512-bit multiply wasthe dominant solver cost and timed out for large operands.
(x*y)/z,y <= z => (x*y)/z <= x(guarded by no-overflow)x <= y => c*x <= c*yfor a literalc. Constantmultiplies stay native
bvmul, but a symbolic operand times a large constantis costly to bit-blast and compare; since
cis concrete the exact no-overflowbound
floor((2^256-1)/c)is computed at encode time, so the guard is oneconstant comparison.
(c1*x)/c2 == (c1/c2)*xfor literals withc2 | c1(itsspecial case
c1==c2gives(c*x)/c == x). The multiply stays nativebvmulbut the surrounding divide is abstracted, so a precision-scaling wrapper
(
x * 1e18 / 1e18, the identity, or the rescalex * 1e18 / 1e6 == x * 1e12) isnot otherwise provably exact; this pins it, under the encode-time bound
floor((2^256-1)/c1). It fires when the divisor literal divides the multiplierliteral.
(A/c1)/c2 == A/(c1*c2)for literalsc1,c2withc1*c2 < 2^256. Two successive constant divides of an abstracted quotient stayuninterpreted and unrelatable to the single combined divide; this folds them, so
a two-step constant rescale (
A / 1e9 / 1e18 == A / 1e27) is equatable with itsone-step form. The collapsed single divide
A/(c1*c2)is also added to the divset, so the lemmas below (const-cancel, fraction-reduce, telescoping) match code
that splits precision across two divides (e.g.
_getSUsdsValue'sx*rate/1e9/1e18,whose collapsed
x*rate/1e27is what they key on) — not only the one-step form.(c1*x)/c2 == x/(c2/c1)for literals withc1 | c2. The mirrorof const cancellation (which needs
c2 | c1): this is thec1 | c2direction —multiply by a small constant, divide by a large one — such as
x * 1e6 / 1e18 == x / 1e12(precision scaling down, e.g. converting to a lower-decimal token).Sound because, under the same encode-time bound
floor((2^256-1)/c1),c1*xisexact and
floor(c1*x / (c1*k)) = floor(x/k). It fires when the multiplierliteral divides the divisor literal (and
c1 != c2, the const-cancel case).Math.ceilDiv(c1*x, c2)is(c1*x == 0) ? 0 : (c1*x - 1)/c2 + 1, so theabstracted divide is over the
c1*x - 1dividend. Whenc2 | c1the product isalways a multiple of
c2, hence ceil == floor and the whole ceilDiv collapses to(c1/c2)*x; the lemma pins the inner divide — forx >= 1,(c1*x - 1)/c2 == (c1/c2)*x - 1(then+1recovers(c1/c2)*x), under the samefloor((2^256-1)/c1)bound. This discharges round-up precision conversions thatscale to a coarser unit exactly (e.g. an 18→6→18-decimal
ceilDiv(x*1e18, 1e6) == x*1e12).a*bhas a factor reused as adivisor elsewhere, the synthetic exact division
(a*b)/bis added to the divset, letting the lemmas above bridge nested divisions (
(a*b)/b <= aplusdiv-monotonicity across the shared divisor). This is what proves cross-divisor
round-trips like
((x*s)/a)*a/s <= x, and it also discharges Solidity 0.8'sdiv(mul(a,b),a)overflow guards, so checked arithmetic proves as written.(a*b)/c - (a*(b-k))/c == a*(k/c)for a literaldivisor
cand literal stepkwithc | k(no-overflow guarded, andb >= kso the step is a true difference). Every lemma above relates a product to a
constant or to itself; this is the only one that pins the EXACT difference of
two distinct abstract products. It discharges value-change accounting — the
recurring vault-test shape "Δvalue == driver × rate-delta" — e.g.
susds*rate/1e27 - susds == susds*(rate-1e27)/1e27. Sound becausea*b = a*(b-k) + a*kanda*kis an exact multiple ofc, so removing it shiftsthe floor by exactly
a*(k/c).Operand bounding (e.g.
require(x < 2**128)) is supplied in Solidity. Enabled bythe same
--abstract-arithflag as #996.Soundness
hevm stays sound — it is a prover here, never a source of false bugs:
QEDis a real proof: every lemma over-approximates real EVM arithmetic, sounsatunder abstraction impliesunsatin reality.abst_evm_bvmulis uninterpreted, a SAT model may assign productsinconsistent with real multiplication, so a counterexample cannot be trusted.
When the mul abstraction is active a SAT result is downgraded to
Unknownrather than reported. Division-only counterexamples (division is exact via its
ground truth) are unaffected.
*-not-unsoundtests confirmgenuinely-false properties stay
Unknown(never a spuriousQED) and the*-cex-preservedtests confirm real counterexamples are still reported.Properties it proves
Monotonicity of conversion functions (more in ⇒ at least as much out; a higher
backing ratio ⇒ at least as much value), divisor ordering, mulDiv bounds, and
cross-divisor round-trips (
convertToAssets(convertToShares(x)) <= xand itsreverse — value in/out cannot increase across a round-trip). The const-cancel and
nested-division-collapse lemmas additionally prove exact-value constant-rescale
identities (e.g. a
x * 1e18 / 1e6precision conversion equalsx * 1e12) — aqualitative step past the relational facts above, since an abstracted divide
otherwise leaves the quotient uninterpreted. All are
unknownnatively (thenested symbolic products bit-blast) and
QEDwith--abstract-arith.Example
A minimal ERC-4626 vault, ordinary checked arithmetic. Share-price monotonicity
and the round-trip both prove
QEDwith--abstract-arith(andunknownnatively):
Use bitwuzla (the bit-vector solver hevm defaults to): it discharges the
checked-arithmetic overflow guards that z3 struggles with on the deepest nested
round-trips. Both solvers are sound.
Verification status
Every property's result (QED / Unknown / CEX) was confirmed through the
hevmCLI locally (bitwuzla 0.8.2). The test group
Mul-Abstractionruns in CI.Known limitation: undefined symbol with abstracted arithmetic over storage-resolved values
The abstraction lemmas (
assertPropsAbstract) are encoded over the raw propsps,whereas SMT variables are declared from the eliminated props
(
eliminateProps . concKeccak[Simp] . [decompose] $ ps). When a calldata valuereaches the abstracted arithmetic through storage — e.g. written into a mapping slot
via
vm.store, or read back across a contract boundary — the lemma encoder(
exprToSMTWith) resolves the read to the calldata symbol, but thevariable-declaration pass (
referencedVarsover the eliminated props) represents itdifferently and never declares it. The emitted lemma then mentions an undeclared
symbol and the solver errors:
This is not unsoundness — the solver errors out and hevm reports the property as
unknown/ partially-explored, never a spuriousQED. But it turns some provableproperties into false negatives.
Observed on PSM3's
previewWithdrawunder--abstract-arith: avm.store'dshares[msg.sender]mapping read, plus atotalAssetsformed as a sum ofcross-contract balance reads, feeding a general-branch round-up division. It does
not reduce to a small standalone contract — a single mapping store, a cross-contract
read, and the round-up
ceilDiveach prove fine in isolation — so the divergencedepends on the specific elimination/decomposition of the combined query.
Partial fix direction: encoding the abstraction lemmas over the same eliminated
props the declarations use removes the
vm.store-into-mapping class of this; adeeper case remains for cross-contract balance reads that
exprToSMTWithresolvesonly inside nested arithmetic, where
referencedVarsstill does not declare thesymbol. Left as a known limitation for now since it yields
unknown, not a wronganswer.
Checklist
Mul-Abstraction)