Skip to content

[PoC] Abstract symbolic multiplication (abst_evm_bvmul) for vault-style verification - #1073

Closed
gustavo-grieco wants to merge 14 commits into
poc-div-encodingfrom
poc-mul-abstraction
Closed

[PoC] Abstract symbolic multiplication (abst_evm_bvmul) for vault-style verification#1073
gustavo-grieco wants to merge 14 commits into
poc-div-encodingfrom
poc-mul-abstraction

Conversation

@gustavo-grieco

@gustavo-grieco gustavo-grieco commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Description

Stacked on #996 (base branch poc-div-encoding). Extends the arithmetic
abstraction from division to multiplication — the missing piece for
verifying vault-style (ERC-4626) properties, where the hard part is the
nonlinear q·S ≤ q·A cancellation, not the division itself.

Symbolic×symbolic Mul is encoded as an uninterpreted function
abst_evm_bvmul (concrete / 0 / 1 / power-of-two factors stay native), with
no ground truth, so the solver never bit-blasts a symbolic product. Meaning
is given by sound lemmas only:

  • commutativity, and 0/1 identities (x*0 == 0, x*1 == x)
  • div×mul link: (x/y)*y <= x
  • division monotonicity in the dividend, and anti-monotonicity in the divisor
  • multiplication monotonicity, guarded by a cheap sufficient no-overflow
    condition 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 was
    the dominant solver cost and timed out for large operands.
  • mulDiv bound: for (x*y)/z, y <= z => (x*y)/z <= x (guarded by no-overflow)
  • const-mul monotonicity: x <= y => c*x <= c*y for a literal c. Constant
    multiplies stay native bvmul, but a symbolic operand times a large constant
    is costly to bit-blast and compare; since c is concrete the exact no-overflow
    bound floor((2^256-1)/c) is computed at encode time, so the guard is one
    constant comparison.
  • const cancellation: (c1*x)/c2 == (c1/c2)*x for literals with c2 | c1 (its
    special case c1==c2 gives (c*x)/c == x). The multiply stays native bvmul
    but the surrounding divide is abstracted, so a precision-scaling wrapper
    (x * 1e18 / 1e18, the identity, or the rescale x * 1e18 / 1e6 == x * 1e12) is
    not otherwise provably exact; this pins it, under the encode-time bound
    floor((2^256-1)/c1). It fires when the divisor literal divides the multiplier
    literal.
  • nested-division collapse: (A/c1)/c2 == A/(c1*c2) for literals c1,c2 with
    c1*c2 < 2^256. Two successive constant divides of an abstracted quotient stay
    uninterpreted and unrelatable to the single combined divide; this folds them, so
    a two-step constant rescale (A / 1e9 / 1e18 == A / 1e27) is equatable with its
    one-step form. The collapsed single divide A/(c1*c2) is also added to the div
    set, so the lemmas below (const-cancel, fraction-reduce, telescoping) match code
    that splits precision across two divides (e.g. _getSUsdsValue's x*rate/1e9/1e18,
    whose collapsed x*rate/1e27 is what they key on) — not only the one-step form.
  • fraction-reduce: (c1*x)/c2 == x/(c2/c1) for literals with c1 | c2. The mirror
    of const cancellation (which needs c2 | c1): this is the c1 | c2 direction —
    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*x is
    exact and floor(c1*x / (c1*k)) = floor(x/k). It fires when the multiplier
    literal divides the divisor literal (and c1 != c2, the const-cancel case).
  • ceilDiv-cancel: the round-up 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, hence 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 (then +1 recovers (c1/c2)*x), under the same
    floor((2^256-1)/c1) bound. This discharges round-up precision conversions that
    scale to a coarser unit exactly (e.g. an 18→6→18-decimal ceilDiv(x*1e18, 1e6) == x*1e12).
  • cancellation synthesis: when an abstract product a*b has a factor reused as a
    divisor elsewhere, the synthetic exact division (a*b)/b is added to the div
    set, letting the lemmas above bridge nested divisions ((a*b)/b <= a plus
    div-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's
    div(mul(a,b),a) overflow guards, so checked arithmetic proves as written.
  • scaled-product telescoping: (a*b)/c - (a*(b-k))/c == a*(k/c) for a literal
    divisor c and literal step k with c | k (no-overflow guarded, and b >= k
    so 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 because
    a*b = a*(b-k) + a*k and a*k is an exact multiple of c, so removing it shifts
    the floor by exactly a*(k/c).

Operand bounding (e.g. require(x < 2**128)) is supplied in Solidity. Enabled by
the same --abstract-arith flag as #996.

Soundness

hevm stays sound — it is a prover here, never a source of false bugs:

  • A QED is a real proof: every lemma over-approximates real EVM arithmetic, so
    unsat under abstraction implies unsat in reality.
  • Because abst_evm_bvmul is uninterpreted, a SAT model may assign products
    inconsistent with real multiplication, so a counterexample cannot be trusted.
    When the mul abstraction is active a SAT result is downgraded to Unknown
    rather than reported. Division-only counterexamples (division is exact via its
    ground truth) are unaffected.
  • The lemmas are mutually consistent: the *-not-unsound tests confirm
    genuinely-false properties stay Unknown (never a spurious QED) and the
    *-cex-preserved tests 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)) <= x and its
reverse — 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 / 1e6 precision conversion equals x * 1e12) — a
qualitative step past the relational facts above, since an abstracted divide
otherwise leaves the quotient uninterpreted. All are unknown natively (the
nested symbolic products bit-blast) and QED with --abstract-arith.

Example

A minimal ERC-4626 vault, ordinary checked arithmetic. Share-price monotonicity
and the round-trip both prove QED with --abstract-arith (and unknown
natively):

contract Vault {
    uint256 public totalAssets;
    uint256 public totalShares;

    function convertToShares(uint256 assets) public view returns (uint256) {
        if (totalAssets != 0) return assets * totalShares / totalAssets;
        return assets;
    }
    function convertToAssets(uint256 shares) public view returns (uint256) {
        if (totalShares != 0) return shares * totalAssets / totalShares;
        return shares;
    }

    // more assets in => never fewer shares minted
    function prove_shares_monotonic(uint256 a, uint256 b) external view {
        require(totalAssets != 0);
        require(a < 2**128 && b < 2**128 && totalShares < 2**128);
        require(a <= b);
        assert(convertToShares(a) <= convertToShares(b));
    }

    // value -> shares -> value never creates value
    function prove_roundtrip(uint256 x) external view {
        require(totalAssets != 0 && totalShares != 0);
        require(x < 2**128 && totalShares < 2**128 && totalAssets < 2**128);
        assert(convertToAssets(convertToShares(x)) <= x);
    }
}

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 hevm
CLI locally (bitwuzla 0.8.2). The test group Mul-Abstraction runs in CI.

Known limitation: undefined symbol with abstracted arithmetic over storage-resolved values

The abstraction lemmas (assertPropsAbstract) are encoded over the raw props ps,
whereas SMT variables are declared from the eliminated props
(eliminateProps . concKeccak[Simp] . [decompose] $ ps). When a calldata value
reaches 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 the
variable-declaration pass (referencedVars over the eliminated props) represents it
differently and never declares it. The emitted lemma then mentions an undeclared
symbol and the solver errors:

[error] undefined symbol 'arg2'
while sending: (assert (= ... (abst_evm_bvudiv (bvmul (_ bv1000000000000000000 256) arg2)
                                               (_ bv1000000000000000000 256)) ...))

This is not unsoundness — the solver errors out and hevm reports the property as
unknown / partially-explored, never a spurious QED. But it turns some provable
properties into false negatives.

Observed on PSM3's previewWithdraw under --abstract-arith: a vm.store'd
shares[msg.sender] mapping read, plus a totalAssets formed as a sum of
cross-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 ceilDiv each prove fine in isolation — so the divergence
depends 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; a
deeper case remains for cross-contract balance reads that exprToSMTWith resolves
only inside nested arithmetic, where referencedVars still does not declare the
symbol. Left as a known limitation for now since it yields unknown, not a wrong
answer.

Checklist

  • tested locally
  • added automated tests (group Mul-Abstraction)
  • updated the docs
  • updated the changelog

gustavo-grieco and others added 12 commits June 17, 2026 19:55
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>
@msooseth

Copy link
Copy Markdown
Collaborator

Wow, ok. I should really review and merge this #996 .... damn. Yeah, it's probably not totally stupid to merge #996. I'll have a look.

@gustavo-grieco

Copy link
Copy Markdown
Collaborator Author

Actually I have refactored and consolidated here: #1075 lets close this one

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.

2 participants