V2.1 — Soken audit remediation (APY-2026-06-001)#2
Merged
Conversation
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
7f34422feat(v2.1): contracts — Soken APY-2026-06-001 remediation (9 findings)eacb892chore(v2.1): scripts — F-04g chain × strategy reward matrix + plumbinga30190atest(v2.1): hardhat V2.1 spec + sync existing V2 specs to new constructor sigs5e3bb50docs(v2.1): V2_DESIGN + TRUST_MODEL + SECURITY + new SOKEN_AUDIT — Soken remediationFindings summary
Critical fix highlight — F-17
Soken's Foundry PoC reproduced a permanent vault brick: once
totalSupply()returnsto zero (all holders exit), the never-reset
lastSharePricebaseline detonates theFeeTooHighdefensive guard on the next deposit and freezes every entrypoint with noin-contract recovery.
Fix: reset
lastSharePrice = 0in both_accrue()'stotalSupply()==0branchand
_withdraw()after the post-burn check. The next deposit re-takes the lazy-initpath and re-snaps the baseline cleanly.
test/v2/Vault.v21.spec.tsreproduces the Soken PoC and asserts the post-fixbehavior —
test_f17_freshDepositAfterEmptyVault_doesNotBrick.Test plan
npx hardhat compile— 16 Solidity files OKnpx hardhat test— 92 passing (4 fork specs pending withoutFORK=true)npx hardhat test test/v2/Vault.v21.spec.ts— 14 V2.1-specific specs passingforge test --fuzz-runs 10000— 10 properties × 10 000 iterations, zero failuresdocs/SOKEN_AUDIT.mdmatchesdocs/V2_DESIGN.md§6.5After merge
v2.1.0tag (this PR's HEAD).v2.1.0GitHub release with the SOKEN_AUDIT mapping + remediation evidence.setDepositCap(0)via Multi-sig → halt new V2.0 deposits.(existing batches in
safe-batches/v2-prod/).