Skip to content

audit(sPRL2V2): Bailsec June 2026 remediation (Bailsec-02/03/05/06) - #13

Merged
FabienCoutant merged 6 commits into
feat/sprl2v2from
audit/bailsec-june-2026-sPRL2v2
Jun 16, 2026
Merged

FabienCoutant merged 6 commits into
feat/sprl2v2from
audit/bailsec-june-2026-sPRL2v2

Conversation

@FabienCoutant

@FabienCoutant FabienCoutant commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Audit remediation for sPRL2V2 following the Bailsec sPRLV2 1st report (June 2026). Targets feat/sprl2v2.

Report: docs/audits/Bailsec - Parallel - sPRL2V2 - 1st Report - June 2026.pdf

Findings: 0 High · 2 Medium · 5 Low · 9 Informational. The four findings below are fixed; the remainder are acknowledged (see table).

Audit remediation

Code changes addressing the report's findings:

  • [Issue_02] Duplicate symbol during migration (Medium) — both sPRL2 and sPRL2V2 exposed the same sPRL2 symbol while both tokens are live during the migration window, risking conflation by any symbol-keyed consumer (explorers, the reward-merkle distributor's off-chain eligibility input). The SYMBOL constant is now sPRL2V2. NAME already differed from the legacy ...Aura Deposit Vault, so it is left untouched.
  • [Issue_03] Extra rewards cannot be withdrawn (Medium) — reward tokens routed to the contract on its behalf (e.g. MERKL onBehalf distributions) would be permanently locked. Adds a restricted recoverERC20(token) that forwards the full balance to feeReceiver. The guard reverts with CannotRecoverUnderlying() when token == underlying, so the share-backing asset (BPT for sPRL2V2, PRL for sPRL1) can never be drained. Placed in the shared TimeLockPenaltyERC20 base keyed on underlying rather than a contract-specific BPT check, so every derived staking token benefits.
  • [Issue_05] requestWithdraw can be invoked with zero amount (Low) — requestWithdraw(0) minted an empty, cancellable request occupying a request ID. Now reverts with NullAmount(). Guard added to the shared base.
  • [Issue_06] feeReceiver coupled exits can produce failure states (Low) — an address(0) feeReceiver would revert the slash transfer in every premature exit, DoS-ing all early withdrawals. Adds a zero-address guard (FeeReceiverZeroAddress()) in both the base constructor and updateFeeReceiver.

Acknowledged (no code change)

ID Sev Title Rationale
Issue_01 Low Shared nonce (permit / delegateBySig) Inherent to OZ ERC20Votes + ERC20Permit; recoverable, no fund loss. Document for users.
Issue_04 Low Missing native ETH exit Users unwrap WETH→ETH themselves; keeps the contract surface minimal.
Issue_07 Low Min amounts calibrated on post-fee amount Slippage params are the user's responsibility; documentation note.
Issue_08 Info Balancer V3 minimum trade amount on exit Atomic revert, no state corruption.
Issue_09 Info Pending sPRL2 requests bypass waiting period Expected during the paused migration; note only.
Issue_10 Info balanceOf manipulable via request/cancel No governance logic relies on raw balanceOf.
Issue_11 Info Voting power denomination fluctuates Inherent to BPT-backed VP; note only.
Issue_12 Info Address-derived isReversedBalancerPair Current mainnet deployment order matches; slippage params guard.
Issue_13 Info findUnlockingIDs empty array on start-from-end Off-chain helper only; callers use _startFromEnd=false.
Issue_14 Info uint160 cast truncation Supply never reaches 2^160; consistency note.
Issue_15 Info Theoretical uint208 supply cap Atomic revert; no corruption.
Issue_16 Info Migrated deposits attributed to migrator in event Cosmetic; document the migration flow.

Tests

  • New: recoverERC20 (recover to feeReceiver, revert on underlying, revert on unauthorized) in the timeLockPenaltyERC20 suite (matching the base contract).
  • New: zero-amount requestWithdraw revert, zero-address feeReceiver revert (constructor + updateFeeReceiver), updated symbol assertion.
  • Full CI suite green locally (lint + forge build --sizes + forge test): 188 passed, 0 failed (fuzz 1000 runs, forks included). sPRL2V2 runtime size 21,423 < 24,576.

Note

PR intended for audit review — do not merge until the audit is finalized.

During the migration window both sPRL2 and sPRL2V2 are live and
shared the same "sPRL2" symbol. Any downstream consumer keying on
the token symbol (explorers, the reward-merkle distributor's
off-chain eligibility input, dashboards) could conflate the two
tokens, excluding or double-counting balances.

Renames the SYMBOL constant to "sPRL2V2" so the new token is
unambiguously distinguishable on-chain. NAME already differed from
the legacy "...Aura Deposit Vault" so it is left untouched.

The constructor integration test asserts the new symbol.
… requests

requestWithdraw could be called with amount = 0, minting an empty
WithdrawalRequest that occupies a request ID and can even be
cancelled. It serves no purpose in normal operation and only adds
tail risk / a precondition for other invalid states.

Reverts with NullAmount() when the unlocking amount is zero. The
guard lives in the shared base so sPRL1/sPRL2/sPRL2V2 all inherit it.

Adds an integration test asserting the revert.
… zero address

Premature exits transfer the BPT slash to feeReceiver inside the
withdrawal flow. If feeReceiver were address(0), every early
withdrawPRLAndWeth / withdrawBPT would revert on the slash transfer,
DoS-ing all premature withdrawals until governance fixes the address.

Adds a zero-address check both in the base constructor and in
updateFeeReceiver so the fee receiver can never be set to address(0)
at deploy time or via governance. Reverts with FeeReceiverZeroAddress().

Adds constructor and updateFeeReceiver revert tests.
…le tokens

The BPT backing sPRL2V2 sits idle in the contract, so reward tokens
claimed on the contract's behalf (e.g. MERKL distributions routed
onBehalf to the holder) would otherwise be permanently locked with
no way to retrieve them.

Adds a restricted recoverERC20(token) that forwards the full balance
of an arbitrary token to feeReceiver. The guard reverts with
CannotRecoverUnderlying() when token == underlying, so the share-
backing asset (BPT for sPRL2V2, PRL for sPRL1) can never be drained.
Placed in the shared base keyed on `underlying` rather than a
contract-specific BPT check, so every derived staking token benefits.

Adds integration tests in the timeLockPenaltyERC20 suite (matching the
base contract): successful recovery to feeReceiver, revert on the
underlying token, and revert for an unauthorized caller.
@FabienCoutant
FabienCoutant merged commit ab6a745 into feat/sprl2v2 Jun 16, 2026
3 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