Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save lucas-manuel/6eb120035e5ad558ccf03430b1c23fa5 to your computer and use it in GitHub Desktop.

Select an option

Save lucas-manuel/6eb120035e5ad558ccf03430b1c23fa5 to your computer and use it in GitHub Desktop.
SparkLend Rounding Release — Security Notes (dev @ 900c189f, diff base v1.0.0)

SparkLend Rounding Release — Security Notes

Design summary, known issues, and deployment constraints for the SparkLend rounding release. Prepared by Phoenix Labs as supplemental material for the release and its external audit. Version of 2026-09-02.

Scope

  • Repository: sparkdotfi/sparklend-v1-core
  • Release candidate: branch dev, commit 900c189f
  • Diff base: tag v1.0.0, commit 8120e495 (the code deployed behind the live proxies)

Production files changed (git diff --name-status v1.0.0..dev, excluding mocks, interfaces, and test suites): WadRayMath, DataTypes, Pool, the six logic libraries (BorrowLogic, GenericLogic, LiquidationLogic, PoolLogic, SupplyLogic, ValidationLogic), the three tokens (AToken, VariableDebtToken, StableDebtToken), and the token bases (DebtTokenBase, IncentivizedERC20; MintableIncentivizedERC20 and ScaledBalanceTokenBase deleted, MintableScaledBalanceToken added). No other production contract changed.

What Changed and Why

SparkLend is a fork of older Aave V3 code that used half-up rounding for ray arithmetic, so conversions between asset amounts and scaled balances could round in favor of the user or the protocol depending on the reserve index and amount. For high-value, low-decimal assets, repeated operations could turn atomic-unit user-favoring rounding into an extractable loop.

The release is a narrow backport of the protocol-favoring rounding directions introduced in Aave v3.5, together with Aave v3.6's correction to delegated variable-debt allowance accounting. Every scaled-accounting boundary now rounds deterministically against the user:

Operation Direction Safety property
aToken mint / supply Down No more claim value credited than supplied.
aToken burn / withdraw / transfer Up Enough scaled balance burned for the amount moved.
aToken balance and total supply Down Indexed aToken amounts never rounded above exact value.
Variable debt mint / borrow Up Recorded debt never smaller than the amount borrowed.
Variable debt burn / repay Down Repayment never erases more scaled debt than it covers.
Variable debt balance and total supply Up Debt never rounded below its exact indexed obligation.
Collateral base-currency conversion Down Account collateral not overstated.
Aggregate debt base-currency conversion Up Positive debt dust never discarded.

WadRayMath gains rayMulFloor / rayMulCeil / rayDivFloor / rayDivCeil; the shared mint/burn path (MintableScaledBalanceToken) takes an explicit RoundingMode and fails closed. transferFrom and delegated borrowing charge allowances by the actual indexed balance change (capped at the allowance, preserving exact-request compatibility). The legacy half-up rayMul/rayDiv remain for protocol math outside these boundaries. Later follow-ups in the same release add unconditional post-action health-factor checks to executeBorrow and the useATokens repay branch (SC-1650 mitigation, #23/#24), scaled-balance-based collateral-flag clearing on the useATokens repay path (#27/#29), the missing ReserveUsedAsCollateralDisabled event (#28), and a zero-mint guard in executeMintToTreasury.

Upgrade Manifest and Storage-Layout Safety

Contract Live revision Release revision Ships
Pool 0x4 0x5 Yes
AToken 0x1 0x2 Yes
VariableDebtToken 0x1 0x2 Yes
StableDebtToken 0x1 0x1 (unchanged) No — not upgraded

DelegationAwareAToken inherits AToken unchanged and carries the same ATOKEN_REVISION. StableDebtToken source changed in this branch but its implementation will not be deployed; live proxies keep the v1.0.0 stable-debt code. (If it is ever upgraded, the revision must be bumped first — VersionedInitializable rejects a revision that does not increase.)

Storage safety: forge inspect ... storage (solc 0.8.10) over the v1.0.0 and dev sources shows byte-identical layouts — slots, intra-slot offsets, and widths — for Pool, AToken, DelegationAwareAToken, VariableDebtToken, and StableDebtToken. The deleted and added base contracts declare no storage; the _allowances visibility change is layout-neutral. The in-place upgrade is slot-safe.

Deployment Configuration Assumptions (Ethereum mainnet, verified August 2026)

Several accepted findings below are gated on configuration rather than code. These are facts to re-verify per deployment, not code invariants:

  • Stable-rate borrowing is disabled on every listed reserve; the stable-debt paths are dormant.
  • FLASHLOAN_PREMIUM_TOTAL is 0, so flash-loan premium index inflation (Certora I-03) is inert.
  • The only reserve with a live debt ceiling is GNO, and that reserve is frozen with LTV 0.
  • Every listed reserve keeps ≥ 100 bps between liquidation threshold and LTV, so the exact-capacity borrow boundary below is unreachable at current parameters.

Known Issues and Accepted Risks

Each item has been analyzed and either accepted or scoped out; none is an unknown. Reviewers who reach these are confirming known ground.

Ghost collateral flag (Certora L-02, acknowledged). Withdraw, aToken transfer, and liquidation clear the collateral flag by comparing rebased amounts, while burns/transfers round scaled units up — so a near-full operation can zero the scaled balance with the flag still set. The liquidation variant is third-party-triggerable via debtToCover. The state is operational (no fund loss across five independent reviews), self-clearable via setUserUseReserveAsCollateral(asset, false), and also reachable under the pre-change code. The useATokens repay branch already uses the correct scaled-balance check. Accepted consequences: a stale bit on an LTV-0 asset sets hasZeroLtvCollateral on a zero balance; a stale bit plus a real one defeats single-bit isolation detection if a debt ceiling is later set; a stale bit survives dropReserve and re-binds to the next asset listed at the reused id. Governance mitigations are listed under Deployment Constraints.

Stranded isolation-debt counter. A full-collateral, partial-debt liquidation of an isolated position clears the isolation flag while debt remains, so later repays no longer decrement isolationModeTotalDebt; the residue consumes ceiling capacity until governance zeroes the ceiling and resets the counter. Accepted (no isolated reserve is active; see configuration above).

Treasury mint reconciliation. executeMintToTreasury converts the scaled accrual to a rebased amount (half-up) and back (floor), so a call can credit the treasury one scaled unit less than the accrual it zeroes; a zero-mint guard skips the total-loss case. Bounded at one scaled unit per call; accepted as negligible.

Absolute post-action health-factor threshold. The new checks on executeBorrow and the useATokens repay enforce HF ≥ 1, not no-worse-than-before. An underwater holder repaying their own collateral asset with aTokens is blocked even when the repay improves the position (accepted as an edge case; the underlying-repay path stays open), and repayWithATokens now reads a price for every asset in the user's configuration.

Exact-capacity borrow boundary. validateBorrow values the request with floor conversion while the post-mint health check values debt with ceil, so a borrow at the exact LTV limit reverts wherever liquidationThreshold == ltv. Unreachable at current parameters (all gaps ≥ 100 bps); pinned by intentionally strict tests.

Dust and rounding residue (all bounded, accepted). Supply/repay/liquidation amounts below one scaled unit pass validation and revert in the token; dust positions resist liquidation until interest grows them. Allowance consumption equals the actual indexed balance change and can exceed the nominal amount by up to one scaled unit per call (never under-consumes; exact-total integrators should approve with headroom). transferOnLiquidation can move zero shares for a dust value while events report the rebased amount. Directional rounding also means a supply-then-withdraw of the exact opening amount can revert (Certora I-04) — integrators should withdraw with the max sentinel. Legacy Mint/Burn/Transfer event values remain request-based and can differ from exact balance deltas by 1 wei; the aToken ABI now includes the inherited ERC20InsufficientAllowance error.

Dormant and inherited paths. Stable-debt internals (half-up balanceOf in the health factor; burn supply-zeroing branches returning a stale next supply) are deprecated, disabled on all markets, and intentionally unchanged. supplyWithPermit/repayWithPermit do not wrap permit in try/catch, so a front-run signature replay reverts the call (resubmit with plain approve). Pool.supply accepts onBehalfOf = address(0), minting unburnable aTokens that block a later dropReserve. Isolation-mode ceiling accounting truncates to 2 decimals on both sides, and its normalization underflows for assets with fewer than 2 decimals — both inherited from upstream Aave v3 and gated on listing decisions. Certora acknowledgements I-01–I-04 stand as published.

Formal-verification status. certora/specs/{AToken,VariableDebtToken}.spec still model the legacy half-up arithmetic; they are stale and must not be cited as evidence about the shipped code until revised.

Deployment Constraints

  1. Atomic upgrade only — Pool and every token implementation in one transaction. A window with new aTokens and the old Pool makes withdraw(max) revert for roughly half of all holders at live indices; the reverse mixes rounding families in one flow.
  2. Per-reserve initialize parameters — the upgrade re-runs initialize, overwriting treasury and incentives parameters with no equality check, and reserve treasuries are not uniform (spDAI differs). Read each proxy's current parameters and pass them back explicitly.
  3. Offboarding pre-flightdropReserve requires zero supplies and zero accruedToTreasury; known blockers include stuck one-unit treasury accrual, dust shares from aToken-path liquidations, and zero-address-held aTokens. Before dropping or re-listing into a vacated id, sweep stale collateral bits from ReserveUsedAsCollateralEnabled events.
  4. Listing checklist — no assets with fewer than 2 decimals; treat low-decimal assets as high-risk for the dust-revert family; never set an eMode category priceSource; keep FLASHLOAN_PREMIUM_TOTAL at 0 unless Certora I-03 is revisited.

Prior Reviews and Reproduction

  • Certora, "SparkLend Rounding Mitigation" (final report, August 2026) — 3 Low / 4 Informational / 0 Critical-High-Medium; L-01 fixed and confirmed, L-02/L-03 acknowledged as documented above.
  • ChainSecurity, "SparkLend Core Updates" and the upstream Aave V3 audit history — audits/ in the repository.
  • Internal adversarial reviews (July–September 2026) — five multi-agent passes including a three-pass scan of v1.0.0..dev at 900c189f; all confirmed findings are reflected above.
  • Reproduction: Solidity 0.8.10 (London target), Node ≥ 16, npm run test for the in-repo Hardhat suite; integration/fuzz/invariant coverage lives in sparkdotfi/sparklend-testing (Foundry), with pre-audit hygiene tracked in sparklend-testing#47.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment