███████╗ █████╗ ██████╗ ███╗ ██╗ ██████╗ ██████╗ ██╗██████╗ ██╔════╝██╔══██╗██╔══██╗████╗ ██║██╔════╝ ██╔══██╗██║██╔══██╗ █████╗ ███████║██████╔╝██╔██╗ ██║██║ ███╗██████╔╝██║██║ ██║ ██╔══╝ ██╔══██║██╔══██╗██║╚██╗██║██║ ██║██╔══██╗██║██║ ██║ ███████╗██║ ██║██║ ██║██║ ╚████║╚██████╔╝██║ ██║██║██████╔╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═════╝

SECURITY AUDIT

BlendedVault v2.0.0 · Base Mainnet

Date: 2026-08-15 Vault: 0xbDac…b20D3 SwapAndEarn: 0x0de2…10B62 Chain: Base (8453) Auditor: 🤖
Sourcify Verified ERC-4626 Compliant Reentrancy Protected Invariant Tested Timelock Governance Automated Audit

Scope. v2.0.0 is a full contract redeploy, verified on Sourcify (exact_match). The vault was rebuilt on a purpose-built ERC-20/4626 core instead of OpenZeppelin inheritance: AccessControl is gone in favour of a single admin() behind the same two-step delayed handover, and in-vault referrals were added. The performance fee is 4%, split 50/50 with a referrer, and both FeeBps and ReferralShareBps are timelocked on the way up and immediate on the way down. 283 unit tests and 176 fork tests green; pre-deploy review raised 10 findings, 8 fixed.

Sections below covering v1.0.7 and earlier are kept as the historical record of the retired v1 vault 0x8694…233dE. They do not describe the live contract.

📊

BY THE NUMBERS

283 Unit Tests (CI)
176 Fork Tests
8 Invariants
819,200 Invariant Calls
1,922 Solidity Lines
22,136 Contract Bytes
90.07% EIP-170 Used
20 Test Files
50+ Findings Resolved
🔧

v1.0.7 — FIXES WAVE + SEO

Change What it does
Allocator — measured withdrawals (E-04) The deposit leg is sized against the amount the withdrawal leg actually delivered (read from the Rebalanced receipt), not the amount requested. A strategy that under-delivers no longer strands capital or reverts the deposit.
Allocator — missing APY ≠ 0% (A13-01) A strategy the Morpho API failed to price is omitted from ranking (its position is preserved) instead of being quoted as 0% yield and withdrawn.
Allocator — queue deferral Strategies clamped by tier caps are deferred to the tail of the deposit queue rather than holding a queue slot they cannot fund.
Swap form — share floor (B-06) The swap form waits for the share preview before signing; it can no longer submit a deposit with minShares=0 while the quote is still loading. A rejected approval no longer fires the deposit either.
Indexer /api/allocations now reports unreadable strategies instead of dropping them silently; proof uploads pin their file extension to the allowlisted type with nosniff/CSP headers.
Scripts Daily-report metrics (rebalances, harvests, TVL fallback) now read live data instead of structurally zero; harvest.sh fee parsing fixed.
SEO & docs Per-route canonicals, llms.txt, GA4, IndexNow, Search Console, one-command web deploy, REFERALS.md, and a full-stack audit pass.
🔄

v1.0.6 — DEPOSIT-QUEUE AUTO-SYNC

Change What it does
Allocator feature The allocator now rewrites the vault's deposit queue to its risk-adjusted ranking every tick. New deposits land in the top-paying strategy in the deposit transaction itself — previously the queue was frozen at bootstrap tier order, so deposits parked in Prime (4.12%) while 99.5% of TVL sat in Frontier (5.93%).
Governance write One onchain setDepositQueue (tx 0x8d97db…2bf, 75,271 gas): [Frontier, HY, Prime, Moonwell, Steakhouse] = model ranking.
Fix 1 (automated review) desiredDepositQueue returned registration order when ALL strategies were frozen/unpriced — a market-wide freeze would have reverted deposit routing to bootstrap order (Prime first). Now proposes no queue; the last good order stays in force.
Fix 2 (automated review) Queue sync moved BEFORE the rebalance legs so a repeating TierLimitExceeded can't silently disable deposit routing.
Fork-tested DepositQueueFork.t.sol (5/5 pass against the live vault on a Base fork): deposit reaches the queue head in the same tx, reordering reroutes instantly, duplicates/unregistered entries revert InvalidQueueStrategy. Review verdict: APPROVE.
Test counts Contracts 233 (CI) + 5 fork · Allocator 129 (up from 74) · Indexer 130 · 492 total (497 with fork).
🏗️

ARCHITECTURE

BlendedVault — ERC-4626 Vault-of-Vaults

Users deposit USDC and receive shares. The vault allocates idle liquidity across a curated allowlist of ERC-4626 strategy vaults (MetaMorpho markets), subject to per-strategy caps and per-tier exposure limits. Five strategies are currently registered, spread across all three risk tiers.

architecture.sol
// Contract inheritance chain BlendedVault ├── ERC4626 // Standard vault interface │ └── ERC20 // Share token (12 decimals) ├── AccessControl // Role-based authorization │ ├── CURATOR // Strategy curation │ ├── ALLOCATOR // Rebalance execution │ └── GUARDIAN // Emergency response └── ReentrancyGuardTransient // EIP-1153 SwapAndEarn ├── Ownable2Step // Two-step ownership └── ReentrancyGuardTransient // EIP-1153
🛡️

SECURITY PRACTICES

Reentrancy Protection — EIP-1153 Transient Storage

Every state-changing entrypoint is guarded by ReentrancyGuardTransient using EIP-1153 transient storage (Cancun). External strategy calls sit inside the guard. Tests assert the re-entrant call failed, not just that something reverted.

Inflation Attack Resistance — 1e6 Virtual Shares

_decimalsOffset() = 6 gives 1,000,000 virtual shares against 1 virtual asset. A donation/first-depositor attack must burn ~1,000,000× what it can steal. Fuzz-tested: the attacker cannot profit and the victim keeps ≥99.9% of principal.

Fee Escape Prevention

Withdraw and redeem settle fees before pricing shares, so a holder cannot exit ahead of harvest() and dodge their share of performance fees. The high-water mark records the post-dilution price, preventing double-charging.

Strategy Transfer Measurement

Deposits and withdrawals use the asset balance delta rather than trusting a strategy to move exactly the requested amount. Unconsumed approvals are revoked immediately. A lossy strategy cannot corrupt vault accounting.

Bounded Loops & Governance Safety

MAX_STRATEGIES = 16 caps every accounting pass. Scheduled changes expire after 30 days so a forgotten or pre-planted change cannot execute months later. MAX_FEE_BPS = 2,000 (20%) is a hard, non-governable ceiling.

🔑

ACCESS CONTROL

Check Status Detail
Role separation in code ✓ 4 distinct roles: Admin, Curator, Allocator, Guardian
Two-step admin transfer ✓ beginAdminTransfer → acceptAdminTransfer
Admin grant/revoke blocked ✓ Only via two-step path; direct grantRole reverts
Timelock for risk-increasing changes ✓ New strategies, cap increases, parameter changes
Scheduled-change expiry ✓ 30-day grace period after becoming executable
Risk-reducing changes ✓ Immediate: lower caps, pause, increase cooldowns
📐

ERC-4626 COMPLIANCE

Requirement Status Notes
asset, totalAssets, convertToX, previewX ✓ previewX asserted equal to actual in fuzz tests
maxDeposit / maxMint ✓ Returns 0 when paused
maxWithdraw / maxRedeem ✓ Capped at reachable liquidity, and fee-aware as of v1.0.3 (D-01)
Rounding direction ✓ Always favours the vault; fuzz-tested round-trip never profits
First-deposit attack ✓ 1e6 virtual shares + minInitialDeposit
mint mints exactly shares ✓ Tested
Slippage protection ✓ deposit(…, minShares), redeem(…, minAssets)
🧪

TEST COVERAGE

8 invariants exercised across 819,200 calls (1,024 runs × 100 depth):

Invariant Asserts
totalAssetsEqualsIdlePlusStrategies Accounting identity holds exactly
totalAssetsCoversIdle No assets conjured from idle cash
shareSupplyIsFullyAttributed Every share belongs to a known holder
solvency All holders redeeming at once cannot exceed totalAssets
strategyListIndexIntegrity listIndex stays in sync with strategyList
liquidityWithinTotalAssets Never advertise more liquidity than owned
queuesOnlyContainRegisteredStrategies No stale queue entries
emptyVaultPricesAtPar HWM resets when fully drained
test-suite.sh
$ forge test --summary # 283 passed, 0 failed, 7 skipped (290 total) across 19 suites # The 7 skips are the fork suites: each probes for code at its target # address in setUp and skips when there is none, so CI stays green # without an archive RPC. On a Base fork they run 176/176. # BlendedVault.t.sol — core deposit/withdraw/mint/redeem # BlendedVaultFuzz.t.sol — fuzz testing # BlendedVaultInvariants.t.sol — 8 invariants, 819,200 calls # BlendedVaultLifecycle.t.sol — strategy lifecycle # BlendedVaultTimelock.t.sol — governance timelock # BlendedVaultFees.t.sol — HWM, dilution, fee math # BlendedVaultReferrals.t.sol — bind guards, 2/2 split, claim idempotence # BlendedVaultGas.t.sol — gas benchmarks # BlendedVaultReentrancy.t.sol — attack scenario tests # BlendedVaultAuditRegressions.t.sol — one test per audit finding # SwapAndEarn.t.sol — peripheral router # DepositQueueFork.t.sol — deposit-queue routing vs the live v2 vault All tests passed ✓ # +149 allocator tests, +151 indexer tests = 583 unit total (759 with fork)
📦

DEPENDENCIES

Package Version Notes
OpenZeppelin Contracts 5.5.0 Industry standard, audited
Solidity 0.8.28 evm_version = cancun for EIP-1153
Node.js 26.5.0 Runtime
viem 2.55.0 TypeScript Ethereum client
Next.js 16.2.12 Web frontend
📡

MONITORING & DEFENSE

🔍

VERIFY YOURSELF

verify.sh
# Check the onchain version string $ cast call 0xbDacA8B7782C66cc0ee32Cf70F835EBe86cb20D3 \ "VERSION()(string)" --rpc-url https://base-rpc.publicnode.com "2.0.0" # Verify source on Sourcify (exact match, creation + runtime) $ curl -s "https://sourcify.dev/server/v2/contract/8453/\ 0xbDacA8B7782C66cc0ee32Cf70F835EBe86cb20D3?fields=compilation" \ | jq '{match, creationMatch, runtimeMatch}' { "match": "exact_match", "creationMatch": "exact_match", "runtimeMatch": "exact_match" } # Who governs it? v2 has no roles — one admin, moved only by two-step delayed handover $ cast call 0xbDacA8B7782C66cc0ee32Cf70F835EBe86cb20D3 \ "admin()(address)" --rpc-url https://base-rpc.publicnode.com # Never trust a documented fee — read it. 400 = 4.00% $ cast call 0xbDacA8B7782C66cc0ee32Cf70F835EBe86cb20D3 \ "feeBps()(uint16)" --rpc-url https://base-rpc.publicnode.com # Check total assets $ cast call 0xbDacA8B7782C66cc0ee32Cf70F835EBe86cb20D3 \ "totalAssets()(uint256)" --rpc-url https://base-rpc.publicnode.com # Read the assets-per-share price index $ cast call 0xbDacA8B7782C66cc0ee32Cf70F835EBe86cb20D3 \ "assetsPerShare()(uint256)" --rpc-url https://base-rpc.publicnode.com