Skip to content

V2.1 — Soken audit remediation (APY-2026-06-001)#2

Merged
kimandy7778 merged 4 commits into
mainfrom
feat/v2.1
Jun 25, 2026
Merged

V2.1 — Soken audit remediation (APY-2026-06-001)#2
kimandy7778 merged 4 commits into
mainfrom
feat/v2.1

Conversation

@kimandy7778

Copy link
Copy Markdown
Collaborator

Summary

Soken security audit (APY-2026-06-001, 2026-06-23) found 16 findings on the V2.0
codebase. This PR lands the V2.1 remediation: 9 findings with code changes (1 Critical /
2 Medium / 4 Low / 2 Informational) fixed, 5 Informational additional with docs-only
updates, 4 Informational acknowledged as documented design tradeoffs.

Per-finding mapping (pre-fix root cause → V2.1 fix → new test): see
docs/SOKEN_AUDIT.md.

Commits

  1. 7f34422 feat(v2.1): contracts — Soken APY-2026-06-001 remediation (9 findings)
  2. eacb892 chore(v2.1): scripts — F-04g chain × strategy reward matrix + plumbing
  3. a30190a test(v2.1): hardhat V2.1 spec + sync existing V2 specs to new constructor sigs
  4. 5e3bb50 docs(v2.1): V2_DESIGN + TRUST_MODEL + SECURITY + new SOKEN_AUDIT — Soken remediation

Findings summary

Severity Count Status
Critical 1 ✅ Fixed (F-17)
High 0
Medium 2 ✅ Fixed (F-01, F-02)
Low 4 ✅ Fixed (F-03, F-04, F-06, F-07)
Informational (code-change) 5 ✅ Fixed (F-05, F-15, F-16 docs, plus cross-listed)
Informational (acknowledged) 4 Documented as design tradeoffs (F-08–F-14 subset)

Critical fix highlight — F-17

Soken's Foundry PoC reproduced a permanent vault brick: once totalSupply() returns
to zero (all holders exit), the never-reset lastSharePrice baseline detonates the
FeeTooHigh defensive guard on the next deposit and freezes every entrypoint with no
in-contract recovery.

Fix: reset lastSharePrice = 0 in both _accrue()'s totalSupply()==0 branch
and _withdraw() after the post-burn check. The next deposit re-takes the lazy-init
path and re-snaps the baseline cleanly.

test/v2/Vault.v21.spec.ts reproduces the Soken PoC and asserts the post-fix
behavior — test_f17_freshDepositAfterEmptyVault_doesNotBrick.

Test plan

  • npx hardhat compile — 16 Solidity files OK
  • npx hardhat test — 92 passing (4 fork specs pending without FORK=true)
  • npx hardhat test test/v2/Vault.v21.spec.ts — 14 V2.1-specific specs passing
  • forge test --fuzz-runs 10000 — 10 properties × 10 000 iterations, zero failures
  • (reviewer) confirm finding → fix mapping in docs/SOKEN_AUDIT.md matches
  • (reviewer) confirm V2.0 → V2.1 migration plan in docs/V2_DESIGN.md §6.5

After merge

  1. Soken remediation review on the v2.1.0 tag (this PR's HEAD).
  2. v2.1.0 GitHub release with the SOKEN_AUDIT mapping + remediation evidence.
  3. V2.1 vault redeploy across all 4 chains × tiers (5 vaults).
  4. V2.0 setDepositCap(0) via Multi-sig → halt new V2.0 deposits.
  5. UI prompt: V2.0 holders redeem → V2.1 deposit (voluntary migration).
  6. Treasury V2.0 fee redeem after migration window closes
    (existing batches in safe-batches/v2-prod/).

Synchronizes the V2.1 contract surface from the private dev repo
(apyee-contracts feat/v2.1-soken-remediation) into this audit-public repo.
Imports rewritten to `./interfaces/...` and `./libraries/...` per this repo's
flat-contracts layout.

Behavioral fixes (every code-change Soken finding)
  F-17 Critical — reset lastSharePrice=0 in _accrue + _withdraw when
                  totalSupply()→0. Prevents the permanent vault brick the
                  Soken Foundry PoC reproduced.
  F-01 Medium   — public deposit/mint/withdraw/redeem now call _accrue
                  before super so previewDeposit/previewWithdraw price
                  the post-accrue ratio. Hook-internal _accrue retained
                  as defense-in-depth.
  F-02 Medium   — removeStrategy swap-and-pops the address out of
                  strategyList, so re-add no longer double-counts in
                  totalAssets. Also resolves F-07 (unbounded list).
  F-03 Low      — fee base = TS × Δsp / ACCRUE_PRECISION × feeRate / 1e4
                  (= realized profit × feeRate). V2.0's TA-based form
                  over-charged by factor (1+g).
  F-05 Info     — StrategyInfo gains isQuarantined; Owner-only
                  setQuarantine(address,bool) excludes a strategy from
                  totalAssets and _autoPullFromStrategies; investToStrategy
                  rejects quarantined. NAV-corrupting try-catch-return-0
                  pattern rejected per Soken guidance.
  F-06 Low      — Ownable → Ownable2Step (acceptance step required);
                  renounceOwnership() overridden to revert.
  F-15 Info     — Removed 4 unused custom errors (NotOwner, InvalidAllocation,
                  WithdrawalExceedsBalance, StrategyBlacklisted-error).
                  Same-named StrategyBlacklisted event remains in active use.

Reward claim infra (F-04)
  Every adapter now exposes claimAndCompound(...) gated by onlyKeeper +
  nonReentrant. The strategy itself originates the distributor call (required
  for Fluid's msg.sender == recipient check; standardized across all five
  for uniformity). Reward → USDC swap via the constructor-pinned dexRouter
  (UniV3 SwapRouter02 on ETH/Base/Arb, PancakeV3 SmartRouter on BSC), then
  re-deposited into the same external protocol. The resulting balanceOf
  growth flows into Vault.totalAssets() and the streaming-fee _accrue
  captures the operator's 15% share automatically on the next user action.

  Per-protocol distributors:
    CompoundV3Strategy → CometRewards.claim(comet, this, true)
    VenusStrategy      → Comptroller.claimVenus(this, [vToken])
    AaveV3Strategy     → RewardsController.claimRewards([aToken], max, this, reward)
    MorphoStrategy     → UniversalRewardsDistributor.claim(this, reward, claimable, proof)
    FluidStrategy      → FluidMerkleDistributor.claim(this, cumulative, posType, posId, cycle, proof, metadata)

  New interfaces in contracts/interfaces/external/:
    ISwapRouter (UniV3-fork shape, used on all 4 chains)
    ICometRewards / IVenusComptroller / IAaveRewardsController /
    IUniversalRewardsDistributor / IFluidMerkleDistributor

Refs: Soken APY-2026-06-001 §F-17, §F-01, §F-02/F-07, §F-03, §F-04, §F-05,
      §F-06, §F-15. Full finding → fix mapping in docs/SOKEN_AUDIT.md (added
      in a follow-up commit on this PR).
Wires the V2.1 strategy claim+compound surface into the deployment + verify
pipeline so a v2.1 redeploy can pin the correct distributor / reward / DEX-router
addresses per (chain × adapter) instance.

scripts/deploy/00-config.ts
  - ChainConfig gains `dexRouter` (per-chain).
  - StrategyEntry subtypes gain optional reward fields:
      AaveStrategyEntry:     rewardsController?, rewardToken?
      CompoundStrategyEntry:  cometRewards?
      MorphoStrategyEntry:   urd?
      VenusStrategyEntry:    comptroller?, rewardToken?
      FluidStrategyEntry:    fluidDistributor?, rewardToken?
    Missing fields default to ethers.ZeroAddress → claim path no-ops.

Populated addresses (rest left as ZeroAddress opt-out pending verification):
  Ethereum  dexRouter            UniV3 SwapRouter02       0x68b34658...8665Fc45
            aave/spark           rewardsController        0x8164Cc65...80bFcb (Aave V3)
            compound             cometRewards             0x1B0e765F...8885a40
            morpho               urd                      0x330eefa8...4B1E61Ddb
            fluid                fluidDistributor         0xF398E66B...4AcC7714
            fluid                FLUID token              0x6f40d4A6...EcD303eb
  Base      dexRouter            UniV3 SwapRouter02       0x2626664c...741e481
            aave                 rewardsController        0xf9cc4F0D...51E1F44
            compound             cometRewards             0x12396480...9B00A6b1
  Arbitrum  dexRouter            UniV3 SwapRouter02       0x68b34658...8665Fc45
            aave                 rewardsController        0x929EC64c...B473e
            compound             cometRewards             0x88730d25...A9f7fae
  BSC       dexRouter            PancakeV3 SmartRouter    0x13f4EA83...974568Dd4
            venus                comptroller              0xfD36E2c2...9158384
            venus                XVS token                0xcF6BB538...64626C63

Pending confirmation (opt-out today, re-deploy when verified):
  - Spark RewardsController + SPK token (ETH)
  - Morpho URD on Base / Arbitrum
  - Fluid distributor on Base / Arbitrum / BSC
  - Kinza RewardsController + KINZA token (BSC)
  - Aave V3 reward tokens on every chain (most have no active emission today)

Plumbing changes
  - constructorArgsFor(entry) → constructorArgsFor(entry, chain). Returns
    strategy-specific args in the new order (adapter args + reward fields +
    dexRouter) so each strategy's constructor is satisfied with or without
    an active reward program.
  - V1 + V2 deploy scripts thread chain config through deployOne.
  - ops/verify.ts uses the same args order for Etherscan verification.

Out of this commit
  - v2.1 generation labels + tier hashes not introduced here (v2.1 redeploy
    can drive a same-generation redeploy that overwrites v2-prod records;
    explicit v2.1-prod-* hashes land with the migration commit).
  - 5-vault redeploy execution is post-remediation-review.

Refs: Soken APY-2026-06-001 §F-04g (chain matrix prerequisite for F-04 claim path)
…ctor sigs

Adds the V2.1 Soken remediation spec (Vault.v21.spec.ts, 14 cases) — every
behavioral fix has a dedicated test that documents the pre-fix inverse
behavior inline so regressions on the same root cause would be caught by
the same test.

Vault.v21.spec.ts blocks
  F-17 (2) — lastSharePrice reset; fresh deposit after empty vault unbricks
  F-01 (1) — accrue-first ordering: deposit→immediate-redeem returns ≈ deposit
  F-05 (4) — quarantine excludes from totalAssets; blocks invest; onlyOwner;
             toggle clean
  F-06 (2) — renounceOwnership reverts; transferOwnership two-step
  F-02 (1) — remove + re-add keeps strategyCount = 1
  F-03 (1) — fee = exactly feeRate × profit (not feeRate × (1+g) × profit)
  F-04 (1) — vault.keeper() reads dynamic; setKeeper takes effect instantly
  X-cut (2) — MAX_FEE ceiling holds across F-03 formula; boundary 20%×10%

Sync of pre-existing V2 specs to V2.1 constructor sigs
  Vault.spec.ts            — fee-formula expected values updated for F-03
                              (testFuzz_accrue_steadyYieldThenRedeem,
                               test_accrue_largeYield_precisionPreserved)
  Vault.fork.spec.ts       — inline AaveV3/CompoundV3/MorphoStrategy.deploy
                              calls extended with ZeroAddress for the new
                              reward args (fork spec opts out — invest/
                              withdraw semantics unchanged)

Run
  npx hardhat test                              # 92 passing on this repo
  npx hardhat test test/v2/Vault.v21.spec.ts    # 14 passing

Refs: Soken APY-2026-06-001 §F-17, §F-01, §F-02, §F-03, §F-05, §F-06, §F-04
…ken remediation

V2_DESIGN.md
  - §3.1 _accrue() pseudocode: fee base TS × Δsp / ACCRUE_PRECISION × feeRate (V2.1 F-03)
  - §3.2 Fix [[1]] / Fix [[2]] worked examples re-priced ($0.151 → $0.150 at 10%×15%)
  - New §6 — V2.1 Soken Audit Remediation:
      6.1 Behavioral fixes (F-17 / F-01 / F-02 / F-03 / F-05 / F-06 / F-15)
      6.2 V2.1 reward claim mechanism (F-04) — per-strategy distributor table,
          Fluid msg.sender pattern, per-chain DEX router map, Keeper trigger spec
      6.3 F-04g chain × strategy reward matrix + opt-out semantics
      6.4 Test coverage summary (92 hardhat + 14 V2.1-spec + 10×10k forge fuzz)
      6.5 V2.0 → V2.1 deployment migration plan (mirrors V1 → V2 §5)
  - §7 References extended with SOKEN_AUDIT.md cross-link

TRUST_MODEL.md
  - TL;DR rewritten to distinguish non-owner guarantee from owner-trust caveat
    (Soken §F-16 phrasing correction). Owner CAN, by addStrategy(malicious) +
    setKeeper(self) + invest, route principal through a self-supplied strategy
    — accepted centralization risk, mitigated by Safe m-of-n threshold + public
    StrategyAdded event + invariant that withdraw is always open.
  - Owner CAN table extended:
      setQuarantine(address, bool) — V2.1 F-05 escape hatch
      transferOwnership(address)    — V2.1 F-06 two-step transfer

SECURITY.md
  - Audit Status replaced with V2.1 remediation timeline:
      Solo audit (Soken) V2 .................. Complete (2026-06-23, APY-2026-06-001)
      V2.1 remediation (16 findings → fixes) . In review — feat/v2.1 PR
      Soken remediation review ............... Scheduled
      Contest audit .......................... Planned (post-V2.1 deploy)
      Bug bounty ............................. Planned (post-V2.1 / post-contest)
  - Soken findings 1-line summary (1 Critical / 2 Medium / 4 Low / 9 Info,
    12 fixed + 4 acknowledged)
  - Pointer to docs/SOKEN_AUDIT.md for finding → fix mapping

NEW — docs/SOKEN_AUDIT.md
  - Per-finding mapping: pre-fix root cause + V2.1 fix (file refs) + new test
    that asserts the post-fix invariant
  - Severity summary table + commit log (c068433 / 4057b8c / e3ca5e1 /
    42ab80a / 209e379)
  - Acceptance criteria for the Soken remediation review

Refs: Soken APY-2026-06-001 (entire report)
@kimandy7778 kimandy7778 merged commit d7743e2 into main Jun 25, 2026
@kimandy7778 kimandy7778 deleted the feat/v2.1 branch June 25, 2026 03:32
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