Quick note before anything else: I verified every fact here against live mainnet state and the repo history, not just by reading source. The Foundry test that proves the math is included (poc/test/RoundingProof.t.sol, 3 passing tests). I also checked the repo PRs - this exact issue has PRs dated Apr-Jul 2026, so it may already be known to the team; I am filing it against the DEPLOYED mainnet contract, which is still running the vulnerable code as of today.
Full evidence is in the gist.
SparkLend is an Aave V3 fork. Its deployed token math uses Aave's legacy "half-up" rounding
in WadRayMath.rayMul / rayDiv (adds HALF_RAY before dividing). At a fractional
liquidity index, a user can pick deposit/withdraw (or borrow/repay) amounts that sit on the
half-up boundary and get credited with more scaled units than a fair share - or burn fewer
scaled units than the value they receive. Repeated supply/withdraw or borrow/repay cycles
turn that per-operation rounding delta into free value. The higher the asset value per
smallest unit (low-decimal, high-value assets like WBTC/cbBTC at 8 decimals, USDC/USDT at 6),
the more each rounding step is worth.
The team fixed this internally: commit 52c367b8 ("Fix: Rounding Issue (SC-1569)",
2026-07-28) backports Aave v3.5/v3.6 protocol-favoring rounding (rayMulFloor,
rayMulCeil, rayDivFloor, rayDivCeil) plus a delegated variable-debt allowance fix,
documented in ROUNDING_MITIGATION_REPORT.md. The fix is in the repo but NOT deployed.
- SparkLend mainnet pool: 0xC13e21B648A5Ee794902342038FF3aDAB66BE987
- Pool implementation (EIP-1967 slot): 0x5aE329203E00f76891094DcfedD5Aca082a50e1b
- That implementation was deployed/upgraded 2024-04-08 (sparklend-deployments script/output/1/primary-pool-20240408.json; current primary-pool-latest.json still points to 0x5aE32920). No pool upgrade since.
- The deployed WadRayMath (recovered from Sourcify, exact_match, solc 0.8.10) contains only
the legacy
rayMul/rayDivwithHALF_RAY- there is norayMulFloor/rayMulCeil. - The deployed ScaledBalanceTokenBase
_mintScaled/_burnScaleduseamount.rayDiv(index)(half-up). The deployed VariableDebtToken.mint uses_mintScaledand consumes only the nominalamountfrom the borrow allowance. - The deployed GenericLogic still uses
userTotalDebt.rayMul(...)(line 232) and collateralrayMul(...)(line 262) - the exact lines the fix changes torayMulCeil/rayMulFloor.
The repo has a branch feat/rounding-exploit-poc (commit 3d87849b, 2026-05-05) with
test-suites/rounding-exploit.spec.ts. It documents two attacks:
-
Deposit/withdraw cycle: inflate the liquidity index (donate so index grows), then
- depositAmount = index/(2*RAY) + 1 -> mints exactly 1 scaled unit (half-up rayDiv)
- withdrawAmount = 3index/(2RAY) - 1 -> burns exactly 1 scaled unit
The minted unit is worth
1 * index(more than the deposit); the withdrawal returns the larger amount while burning the same 1 unit. Free value per cycle.
-
Borrow/repay cycle: with a matured borrow index,
- borrowAmount = 3index/(2RAY) - 1 -> mints exactly 1 debt scaled unit
- repayAmount = index/(2*RAY) + 1 -> burns exactly 1 debt scaled unit The borrower receives more than they record as debt, then erases it with a small repay.
The fix's tests assert profit == 0 on the patched code; my reproduction test asserts the
deployed half-up math still produces profit > 0.
poc/test/RoundingProof.t.sol - 3 passing tests reproducing the deployed half-up math and showing the extraction boundary:
index: 5000139738244916552189612774
depositAmount: 3
scaled minted (half-up, deployed): 1
scaled minted (floor, fixed): 0
value of 1 scaled unit (half-up rayMul): 5
extractable per deposit (value - deposit): 2
deposit: 3 scaled: 1
withdraw: 6 scaled: 1
free value extracted per cycle: 3
withdrawAmount: 6
scaled burned (half-up, deployed): 1
Run:
forge test --match-path test/RoundingProof.t.sol -vvv
All 3 tests pass.
- The bug is a value-extraction (theft of protocol funds) via rounding, not a one-shot freeze.
- Exploitability depends on the reserve liquidity index having a fractional part and on the attacker being able to inflate it (donation) and/or time it. WBTC's live normalized income is ~1.000139738e27 (fractional part present), so the boundary condition exists today.
- The team's PoC uses USDC (6 decimals) as the victim asset - low-decimal assets maximize the per-operation value of one rounded unit.
- I did NOT run a full end-to-end extraction against mainnet (that would require a large whale supply and index inflation and is what the team's own PoC demonstrates). The math proof is solid and matches the team's internal reproduction.
- Severity per the program: repeated rounding extraction on high-value low-decimal assets is in the "Direct theft of user funds" / "Protocol insolvency" class. The exact reward depends on the funds-at-risk figure the team computes at submission time.
- The fix and the ROUNDING_MITIGATION_REPORT.md are PUBLIC in the repo (committed 2026-07-28), and there are multiple PRs (7-13) dating back to 2026-04 documenting the rounding fix.
- This strongly suggests the issue was already known to the team internally (tickets DEV-1596 / SC-1569) and may have been self-reported to the bounty.
- Per the program's Known Issue Assurance, Spark must disclose known issues publicly or via self-report. I could not see Immunefi's private submissions DB, so I cannot rule out that this is already a known/paid issue.
- What IS reportable regardless: the deployed mainnet pool (0x5aE32920) is STILL running the vulnerable half-up code - the fix has not been deployed. The program rule explicitly says a finding must exist in the deployed contract, and this one does.
- I am not claiming the fix was maliciously withheld.
- I am not claiming a specific dollar figure of currently-extractable funds; the real number depends on live reserve indices and the attacker's ability to inflate them.
- I am not claiming this is a novel-to-the-world bug; I am claiming it is live on the deployed mainnet contract today, which is what the program pays on.
- Deploy the committed rounding fix (commit 52c367b8) to the mainnet pool implementation, including the aToken / variableDebtToken implementation upgrade.
- Add the max-withdraw alignment and the collateral-flag follow-ups flagged in ROUNDING_MITIGATION_REPORT.md as still-pending.
- Consider whether the half-up rounding in any other live SparkLend contract (Gnosis, Base, Arbitrum, Optimism, Unichain instances) still allows the same extraction.
- SparkLend mainnet pool: 0xC13e21B648A5Ee794902342038FF3aDAB66BE987
- Pool implementation (live): 0x5aE329203E00f76891094DcfedD5Aca082a50e1b (deployed 2024-04-08)
- Fix commit: sparklend-v1-core@52c367b8 (2026-07-28) + ROUNDING_MITIGATION_REPORT.md
- Team's internal PoC branch: feat/rounding-exploit-poc@3d87849b (2026-05-05)
- Deployed sources (Sourcify exact_match): WadRayMath.sol, ScaledBalanceTokenBase.sol, VariableDebtToken.sol, GenericLogic.sol, LiquidationLogic.sol
- PoC: poc/test/RoundingProof.t.sol (3 passing tests)