Introduction
ERC-4626 is a standard to optimize and unify the technical parameters of yield-bearing vaults. It provides a standard API for tokenized yield-bearing vaults that represent shares of a single underlying ERC-20 token. Before EIP-4626 finalized in March 2022, every yield protocol shipped its own interface. Yearn vaults, Aave aTokens, Compound cTokens, and the Maker DSR each carried bespoke functions, and every integrator wrote a custom adapter for each one. The standard collapses that surface area into one Solidity interface, which is why most DeFi yield primitives shipped after 2023 implement it natively.
That interoperability win carries a cost: the standard specifies how vaults expose their interface, not how securely they implement it. At the time of writing, Solodit indicates 265 findings related to ERC-4626, of which 169 are HIGH and MEDIUM severity. Fully permissionless use cases could fall prey to malicious implementations which only conform to the interface but not the specification. It is recommended that all integrators review the implementation for potential ways of losing user deposits before integrating.
This article tears through the entire ERC-4626 security surface: standard mechanics, the inflation attack and its mitigations, rounding direction, totalAssets manipulation, withdrawal queue design, accounting invariants, wrapping risks, and the unique vulnerabilities introduced by yield strategies that reach into external protocols. Solidity throughout, audit checklist at the end.
1. Standard Mechanics
ERC-4626 is an extension of ERC-20: an ERC-4626 vault is itself an ERC-20 token whose balance represents a claim on the vault’s underlying asset. The standard adds eight functions and four events for deposit, withdraw, mint, redeem, and the conversion math between assets and shares.
An ERC-4626 vault wraps an “asset” (for example, DAI or an LST) and mints “shares” to depositors. The core mechanics are: totalAssets, which returns the vault’s view of all underlying assets it controls; convertToShares/convertToAssets, deterministic conversion helpers based on the current exchange rate; deposit/mint, which deposit a precise amount of asset or request a specific number of shares; withdraw/redeem, which withdraw a precise amount of asset or burn a specific number of shares; and the preview functions — previewDeposit, previewMint, previewWithdraw, previewRedeem — which allow UI, routers, and wallets to simulate outcomes and handle slippage.
The share/asset exchange rate is the vault’s heartbeat. For a deposit, the vault calculates shares = assets * totalSupply / totalAssets. For a redeem: assets = shares * totalAssets / totalSupply. When yield accrues to the vault (the vault’s underlying balance grows from interest, lending revenue, or strategy returns), totalAssets increases while totalSupply stays constant. Each existing share now claims more underlying asset — the share price went up.
// Minimal compliant vault skeleton
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleVault is ERC4626 {
constructor(IERC20 asset_)
ERC20("Simple Vault Share", "svTKN")
ERC4626(asset_)
{}
// totalAssets() is inherited; returns asset.balanceOf(address(this))
// For yield-bearing vaults, override this to include deployed capital.
}
Security hinges on totalAssets(): it drives pricing for convertToShares/convertToAssets. Top risks include first-depositor inflation, reentrancy, fee-on-transfer/rebasing tokens, oracle manipulation, and rounding drift.
2. The Inflation Attack
How It Works
The most discussed pitfall in ERC-4626 is the inflation attack, sometimes called the donation attack or first-depositor attack. The attack works against the share-pricing math at low totalSupply.
The step-by-step mechanic:
An attacker is the first depositor into a fresh vault. They deposit 1 wei of the underlying asset and receive 1 share. They then directly transfer (not deposit, bypassing the share-mint logic) a large amount of the underlying asset into the vault. totalAssets() now reads, say, 10,000 USDC, while totalSupply remains at 1 share. A subsequent victim depositor deposits 5,000 USDC, expecting roughly half ownership. The share-mint formula (assets × totalSupply / totalAssets) gives them 5,000 × 1 / 10,000 = 0 shares, rounded down. The attacker still holds the only share, now backed by 15,000 USDC, and can redeem the entire balance.
// Vulnerable vault - no inflation protection
// Attacker steps:
// 1. deposit(1) → totalSupply=1, totalAssets=1
// 2. asset.transfer(vault, 1e18) → totalSupply=1, totalAssets=1e18+1
// 3. victim.deposit(5e17) → shares = 5e17 * 1 / (1e18+1) = 0
// victim receives 0 shares; 5e17 asset is gifted to attacker
// 4. attacker.redeem(1) → receives 1e18+1 + 5e17 assets
The vulnerability arises from a rounding issue in the ‘mint shares’ function: sharesAmount = totalShares * assetAmount / asset.balanceOf(address(this)). Hackers can manipulate the denominator, causing a victim to receive either zero or one shares of the vault.
Virtual Shares: The Standard Mitigation
The defense most vaults adopted comes from OpenZeppelin’s reference implementation: a virtual-shares offset. The vault adds 1:N virtual asset/shares to enforce an initial exchange rate. The N value is 10**offset, with offset being the decimal (precision) difference between the vault and the underlying token. Technically, the offset corresponds to the difference (in orders of magnitude) between the funds required by the attacker and the funds being deposited by the user.
OpenZeppelin’s implementation translates to:
// OpenZeppelin ERC4626 virtual offset pattern (simplified)
function _convertToShares(uint256 assets, Math.Rounding rounding)
internal view virtual returns (uint256)
{
// Virtual shares: totalSupply + 10**_decimalsOffset()
// Virtual assets: totalAssets + 1
return assets.mulDiv(
totalSupply() + 10 ** _decimalsOffset(),
totalAssets() + 1,
rounding
);
}
function _convertToAssets(uint256 shares, Math.Rounding rounding)
internal view virtual returns (uint256)
{
return shares.mulDiv(
totalAssets() + 1,
totalSupply() + 10 ** _decimalsOffset(),
rounding
);
}
// Override in your vault to select a meaningful offset
function _decimalsOffset() internal pure virtual override returns (uint8) {
return 3; // 1000x virtual cushion; offset of 9 provides near-bulletproof protection
}
Even with an offset of 0, the virtual shares and assets make this attack non-profitable for the attacker. Bigger offsets increase the security even further by making any attack on the user extremely wasteful.
Dead Shares: An Alternative
Morpho DAO mitigates inflation attacks by depositing assets into the vault upon initialization. The corresponding shares are minted to the vault itself, which functions as a non-operational address. This strategy is akin to the creation of dead shares, but the loss is borne by the project. The more assets deposited initially, the more challenging it becomes to execute an inflation attack. However, sufficient funds must be available for the initial deposit, as they are effectively lost.
// Dead-share initialization pattern
constructor(IERC20 asset_, uint256 seedAmount) ERC20("Vault", "vTKN") ERC4626(asset_) {
// Approve and deposit a seed amount, burning shares to address(this)
// This must be done after deployment via a factory or initialization function
// to avoid approve-before-deploy issues.
asset_.transferFrom(msg.sender, address(this), seedAmount);
_mint(address(this), seedAmount); // dead shares, permanently locked
}
The Router Defense
The issue is raised when we do not control the amount of shares created. By using a router — a middleware between the user and the vault — we can verify that the amount of shares matches the one we should expect. If someone tries to manipulate or inflate the vault, we can see it and revert our transaction, avoiding us being trapped in a diluted share allocation.
// Slippage-protected deposit via router
interface IERC4626Router {
function depositWithMinShares(
address vault,
uint256 assets,
address receiver,
uint256 minSharesOut
) external returns (uint256 shares);
}
contract ERC4626Router {
function depositWithMinShares(
address vault,
uint256 assets,
address receiver,
uint256 minSharesOut
) external returns (uint256 shares) {
IERC20(IERC4626(vault).asset()).transferFrom(
msg.sender, address(this), assets
);
IERC20(IERC4626(vault).asset()).approve(vault, assets);
shares = IERC4626(vault).deposit(assets, receiver);
require(shares >= minSharesOut, "INSUFFICIENT_SHARES_OUT");
}
}
3. Share/Asset Conversion Rounding Direction
Rounding is not a cosmetic concern. Wrong rounding direction creates a systematic drain that attackers can exploit at scale.
The Spec’s Requirement
ERC-4626 vault implementers should be aware of the need for specific, opposing rounding directions across the different mutable and view methods, as it is considered most secure to favor the vault itself during calculations over its users. If (1) it’s calculating how many shares to issue to a user for a certain amount of the underlying tokens they provide, or (2) it’s determining the amount of the underlying tokens to transfer to them for returning a certain amount of shares, it should round down. If (1) it’s calculating the amount of shares a user has to supply to receive a given amount of the underlying tokens, or (2) it’s calculating the amount of underlying tokens a user has to provide to receive a certain amount of shares, it should round up.
In plain terms:
deposit/redeem→ round shares down (user gets less)mint/withdraw→ round shares up (user pays more)
Unfortunately, the wording of the rounding specification from ERC-4626 is confusing, making it easy for developers who are coding vault implementations to misinterpret it.
The Free-Withdrawal Exploit
By rounding down in a withdraw function that should round up, the function presents an opportunity for an attacker to manipulate the contract. If the attacker supplies an amount of asset they would like to withdraw such that the calculated shares amount is rounded down to 0, then they have effectively withdrawn assets for free (remember, Solidity rounds to 0 if the numerator is smaller than the denominator).
// VULNERABLE: withdraw() rounds shares burned DOWN instead of UP
function withdraw(uint256 assets, address receiver, address owner)
public returns (uint256 shares)
{
// BUG: should use Math.Rounding.Ceil, not Floor
shares = assets.mulDiv(totalSupply(), totalAssets(), Math.Rounding.Floor);
// If assets is small enough, shares = 0 → free withdrawal
_burn(owner, shares);
IERC20(asset()).transfer(receiver, assets);
}
// CORRECT: withdraw() burns shares rounded UP (user pays more)
function withdraw(uint256 assets, address receiver, address owner)
public returns (uint256 shares)
{
shares = assets.mulDiv(
totalSupply() + 10 ** _decimalsOffset(),
totalAssets() + 1,
Math.Rounding.Ceil // ← user must provide enough shares
);
require(shares > 0, "ZERO_SHARES");
_burn(owner, shares);
IERC20(asset()).safeTransfer(receiver, assets);
}
Rounding Across All Eight Entry Points
| Function | Calculation | Direction |
|---|---|---|
deposit(assets) | shares minted | Round Down |
mint(shares) | assets pulled | Round Up |
withdraw(assets) | shares burned | Round Up |
redeem(shares) | assets sent | Round Down |
previewDeposit | mirrors deposit | Round Down |
previewMint | mirrors mint | Round Up |
previewWithdraw | mirrors withdraw | Round Up |
previewRedeem | mirrors redeem | Round Down |
If you deposit a deposit worth 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit less than 1 share worth of tokens, then you get 0 shares, and you basically made a donation. This loss asymmetry is especially dangerous in a manipulated exchange rate environment.
4. totalAssets Manipulation Surface
totalAssets() is the pivotal function in ERC-4626: it drives pricing for convertToShares/convertToAssets. Any attacker who can move totalAssets without a corresponding change in totalSupply shifts the exchange rate.
Direct Donation
In common ERC-4626 implementations, the conversion between assets and shares relies on the number of shares issued by the vault and the assets it holds. The amount of shares issued is recorded in the vault contract, but the assets it holds are not. These are recorded as the balance of the vault contract in the asset contract. This balance can be altered by anyone who transfers additional tokens to the vault’s address — an action known as a donation. When a vault unexpectedly receives a donation, the value of each existing share increases.
Collateral Oracle Manipulation
Due to the inherent functionality of ERC-4626 vaults, protocols integrating these tokens typically use the vault’s internal exchange rate (assets per share) as a reference for pricing the tokens. For example, consider a protocol that integrates a tokenized vault such as wUSDM (an ERC-4626 token). To price wUSDM in USD, the protocol calls convertToAssets on the vault contract to determine how many underlying assets one share currently represents, then utilizes a USDM/USD price feed to convert this amount into a final USD price.
If the share tokens are used as collateral, and the share price is artificially raised, an attacker might borrow out more value than they are really allowed to. This is the CREAM attack vector applied to ERC-4626 wrappers.
// VULNERABLE: totalAssets() purely reads spot balance, manipulable
function totalAssets() public view override returns (uint256) {
return IERC20(asset()).balanceOf(address(this));
// Anyone can inflate this with a direct transfer
}
// SAFER: track assets explicitly, ignore unaccounted donations
contract AccountedVault is ERC4626 {
uint256 private _trackedAssets;
function totalAssets() public view override returns (uint256) {
return _trackedAssets;
}
function _deposit(address caller, address receiver, uint256 assets, uint256 shares)
internal override
{
_trackedAssets += assets;
super._deposit(caller, receiver, assets, shares);
}
function _withdraw(address caller, address receiver, address owner, uint256 assets, uint256 shares)
internal override
{
_trackedAssets -= assets;
super._withdraw(caller, receiver, owner, assets, shares);
}
}
Manipulation via Strategy Reporting
When a vault delegates capital to external strategies, totalAssets usually aggregates the vault’s idle balance plus each strategy’s reported balance. A strategy that reports inflated balances — even transiently, during the window of a single transaction — can warp pricing for all operations in that block.
// DANGEROUS: totalAssets trusts strategy.estimatedTotalAssets() without sanity check
function totalAssets() public view override returns (uint256) {
uint256 total = IERC20(asset()).balanceOf(address(this));
for (uint256 i = 0; i < strategies.length; i++) {
total += IStrategy(strategies[i]).estimatedTotalAssets(); // ← manipulable
}
return total;
}
// SAFER: cap single-block increases, add oracle floor/ceiling
function totalAssets() public view override returns (uint256) {
uint256 total = IERC20(asset()).balanceOf(address(this));
for (uint256 i = 0; i < strategies.length; i++) {
uint256 stratAssets = IStrategy(strategies[i]).estimatedTotalAssets();
uint256 maxAssets = _strategyDebt[strategies[i]].mulDiv(
MAX_BPS + MAX_GAIN_BPS, MAX_BPS // e.g., allow max 10% gain per report
);
total += stratAssets > maxAssets ? maxAssets : stratAssets;
}
return total;
}
Common mistakes include treating totalAssets() as simply asset.balanceOf(address(this)) when assets can be sent directly or deployed into strategies, ignoring strategy PnL, fees, or pending accruals, and relying on fragile oracles for complex positions (e.g., LP tokens).
Best practices: include only assets truly attributable to shareholders, carefully reconcile direct transfers and strategy balances, and if using oracles, add sanity checks, TWAPs, and circuit breakers.
5. Withdrawal Queue Security
Vaults that deploy capital into strategies rarely hold all assets idly. When a user calls withdraw, the vault may need to pull capital back from one or more strategies. The order and logic of that pull constitutes the withdrawal queue, and it is one of the most complex state machines in vault design.
Queue Ordering Risks
// Naive withdrawal queue - dangerous ordering
contract QueuedVault is ERC4626 {
address[] public withdrawalQueue; // ordered list of strategies
function _withdraw(
address caller,
address receiver,
address owner,
uint256 assets,
uint256 shares
) internal override {
uint256 idle = IERC20(asset()).balanceOf(address(this));
if (idle >= assets) {
// Happy path: enough idle liquidity
super._withdraw(caller, receiver, owner, assets, shares);
return;
}
// Need to unwind strategies
uint256 remaining = assets - idle;
for (uint256 i = 0; i < withdrawalQueue.length; i++) {
if (remaining == 0) break;
IStrategy s = IStrategy(withdrawalQueue[i]);
uint256 toWithdraw = Math.min(remaining, s.estimatedTotalAssets());
// BUG: no slippage check, no minimum out, trusts strategy's own report
uint256 withdrawn = s.withdraw(toWithdraw);
remaining -= withdrawn;
}
require(remaining == 0, "INSUFFICIENT_LIQUIDITY");
super._withdraw(caller, receiver, owner, assets, shares);
}
}
Problems with the above:
- The vault trusts
estimatedTotalAssets()as exact — the actualwithdrawmay return less due to slippage or locked liquidity. - No
minOutguard on strategy withdrawals; a compromised strategy can return 0 while consuming theremainingbudget. - No check that
withdrawn > 0; a strategy that always returns 0 causes an infinite loop in partially-broken queue states.
// Hardened withdrawal queue
function _unwindStrategies(uint256 needed) internal returns (uint256 freed) {
for (uint256 i = 0; i < withdrawalQueue.length; i++) {
if (freed >= needed) break;
IStrategy s = IStrategy(withdrawalQueue[i]);
uint256 available = s.estimatedTotalAssets();
if (available == 0) continue;
uint256 toRequest = Math.min(needed - freed, available);
uint256 before = IERC20(asset()).balanceOf(address(this));
s.withdraw(toRequest);
uint256 actuallyFreed = IERC20(asset()).balanceOf(address(this)) - before;
// Never trust the strategy's return value — measure actual balance delta
freed += actuallyFreed;
_strategyDebt[address(s)] -= Math.min(_strategyDebt[address(s)], actuallyFreed);
}
}
Async Queues and ERC-7540
ERC-4626 is optimized for atomic deposits and redemptions up to a limit. This limitation does not work well for any smart contract system with asynchronous actions or delays as a prerequisite for interfacing with the vault (e.g., real-world asset protocols, undercollateralized lending protocols, cross-chain lending protocols, liquid staking tokens, or insurance safety modules).
ERC-7540 introduces asynchronous state transitions on top of ERC-4626, significantly expanding the attack surface. Because async flows depend on delayed settlement, oracle callbacks, and administrative fulfillment, misalignment between on-chain and off-chain processes can lock capital, break invariants, or enable administrative abuse.
Strategies with lockups or withdrawal buffers should reflect that in previewWithdraw/previewRedeem and events, and document potential delays.
6. Vault Accounting Invariants
A correctly implemented ERC-4626 vault must maintain a set of hard invariants across all state transitions. Breaking any of them opens a path to theft or insolvency.
Core Invariants
// Invariant test structure (Foundry-style)
// These should be run as invariant tests, not just unit tests
contract VaultInvariantTest {
ERC4626 vault;
// Invariant 1: Sum of user shares equals totalSupply
// ∀ users: Σ balanceOf(user) == totalSupply()
function invariant_shareSupplyMatchesBalances() public view {
// Enforced by ERC-20 itself, but double-check no shares
// are minted/burned outside standard paths
}
// Invariant 2: convertToAssets(convertToShares(x)) <= x
// Depositing x assets and immediately redeeming should yield <= x assets
function invariant_depositRedeemLossy(uint256 assets) public view {
uint256 shares = vault.convertToShares(assets);
uint256 assetsOut = vault.convertToAssets(shares);
assert(assetsOut <= assets); // never profit from roundtrip
}
// Invariant 3: previewDeposit == actual deposit shares
// previewDeposit(assets) MUST equal deposit(assets, receiver) shares
function invariant_previewMatchesActual(uint256 assets, address receiver) public {
uint256 preview = vault.previewDeposit(assets);
uint256 actual = vault.deposit(assets, receiver); // in a fork/simulation
assert(preview == actual);
}
// Invariant 4: maxWithdraw respects actual withdrawability
// redeem(maxRedeem(owner), owner, owner) must not revert
function invariant_maxRedeemDoesNotRevert(address owner) public {
uint256 maxShares = vault.maxRedeem(owner);
if (maxShares > 0) {
vault.redeem(maxShares, owner, owner); // must succeed
}
}
// Invariant 5: totalAssets >= sum of all redeemable assets
// The vault must always be solvent
function invariant_solvency() public view {
uint256 totalRedeemable = vault.convertToAssets(vault.totalSupply());
assert(vault.totalAssets() >= totalRedeemable - 1); // allow 1 wei dust
}
}
Key invariants to test include: convertToAssets(convertToShares(x)) ~= x; sum(userShares) == totalSupply; and totalAssets tracks balances plus strategies.
Consider proving invariants for totalAssets() and conversion functions on critical deployments using formal verification.
Fee Accounting Invariants
In ERC-4626 vaults, fees can be captured during the deposit/mint and/or during the withdraw/redeem steps. In both cases it is essential to remain compliant with the ERC-4626 requirements with regard to the preview functions. For example, if calling deposit(100, receiver), the caller should deposit exactly 100 underlying tokens, including fees, and the receiver should receive a number of shares that matches the value returned by previewDeposit(100).
// Performance fee minting - the safe pattern
function harvest() external onlyKeeper {
uint256 totalBefore = _lastTotalAssets;
uint256 totalNow = _computeTotalAssets(); // reads strategies
if (totalNow > totalBefore) {
uint256 gain = totalNow - totalBefore;
uint256 feeAssets = gain.mulDiv(performanceFeeBps, MAX_BPS);
// Mint shares to fee recipient BEFORE updating _lastTotalAssets
// so the dilution is reflected in the new share price
uint256 feeShares = _convertToShares(feeAssets, Math.Rounding.Floor);
_mint(feeRecipient, feeShares);
}
_lastTotalAssets = totalNow;
}
7. Integration Risks When Protocols Wrap ERC-4626 Vaults
ERC-4626’s built-in features — while not vulnerabilities in themselves — can still lead to critical exploits if not properly managed when integrated into other protocols.
Using Vault Shares as Collateral
Protocols integrating ERC-4626 tokens typically use the vault’s internal exchange rate (assets per share) as a reference for pricing the tokens. The EIP states that “The preview methods return values that are as close as possible to exact as possible. For that reason, they are manipulable by altering the on-chain conditions and are not always safe to be used as price oracles.”
A lending protocol that calls convertToAssets(1e18) in the same transaction as a large donation to the vault will compute an inflated collateral value. This is a read-only reentrancy vector when the vault’s storage is mid-update.
// DANGEROUS: price oracle using live convertToAssets
function getSharePrice(address vault) external view returns (uint256) {
// Manipulable via same-block donation or reentrancy
return IERC4626(vault).convertToAssets(1e18);
}
// SAFER: TWAP or capped rate of change
contract VaultPriceOracle {
struct PriceData {
uint256 pricePerShare;
uint256 timestamp;
}
mapping(address => PriceData) private _lastPrice;
uint256 public constant MAX_DAILY_INCREASE = 50; // 5% in basis points per day
function updatePrice(address vault) external {
uint256 current = IERC4626(vault).convertToAssets(1e18);
PriceData storage last = _lastPrice[vault];
if (last.timestamp > 0) {
uint256 elapsed = block.timestamp - last.timestamp;
uint256 maxIncrease = last.pricePerShare.mulDiv(
MAX_DAILY_INCREASE * elapsed,
MAX_BPS * 1 days
);
// Cap upward movements; allow instant downward (losses must flow through)
if (current > last.pricePerShare + maxIncrease) {
current = last.pricePerShare + maxIncrease;
}
}
_lastPrice[vault] = PriceData(current, block.timestamp);
}
function getPrice(address vault) external view returns (uint256) {
return _lastPrice[vault].pricePerShare;
}
}
To effectively mitigate these risks, protocols integrating with ERC-4626 tokens must implement robust security measures. These may include adopting a Correlated-Assets Price Oracle (CAPO) to prevent rapid and excessive price inflation, as well as a rapid-response kill-switch mechanism to swiftly address severe volatility and manipulation attempts.
Vault-of-Vaults Wrapping
When a protocol wraps an ERC-4626 vault into another ERC-4626 vault (a “vault-of-
vaults”) — nested vaults compound every vulnerability present in ERC-4626. The outer vault’s totalAssets() calls the inner vault’s convertToAssets(), which may itself read from the inner vault’s totalAssets(). If the inner vault is manipulated — through donation, rebasing, or price oracle tampering — the outer vault’s share price reflects the distortion amplified by the nesting depth.
The security implications are direct: an attacker who can move the inner vault’s share price by X% moves the outer vault’s share price by a similar magnitude, with potentially larger absolute impact if the outer vault holds more capital. Any protocol integrating ERC-4626 vaults as collateral must treat the vault’s share price as an oracle — and apply the same manipulation-resistance analysis that applies to any price oracle.
ERC-4626 Security Audit Checklist
Share price and rounding
-
convertToShares(deposit path) rounds down — user receives fewer shares -
convertToAssets(withdrawal path) rounds down — user receives fewer assets -
previewWithdrawrounds up — user must burn more shares -
previewMintrounds up — user must pay more assets - Virtual shares offset (
_decimalsOffset) is implemented to prevent the inflation attack
Donation and inflation attack
-
totalAssets()does not read rawbalanceOf(address(this))without a virtual offset or internal ledger - First-depositor path is protected: zero
totalSupplycase cannot be manipulated to round victim shares to zero - Direct token donations cannot meaningfully move the share price in a single transaction
Integration as collateral
- Protocols using ERC-4626 shares as collateral treat the share price as an oracle
- The share price oracle uses a TWAP or rate limiter — not raw
convertToAssetsat the current block - A CAPO (Correlated Assets Price Oracle) or equivalent is used to bound share price growth per time period
Nested vaults
- Nested vault depth is bounded and explicitly documented
- Share price manipulation in an inner vault and its propagation to the outer vault is analyzed
- Outer vault’s collateral valuation accounts for the amplified manipulation surface
Operational security
- A pause mechanism exists for emergency response
- The vault owner is a multisig or governance contract, not a bare EOA
- All permissioned functions have a timelock appropriate to the protocol’s TVL