Solidity smart contracts for the Tezoro Aggregator yield vault.
Tezoro Aggregator is an ERC-4626 compliant vault that allocates user deposits across multiple DeFi lending protocols (Aave V3, Compound V3, Spark, Morpho Blue, Fluid, and any ERC-4626 vault). A keeper rebalances allocations; a performance fee is charged on yield above a high water mark. Withdrawals are always open regardless of pause state.
This repository contains two versions of the contracts:
- V1_1 -- currently deployed on Ethereum mainnet with live TVL (see Deployments).
- V1_2 -- contains fixes from an internal pre-audit review. These will replace V1_1 after audit, but redeployment requires TVL migration, so V1_1 remains in production for now.
Both versions should be audited. V1_1 is what users interact with today; V1_2 is what will be deployed next. The fixes in V1_2 address issues we identified internally -- we want the auditor to verify both that V1_1's known issues are correctly catalogued and that V1_2's fixes are sound.
| # | Description |
|---|---|
| 1 | Single-pass rebalance does not converge in one call. _rebalance() processes strategies in array order. If an earlier strategy needs funds from a later one, the deposit fails because the withdrawal hasn't happened yet. Requires a second rebalance() call. |
| 2 | Pausing a strategy drops share price. totalAssets() excludes paused strategies' trackedBalance, even when funds are safe and the pause is precautionary. Creates an arbitrage window: buy during pause (cheap), sell after unpause (expensive). |
| 3 | Stale comment. _availableLiquidity comment says "reduced by 1" but code subtracts 2. Behavior is correct. |
| 4 | Simple strategies lack ReentrancyGuard. AaveV3Strategy, CompoundV3Strategy, FluidStrategy rely on vault's nonReentrant + onlyVault. Safe today, but defense-in-depth recommends adding it. |
| 5 | executeClaim return data discarded. Some claim targets return useful info (amounts claimed). Not exploitable. |
| 6 | routerData in swap() is opaque. Keeper trust assumption not documented. Balance-before/after check mitigates, but worth noting. |
| 7 | setKeeper in multi-strategies rejects address(0). Unlike the vault (which allows address(0) for admin-only mode), strategies cannot disable the keeper role. |
| Contract | Changes from V1_1 |
|---|---|
TezoroV1_2.sol |
#1: Two-pass rebalance (withdrawals first, then deposits) for single-call convergence. #2: Paused strategies stay in totalAssets() -- no share price drop on pause. #3: Comment fix. |
RewardsModuleV1_2.sol |
#5: executeClaim emits return data. #6: Trust assumption documented in NatSpec. |
AaveV3StrategyV1_2.sol |
#4: Added ReentrancyGuard (defense-in-depth). |
CompoundV3StrategyV1_2.sol |
#4: Added ReentrancyGuard (defense-in-depth). |
FluidStrategyV1_2.sol |
#4: Added ReentrancyGuard (defense-in-depth). |
ERC4626MultiStrategyV1_2.sol |
#7: setKeeper(address(0)) allowed (admin-only mode). |
MorphoBlueMultiStrategyV1_2.sol |
#7: setKeeper(address(0)) allowed (admin-only mode). |
V1_1 contracts are deployed on Ethereum mainnet with live TVL. They remain in the repo as the production reference. The known issues listed above are accepted risks until V1_2 is deployed.
src/
TezoroV1_2.sol Core ERC-4626 vault (V1_2 -- audit target)
RewardsModuleV1_2.sol Claims executor, swap engine (V1_2 -- audit target)
TezoroV1_1.sol Core ERC-4626 vault (V1_1 -- currently deployed)
RewardsModule.sol Claims executor, swap engine (V1_1 -- currently deployed)
interfaces/
IStrategy.sol Universal strategy adapter interface
ITezoroV1_1.sol Vault interface for RewardsModule
IAaveV3Pool.sol Aave V3 lending pool interface
ICompoundV3Comet.sol Compound V3 Comet interface
ICometRewards.sol Compound V3 rewards interface
IMorpho.sol Morpho Blue singleton interface
IRewardsController.sol Aave/Spark rewards controller interface
strategies/
AaveV3StrategyV1_2.sol Aave V3 adapter (V1_2 -- audit target)
CompoundV3StrategyV1_2.sol Compound V3 adapter (V1_2 -- audit target)
FluidStrategyV1_2.sol Fluid fToken adapter (V1_2 -- audit target)
ERC4626MultiStrategyV1_2.sol Multi-vault adapter (V1_2 -- audit target)
MorphoBlueMultiStrategyV1_2.sol Multi-market Morpho adapter (V1_2 -- audit target)
AaveV3Strategy.sol Aave V3 adapter (V1_1 -- currently deployed)
CompoundV3Strategy.sol Compound V3 adapter (V1_1 -- currently deployed)
FluidStrategy.sol Fluid fToken adapter (V1_1 -- currently deployed)
ERC4626MultiStrategy.sol Multi-vault adapter (V1_1 -- currently deployed)
MorphoBlueMultiStrategy.sol Multi-market Morpho adapter (V1_1 -- currently deployed)
test/
unit/ Unit tests (mock-based, no RPC required)
TezoroV1_2.t.sol V1_2-specific regression tests (M-1, M-2)
fuzz/ Fuzz and invariant tests (mainnet fork)
fork/ Integration tests against live protocols (mainnet fork)
shared/ Shared test infrastructure
+-------------------+
User deposits --> | TezoroV1_2.sol | <-- ERC-4626 vault
| (idle buffer) |
+--------+----------+
|
keeper calls allocate() with bps weights
|
+-------------------+-------------------+
| | | | |
AaveV3 Compound Fluid Morpho ERC4626Multi
Strategy Strategy Strategy MultiStrat MultiStrat
| | | | |
Aave V3 Comet V3 fTokens Morpho Morpho Vaults,
(+ Spark) Blue MetaMorpho, etc.
|
+-----------------------------+
| RewardsModule.sol |
| claim -> swap -> compound |
+-----------------------------+
- Deposit: User calls
deposit(assets, receiver). Vault mints shares, holds assets in idle buffer. Keeper later callsallocate()to deploy idle funds to strategies. - Withdraw: User calls
withdraw(assets, receiver, owner)orredeem(shares, receiver, owner). Vault first uses idle buffer; if insufficient, executes a waterfall withdrawal from strategies until the amount is covered. Try-catch on each strategy prevents a broken strategy from blocking withdrawals.
All strategies implement IStrategy:
function deposit(uint256 amount) external;
function withdraw(uint256 amount) external returns (uint256 withdrawn);
function emergencyWithdraw() external returns (uint256 withdrawn);
function balanceOf() external view returns (uint256);
function availableLiquidity() external view returns (uint256);Multi-strategies (ERC4626MultiStrategy, MorphoBlueMultiStrategy) add sub-position management: the keeper allocates/deallocates across sub-vaults or sub-markets within the strategy.
_decimalsOffset() returns the underlying asset's decimals (read once at construction, stored as immutable). This creates a large virtual share supply that makes first-depositor inflation attacks economically infeasible. The offset scales automatically with the asset (6 for USDC, 18 for DAI, 8 for WBTC).
- Fee applies only to yield above the all-time high water mark share price.
- Fee is accrued before every withdrawal (dodge-proof):
_accruePerformanceFee()mints fee shares before ERC-4626 conversion inwithdraw()andredeem(). - Fee shares are minted to
feeRecipient. - Fee percentage changes are timelocked when increasing.
- Default: 15%. Range: 0--30%.
All four preview functions (previewDeposit, previewMint, previewWithdraw, previewRedeem) account for pending performance fee shares. This ensures preview values match actual execution outcomes, which is required by the ERC-4626 specification:
previewDeposit(x)returns shares user will receive after fee accrual.previewWithdraw(x)returns shares that will be burned (>= actual, per spec).previewRedeem(x)returns assets that will be received (<= actual, per spec).
Account for pending fee dilution and available liquidity across strategies. This ensures withdraw(maxWithdraw(x)) and redeem(maxRedeem(x)) never revert. Each strategy's available liquidity is reduced by 2 wei to absorb rounding in underlying protocol withdrawals.
Configurable depositCap (0 = unlimited). maxDeposit() and maxMint() use previewDeposit() (fee-aware) to accurately report remaining capacity.
Configurable percentage (max 20%) of total assets held as idle in the vault for instant small withdrawals without strategy interaction. Maintained by keeper during allocate().
Force-redeem operations and fee increases support an optional timelock. When timelockDelay > 0:
- Admin calls
proposeTimelock(operationHash). - Wait for delay to pass.
- Execute the operation.
Reducing the timelock delay itself is timelocked (at the current, longer delay). Increasing the delay is immediate (more restrictive = safe). Duplicate proposals for the same hash revert -- cancel first, then re-propose.
Admin ownership is transferred via a two-step process (transferAdmin + acceptAdmin) to prevent accidental transfers to wrong addresses. All contracts (vault, strategies, rewards module) use this pattern.
removeStrategy() is non-reentrant. When removing a strategy with tracked funds, the vault attempts an emergency withdrawal. If recovery is partial, the lost amount is emitted via StrategyRemovalFundsLost for off-chain tracking. The total allocation is validated after removal to prevent exceeding 100%.
Admin can force-redeem users (single or batch up to 50). Uses a dust tolerance per-strategy (2 wei each or 1 bps of the asset amount, whichever is larger) to cover protocol-specific rounding errors in Morpho and Aave.
| Role | Key Permissions |
|---|---|
| Admin | Add/remove/pause strategies, set allocations/caps/fees, freeze deposits, recall to idle, force redeem, transfer admin, set guardian |
| Guardian | Pause strategies and vault, freeze strategy deposits. Cannot unpause or unfreeze (one-way emergency brake). |
| Keeper | Rebalance (allocate), reconcile strategy balances, harvest rewards, sweep strategy rewards, collect fees, allocate/deallocate within multi-strategies |
| User | Deposit (when not frozen/paused, within cap), withdraw and redeem (always open, even when paused) |
1. harvestAll() Aave/Compound claim directly to RewardsModule
2. executeClaim() Merkle claims (Morpho URD, Merkl) land on strategy
3. sweepStrategyReward() Forward from strategy to RewardsModule
4. swap() Swap reward token to base asset via whitelisted DEX router
5. sweepToVault() Deposit base asset back into vault (auto-compounding)
The RewardsModule holds reward tokens; swaps happen in isolation from the vault. DEX routers are admin-whitelisted.
| Protocol | Strategy | Rewards Mechanism | Chains |
|---|---|---|---|
| Aave V3 | AaveV3Strategy |
ARB via RewardsController | Ethereum, Arbitrum |
| Compound V3 | CompoundV3Strategy |
COMP via CometRewards | Ethereum, Arbitrum |
| Spark | AaveV3Strategy (shared) |
via RewardsController | Ethereum |
| Morpho Blue | MorphoBlueMultiStrategy |
MORPHO via URD + sweepReward | Ethereum |
| Fluid | FluidStrategy |
via Merkl + sweepReward | Ethereum, Arbitrum |
| ERC-4626 vaults | ERC4626MultiStrategy |
Protocol-dependent | Ethereum, Arbitrum |
Distributes funds across whitelisted ERC-4626 sub-vaults (e.g., MetaMorpho, Morpho Vaults). Keeper calls allocate(subVault, amount) / deallocate(subVault, amount). Sub-vaults can be added/removed by admin (removal requires zero position). All external calls are non-reentrant.
Distributes funds across whitelisted Morpho Blue markets (supply-side only). Keeper calls allocate(marketId, amount) / deallocate(marketId, amount). Provides accrueAllInterest() for accurate balance reporting before vault reconciliation. Market removal requires zero position. All external calls are non-reentrant.
- Withdrawals never blocked:
withdraw()andredeem()work even whenpaused == true. Only deposits are frozen. - Fee-dodge impossible:
_accruePerformanceFee()runs insidewithdraw(),redeem(),forceRedeem(), andbatchForceRedeem()before share conversion. - Broken strategy cannot DoS: All strategy calls in withdraw waterfall, rebalance, reconcile, and harvest use try-catch. A reverting strategy is skipped, not blocking.
- No reentrancy:
ReentrancyGuardon all state-changing vault functions and all strategy entry points. - No unsafe transfers:
SafeERC20used for all token transfers.
| Attack Vector | Defense |
|---|---|
| First-depositor inflation / donation | Virtual shares offset (_decimalsOffset = assetDecimals), internal accounting |
| Reentrancy | ReentrancyGuard on vault + all strategies |
| Unsafe ERC-20 | SafeERC20 everywhere |
| Rounding exploits | ERC-4626 rounding in vault's favor, preview functions account for pending fees |
| Excessive deposits | Configurable deposit cap |
| Reward token contamination | Swaps isolated in RewardsModule, never in vault |
| Fee dodging | Performance fee accrued before withdrawal conversion |
| Admin transfer to wrong address | Two-step transfer (transferAdmin + acceptAdmin) |
| Sudden fee hike | Fee increases are timelocked |
| Timelock delay reduction | Reducing delay is itself timelocked at the current (longer) delay |
Users trust:
- The immutable vault contract code.
- The admin (Safe multisig
0x9D7e68c2c0D43f97B873336471927d7C0171e78A). Admin can add/remove strategies, change allocations, change fees (timelocked), force-redeem users (optional timelock), and pause. - The keeper (bounded actions). Keeper can rebalance and harvest but cannot extract funds or change parameters.
Admin cannot block withdrawals. The only irreversible admin action is strategy removal with fund loss (emitted as event).
- Foundry (forge, anvil)
- RPC endpoints for fork tests (Alchemy, Infura, or any archive node)
# Install dependencies
forge install
# Copy and configure RPC endpoints
cp .env.example .env
# Edit .env with your RPC URLs# Unit tests (no RPC required, fast)
forge test --match-path "test/unit/*"
# Fuzz + invariant tests (requires Ethereum mainnet RPC)
forge test --match-path "test/fuzz/*"
# Fork tests -- all chains (requires all RPC endpoints)
forge test --match-path "test/fork/*"
# Full suite
forge test| Category | Files | Tests | Description |
|---|---|---|---|
| Unit | 7 | 390 | Core vault, strategies, rewards module, performance fee, deposit freeze |
| Fuzz / Invariant | 6 | 75 | Property-based testing: share price monotonicity, solvency, ERC-4626 round-trips |
| Fork (per-chain) | 6 | ~3,000 | Live protocol integration across Ethereum, Arbitrum, Optimism, Base, BSC, Polygon |
| Fork (standalone) | 11 | ~300 | Security regressions, ERC-4626 compliance, multi-vault isolation, reward pipelines |
Fork tests use shared infrastructure (test/fork/shared/) to run the same test suite against every supported chain and token combination.
- Solidity
^0.8.26 - OpenZeppelin Contracts v5.x (
ERC4626,ERC20,ReentrancyGuard,SafeERC20,Math)
| Contract | Address |
|---|---|
| TezoroV1_1 (tUSDC-A, conservative) | 0x7cc42747862ecACe79FA89BeFcB29C94d2558dEC |
| TezoroV1_1 (tUSDC-B, moderate) | 0xD0F2812F57D284c0d30Ddd8f73b146Ef0cD5a25c |
| TezoroV1_1 (tUSDC-C, aggressive) | 0xa139C6a7dd1Bd76Ae3FBCCF4F8bEbA5C4f26513d |
| Admin (Safe multisig) | 0x9D7e68c2c0D43f97B873336471927d7C0171e78A |