Skip to content

Savings donation-attack guard, audits, and multi-chain upgrade deployment - #32

Merged
FabienCoutant merged 118 commits into
mainfrom
dev
Jul 30, 2026
Merged

FabienCoutant merged 118 commits into
mainfrom
dev

Conversation

@FabienCoutant

Copy link
Copy Markdown
Contributor

Overview

Hardens the Savings vault against the dormant-accrual donation attack, refactors pausing into explicit calls, migrates the deploy toolchain to rocketh 0.19, and records the upgrade deployment across every production chain. The Savings changes carry both the Cyfrin and Bailsec remediations plus a Certora formal verification pass.

Savings donation-attack guard

  • storedAssets now tracks the real backing, and totalAssets() projects storedAssets instead of the raw balanceOf, so a direct token transfer can no longer feed the rate-multiplied accrual.
  • recoverSurplus(to) lets governance sweep untracked surplus (scoped to asset()), which stays out of totalAssets().
  • initializeStoredAssets() (reinitializer(2), restricted) seeds the tracked balance on upgrade, bounded by a lastUpdate freshness window.

Pause refactor

  • togglePause is split into explicit pause() / unpause() on both the Savings vault and the Parallelizer, with AlreadyPaused / NotPaused guards.
  • Pausing genuinely halts emission: pause() settles yield up to the pause moment, unpause() drops the paused interval instead of accruing it.

Audit remediation

  • Cyfrin (all Resolved / Acknowledged): pre-upgrade donation staleness guard (L-1), pause halting yield (L-2), seeding access control (I-3), first accrual window bound (I-4), plus operational guidance on decommission ordering and never registering Savings as a surplus payee.
  • Bailsec (Issues 01 to 07, all Resolved / Acknowledged): ungranted seeding selector, lastUpdate re-anchoring, paused totalAssets(), paused-aware accrual in the rate setters, ERC4626 max* checks reintroduced across the standard and EIP-3009 entry points, and an onlyInitialized guard against a non-atomic upgrade.
  • Formal verification: the Cyfrin Certora report covering the Parallelizer facets, the Savings vault, and FlashParallelToken. All 490 rule runs verify on the mitigated code, including the three donation properties that fail on the pre-fix code.

New surface

  • getSurplusBufferRatio() exposes the surplus buffer ratio, which was the only surplus storage field without a getter.
  • processSurplus and release move to a dedicated keeper role, so routine surplus jobs no longer require governor rights.

Toolchain

  • Migrated the rocketh family to 0.19 and hardhat-deploy to 2 (with hardhat 3.9), switching the deploy setup from setup to setupDeployScripts and adapting to the new per-contract artifact layout.
  • Disabled viaIR so the Swapper facet stays under the EIP-170 contract size limit.

Deployments

  • Records the facet and Savings implementation deployment on Sonic, Base, Avalanche, Ethereum mainnet, and HyperEVM, including the Surplus facet, which was absent from production since the original deployment.
  • On mainnet the Savings records point at the fresh sUSDp vault (proxy 0xd3a452b3, implementation 0xd256379d) that replaces the previous one.

Notes

  • Multisig wiring (the diamondCut, role setup, surplus config, and the Savings upgradeToAndCall) is executed separately by governance and is not part of this branch.

FabienCoutant and others added 30 commits February 25, 2026 17:36
…ation, silently disabling burn ratio protection
…ct-out mint and burn for certain collateral decimals
…ost-check causes Surplus::processSurplus(collateralAddress,0) DoS
…z test

OpenZeppelin v5 uses custom errors (SafeCastOverflowedUintDowncast)
instead of string reverts. Use generic vm.expectRevert() and return
early to avoid asserting on reverted call results.
Prevents CI from resolving a newer OZ version where
__UUPSUpgradeable_init was removed, breaking the build.
… unset ratio

Removed the check for surplusBufferRatio being zero in Surplus.sol and added a new error SurplusBufferRatioNotSet in Errors.sol. Updated LibSurplus to use the new error and adjusted tests to reflect these changes, ensuring proper handling of surplus calculations.
…nd streamline loops for improved clarity and efficiency
…ning all stables

Ceil rounding in getCollateralRatio could return stablecoinsIssued > totalSupply,
allowing users to bypass the CannotBurnAllStableIssued check in the Redeemer.
Floor ensures amountBurnt >= stablecoinsIssued always triggers the safety check.
Ceil is kept locally as divisor for collatRatio computation (conservative).
…leIssued

- Fix Burn test to use >= comparison matching contract Floor rounding logic
- Add quoteIn check to skip no-op swaps (amountOut == 0 from oracle rounding)
- Add BurningAllStableIssued tests with manager and whitelist for Burn
- Add BurningAllStableIssued tests with whitelist and manager+whitelist for Redeem
- Add RedeemWithForfeit BurningAllStableIssued test without manager
- Add RedeemWithForfeit BurningAllStableIssued test with whitelist
- Update MultiRedemptionCurve test to use getTotalIssued() (Floor) for revert checks
…commendation

Replace stablecoinsIssued check with normalizedStables guard in both
_quoteRedemptionCurve and _updateNormalizer to prevent the fee bypass
attack via the redemption path (renormalization truncation to zero).
FabienCoutant and others added 29 commits June 5, 2026 12:12
…savings-donation

Cyfrin Savings Fix — audit remediation (L-1, L-2, I-3, I-4)
…-savings-donation

Bailsec Parallel Savings Fix — audit remediation (Issues 01-05, 07)
…nd-pause-refactor

feat: savings donation-attack guard + parallelizer pause refactor
Bumps the rocketh family to 0.19, hardhat-deploy to 2.0.9 and hardhat to 3.9.1,
which the new hardhat-deploy requires.

The 0.19 API replaces `setup` with `setupDeployScripts` and moves `UserConfig`
to `rocketh/types`. hardhat-deploy 2 also writes one artifact module per
contract under generated/artifacts/ with named exports, so the default import
no longer resolves and is replaced by a namespace import on the index.
Compiling with viaIR pushed Swapper to 27239 bytes, above the 24576 byte
contract size limit, which blocked deployment. Turning it off brings the facet
down to 19109 bytes and aligns hardhat with the foundry profiles, which already
set via_ir to false.
processSurplus and release were held by the governor, which forced the multisig
to run what are routine keeper jobs. They now sit behind a dedicated KEEPER_ROLE
so they can be automated without handing out governor rights.

Adds KEEPER_ROLE to the constants, splits the two selectors out of the governor
batch in SetParallelizerRoles, and updates the test access manager config and
the fixtures that wire selector roles.
surplusBufferRatio was the only surplus storage field without a getter, so its
value could only be read from raw storage even though it gates surplus
extraction. Adds getSurplusBufferRatio alongside the other surplus getters.

Also covers the setter itself, which had no dedicated test: the InvalidParam
guard below BASE_9, the emitted event, the value at the BASE_9 boundary and the
access control check.
…ementation

Adds two scripts that only deploy code and never execute privileged calls, since
the wiring is done by the multisig.

PrepareParallelizerUpgrade deploys the changed facets, diffs their selectors
against the on chain loupe and prints the resulting diamondCut.
DeploySavingsImplementation deploys the implementation and prints the
upgradeToAndCall payload.
hardhat-deploy 2 writes generated/artifacts/ and generated/abis/, and no longer
regenerates generated/artifacts.ts nor generated/types/. Both were left behind
by the previous format, unreferenced and frozen at their last compile.

artifacts.ts was the more harmful of the two: rocketh resolved its default
export ahead of the new directory, so deployments would have silently used stale
bytecode. Verified by recompiling from a clean tree, where neither path is
recreated.
Records the facets and the savings implementation deployed on Sonic, including
the Surplus facet, which was missing from production since the original
deployment. The .chainId marker is replaced by .chain, the format rocketh 0.19
writes.
Keeps the abis emitted by the compile in the repository, as the record of what
the deployment was built from.
…gs fix

Certora engagement covering the Parallelizer facets, the Savings vault and
FlashParallelToken, verified at mitigation commit 7c24bc4. All 490 rule runs
verify, including the three donation properties that fail on the pre-fix code
and confirm the storedAssets remediation.
Records the facets and the savings implementation deployed on Base, including
the Surplus facet, which was missing from production since the original
deployment. The .chainId marker is replaced by .chain, the format rocketh 0.19
writes.
Records the facets and the savings implementation deployed on Avalanche,
including the Surplus facet, which was missing from production since the
original deployment.
Records the facets and the savings implementation deployed on Ethereum mainnet,
including the Surplus facet, which was missing from production since the
original deployment. The .chainId marker is replaced by .chain, the format
rocketh 0.19 writes.
Records the facets and the savings implementation deployed on HyperEVM,
including the Surplus facet, which was missing from production since the
original deployment. The .chainId marker is replaced by .chain, the format
rocketh 0.19 writes.
@FabienCoutant
FabienCoutant merged commit 28e1933 into main Jul 30, 2026
2 checks passed
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.

1 participant