> For the complete documentation index, see [llms.txt](https://docs.curvance.com/app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.curvance.com/app/developer-docs/earn-vaults.md).

# Earn Vaults

### Overview

Earn Vaults are ERC4626-style vaults implemented by `LendingOptimizer.sol`. Each vault accepts one underlying ERC20 asset and allocates that asset across a configured list of Curvance `BorrowableCToken` markets that use the same underlying.

Users deposit the underlying asset into the vault and receive optimizer shares. The vault deposits the underlying into approved cToken markets as lending liquidity. Borrowers inside those isolated markets can use that liquidity, and interest earned by the cToken positions increases the vault's tracked assets and share price after accrual.

Earn Vaults do not borrow, post collateral, or choose a new target allocation on each user deposit. Deposits and exits route pro-rata across the vault's current market balances. Allocation percentages change through `rebalance()`, which can be called by an authorized harvester or market-permissioned address.

### Core Architecture

`LendingOptimizer` combines three main responsibilities:

* ERC4626-style accounting for deposits, mints, withdrawals, redemptions, share balances, previews, and share-to-asset conversion.
* Multi-market allocation across up to `MAX_MARKETS` approved `BorrowableCToken` markets for the same underlying asset.
* Authorized operations for market onboarding, cap updates, rebalancing, deposit pausing, fee updates, and idle-asset recovery.

After deployment, deposits are unavailable until `initializeDeposits()` is called. Initialization deposits a small base reserve into one approved market and mints dead shares to `address(0)` so the first live depositor cannot manipulate the initial exchange rate.

### Key Integrations

* `IERC20`: The underlying asset accepted by the vault. Users approve the optimizer before `deposit()` or `mint()`.
* `BorrowableCToken`: Each approved market receives the same underlying asset from the optimizer. The optimizer validates that each cToken has the matching underlying, is borrowable, has a registered Market Manager, and is listed in that Market Manager.
* `MarketManagerIsolated`: The optimizer reads cToken mint pause state through `actionsPaused(cToken)` and reads market-wide redemption pause state through `redeemPaused()`.
* `ICentralRegistry`: Permission checks come from the central registry. Market permissions govern setup and market management, harvester or market permissions govern rebalancing, DAO permissions govern `skim()`, and the DAO address receives performance fee shares.
* `OptimizerReader`: Offchain and frontend integrations can use this reader for market data, user data, estimated optimizer APY, and proposed rebalance actions plus allocation bounds.

### State Management

Important optimizer state:

| State                       | Unit                   | Meaning                                                                                                                        |
| --------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `approvedCTokensList`       | addresses              | Ordered list of cToken markets approved for allocation.                                                                        |
| `allocationCaps(cToken)`    | WAD                    | Maximum post-rebalance allocation for a market. Constructor and admin inputs use BPS, then the contract stores the cap as WAD. |
| `fee`                       | BPS                    | Performance fee charged on yield above the exchange-rate high watermark.                                                       |
| `exchangeRateHighWatermark` | WAD                    | Highest share price used for performance fee accounting.                                                                       |
| `mintPaused`                | enum-like `uint8`      | `0` means uninitialized, `1` means deposits active, and `2` means deposits paused.                                             |
| `_totalAssets`              | underlying asset units | Cached total assets tracked across approved markets.                                                                           |

Allocation caps are configured in BPS and stored in WAD. `10000` BPS equals `100%`, and `1e18` WAD equals `100%`. The sum of allocation caps must be at least `100%` when the optimizer is deployed, when a cap decrease would reduce total caps, and when a market is removed.

### Data Flow

<figure><img src="/files/G68eyDJGLFP2JRZx4AiF" alt=""><figcaption></figcaption></figure>

#### Deployment and Activation

1. The optimizer is deployed with one underlying asset, a central registry, initial approved cTokens, initial allocation caps in BPS, and a performance fee in BPS.
2. The constructor validates that the approved market list is non-empty, has at most `MAX_MARKETS` entries, has no duplicates, and has a matching cap for each market.
3. Each cToken must use the optimizer's underlying asset, be borrowable, point to a registered Market Manager, and be listed in that Market Manager.
4. The initial caps must each be greater than `0`, no more than `10000` BPS, and sum to at least `10000` BPS.
5. A market-permissioned address calls `initializeDeposits(targetMarket)`.
6. `initializeDeposits()` pulls the base underlying reserve from the caller, deposits it into the target market, mints dead shares to `address(0)`, and changes `mintPaused` from `0` to `1`.

#### Deposits and Mints

1. The user approves the optimizer to spend the underlying asset.
2. The user calls `deposit(assets, receiver)` or `mint(shares, receiver)`.
3. The optimizer rejects zero-sized deposits or mints, uninitialized vaults, paused vault deposits, and deposits when any approved market has minting paused.
4. The optimizer accrues all approved markets before routing the user action.
5. The optimizer pulls underlying from the caller.
6. The optimizer splits the deposit pro-rata across current market balances and deposits into the approved cToken markets.
7. The receiver gets optimizer shares.

`deposit()` mints shares from the recoverable value actually tracked after cToken deposit rounding. `mint()` computes the required assets for exact shares and uses a conversion roundtrip so the new position can cover the requested share amount.

#### Withdrawals and Redemptions

1. The user calls `withdraw(assets, receiver, owner)` or `redeem(shares, receiver, owner)`.
2. If the caller is not the `owner`, the optimizer spends optimizer-share allowance from `owner`.
3. The optimizer rejects zero-asset exits and rejects exits while any approved market has redemption paused.
4. The optimizer accrues all approved markets before routing the exit.
5. The optimizer withdraws pro-rata across approved markets, capped by each market's idle liquidity.
6. If total idle liquidity across markets cannot satisfy the requested exit, the transaction reverts.
7. The optimizer burns shares and transfers underlying assets to `receiver`.

`withdraw()` returns the shares burned for a target asset amount. `redeem()` returns the assets withdrawn for a target share amount. Both paths re-sync `_totalAssets` from the post-withdrawal cToken balances.

#### Rebalancing

1. An offchain keeper or bot computes one `ReallocationAction` per approved market, usually through `OptimizerReader.optimalRebalance()`.
2. An authorized harvester or market-permissioned address calls `rebalance(actions, bounds)`.
3. The optimizer accrues all markets and charges any performance fee before rebalancing.
4. The optimizer processes withdrawals first. Negative `assetsOrBps` values are withdrawals.
5. The optimizer processes deposits second. Positive `assetsOrBps` values are deposits.
6. Declared withdrawal assets must equal declared deposit assets.
7. The optimizer verifies that every post-rebalance market allocation is at or below its allocation cap and within the caller-specified `AllocationBound`.
8. The optimizer updates `_totalAssets` from the post-rebalance balances and emits `Rebalanced`.

The `actions` and `bounds` arrays must match the exact order and length of `approvedCTokensList`. Include zero-value actions for markets that do not need a move.

#### Market Removal

1. A market-permissioned address calls `removeApprovedAsset(cTokenToRemove, removeActions, bounds)`.
2. The optimizer requires more than one approved market and requires an explicit reallocation plan.
3. The market being removed must be approved and must not be paused for redemptions.
4. Remaining allocation caps must still sum to at least `100%`.
5. The optimizer redeems its full cToken position from the removed market.
6. The optimizer redistributes redeemed assets according to positive BPS values in `removeActions`; the values must sum to exactly `10000`.
7. Reallocation targets must be approved markets, cannot be the removed market, cannot be duplicated, and cannot have minting paused.
8. The approved market list uses swap-and-pop removal, so post-removal ordering can change. `bounds` must match the final list order.

#### Accrual and Fees

State-changing user and keeper flows call `_accrueIfNeeded()` before routing. Accrual calls `accrueIfNeeded()` on every approved cToken, refreshes `_totalAssets`, and charges performance fees only when the current exchange rate is above `exchangeRateHighWatermark`.

Performance fees are minted as optimizer shares to `centralRegistry.daoAddress()`. The fee is configured in BPS and capped by `MAX_FEE_BPS`, which is `5000`.

### Security and Risk Considerations

* Initialization is required. Before `initializeDeposits()`, `mintPaused` is `0`, `maxDeposit()` and `maxMint()` return `0`, and deposit paths revert.
* Deposits are globally blocked for the optimizer when any approved market has cToken minting paused.
* Withdrawals and redemptions are globally blocked for the optimizer when any approved market has redemptions paused.
* Withdrawals and redemptions depend on idle liquidity in the underlying cToken markets. A cached maximum value can be higher than what can be withdrawn in the current transaction if market liquidity is insufficient.
* Previews and conversions use cached `_totalAssets`. They are estimates, not settlement guarantees. State-changing entrypoints accrue markets before routing.
* `rebalance()` is the normal path for changing allocation percentages. User deposits and exits route against the current allocation.
* Allocation bounds protect rebalances against state changes between offchain computation and onchain execution.
* The optimizer validates post-rebalance allocations against configured caps.
* Performance fees are taken only on yield above the high watermark.
* Optimizer-specific state-changing entrypoints use `nonReentrant`.
* Idle underlying sitting directly on the optimizer is treated as excess and can be recovered by the DAO through `skim()`.

### Integration Considerations

* Treat each Earn Vault as a vault for one underlying asset. Do not send a different token to `deposit()` or `mint()`.
* Load vault, underlying, cToken, and reader addresses from the deployment registry for the chain you are on. Do not hardcode live addresses in reusable integrations.
* Check `asset()` before building a deposit form so the frontend uses the right ERC20 decimals and allowance target.
* Check `maxDeposit(receiver)` and `maxMint(receiver)` before showing deposit availability. Both return `0` when deposits are not available.
* Check `previewDeposit(assets)` or `previewMint(shares)` for estimates, but label them as estimates because state-changing paths accrue and route through cToken markets.
* Check `maxWithdraw(owner)` and `maxRedeem(owner)` before showing exit availability, then handle `LendingOptimizer__InsufficientLiquidity()` on execution.
* For fresh share price, use `exchangeRateUpdated()` or call `accrueIfNeeded()` before `exchangeRate()`. Both fresh paths update state.
* Use `OptimizerReader.getOptimizerMarketData()` for market-level views when a state update is acceptable. It calls `exchangeRateUpdated()` on each optimizer.
* Use `OptimizerReader.getOptimizerUserData()` when only user share balance and cached redeemable assets are needed.
* Use `OptimizerReader.optimalRebalance(optimizer, slippageBps)` to construct `rebalance()` calldata, then review the returned actions and bounds before submission.

### User Interaction Functions

#### Structs

Contract: `LendingOptimizer.sol`

```solidity
struct ReallocationAction {
    IBorrowableCToken cToken;
    int256 assetsOrBps;
}

struct AllocationBound {
    address cToken;
    uint256 minBps;
    uint256 maxBps;
}
```

`ReallocationAction.assetsOrBps` has two meanings:

* In `rebalance()`, positive values deposit assets and negative values withdraw assets.
* In `removeApprovedAsset()`, values are positive BPS percentages and must sum to `10000`.

`AllocationBound` values are BPS percentages and must match the current approved market order required by the function being called.

### Deposits

#### **deposit()**

**Description:** Deposits an exact amount of underlying assets and mints optimizer shares to `receiver`.

**Contract:** `LendingOptimizer.sol`

**Function signature:**

```solidity
function deposit(uint256 assets, address receiver) public returns (uint256 shares);
```

**Inputs:**

<table><thead><tr><th width="122">Type</th><th width="114">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint256</code></td><td><code>assets</code></td><td>Amount of underlying assets to deposit.</td></tr><tr><td><code>address</code></td><td><code>receiver</code></td><td>Account that receives optimizer shares.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="114">Type</th><th width="133">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint256</code></td><td><code>shares</code></td><td>Optimizer shares minted to <code>receiver</code>.</td></tr></tbody></table>

**Events:**

```solidity
Deposit(address indexed by, address indexed owner, uint256 assets, uint256 shares)
PerformanceFeeAccrued(uint256 feeShares, address indexed recipient) if accrual charges a performance fee
```

{% hint style="info" %}
Caller must approve the optimizer to spend at least `assets` of the underlying token. Reverts if `assets == 0`, the optimizer is uninitialized, deposits are paused, any approved market has minting paused, or the tracked deposit value mints zero shares. Deposits route pro-rata across current market balances.
{% endhint %}

#### **mint()**

**Description:** Mints an exact amount of optimizer shares by depositing the required amount of underlying assets.

**Contract:** `LendingOptimizer.sol`

**Function signature:**

```solidity
function mint(uint256 shares, address receiver) public returns (uint256 assets);
```

**Inputs:**

<table><thead><tr><th width="128">Type</th><th width="100">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint256</code></td><td><code>shares</code></td><td>Exact optimizer shares to mint.</td></tr><tr><td><code>address</code></td><td><code>receiver</code></td><td>Account that receives optimizer shares.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="119">Type</th><th width="102">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint256</code></td><td><code>assets</code></td><td>Underlying assets pulled from the caller.</td></tr></tbody></table>

**Events:**

```solidity
Deposit(address indexed by, address indexed owner, uint256 assets, uint256 shares)
PerformanceFeeAccrued(uint256 feeShares, address indexed recipient) if accrual charges a performance fee
```

{% hint style="info" %}
Caller must approve the optimizer to spend the required underlying assets. Reverts if `shares == 0`, the optimizer is uninitialized, deposits are paused, any approved market has minting paused, or cToken rounding makes the tracked assets insufficient for the requested shares.
{% endhint %}

### Withdrawals

#### **withdraw()**

**Description:** Withdraws an exact amount of underlying assets to `receiver` and burns the required optimizer shares from `owner`.

**Contract:** `LendingOptimizer.sol`

**Function signature:**

```solidity
function withdraw(uint256 assets, address receiver, address owner) public returns (uint256 shares);
```

**Inputs:**

<table><thead><tr><th width="124">Type</th><th width="116">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint256</code></td><td><code>assets</code></td><td>Exact underlying assets to withdraw.</td></tr><tr><td><code>address</code></td><td><code>receiver</code></td><td>Account that receives the underlying assets.</td></tr><tr><td><code>address</code></td><td><code>owner</code></td><td>Account whose optimizer shares are burned.</td></tr></tbody></table>

**Return data:**

| Type      | Name     | Description                           |
| --------- | -------- | ------------------------------------- |
| `uint256` | `shares` | Optimizer shares burned from `owner`. |

**Events:**

```solidity
Withdraw(address indexed by, address indexed to, address indexed owner, uint256 assets, uint256 shares)
// if accrual charges a performance fee
PerformanceFeeAccrued(uint256 feeShares, address indexed recipient)
hdraw(address indexed by, address indexed to, address indexed owner, uint256 assets, uint256 shares)
PerformanceFeeAccrued(uint256 feeShares, address indexed recipient) if accrual charges a performance fee
```

{% hint style="info" %}
If `msg.sender != owner`, the caller must have optimizer-share allowance from `owner`.

Reverts if `assets == 0`, any approved market has redemptions paused, or total available cToken liquidity cannot satisfy the withdrawal.
{% endhint %}

#### **redeem()**

**Description:** Burns an exact amount of optimizer shares from `owner` and sends the withdrawn underlying assets to `receiver`.

**Contract:** `LendingOptimizer.sol`

**Function signature:**

```solidity
function redeem(uint256 shares, address receiver, address owner) public returns (uint256 assets);
```

**Inputs:**

<table><thead><tr><th width="121">Type</th><th width="126">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint256</code></td><td><code>shares</code></td><td>Exact optimizer shares to burn.</td></tr><tr><td><code>address</code></td><td><code>receiver</code></td><td>Account that receives the underlying assets.</td></tr><tr><td><code>address</code></td><td><code>owner</code></td><td>Account whose optimizer shares are burned.</td></tr></tbody></table>

**Return data:**

<table><thead><tr><th width="106">Type</th><th width="104">Name</th><th>Description</th></tr></thead><tbody><tr><td><code>uint256</code></td><td><code>assets</code></td><td>Underlying assets sent to <code>receiver</code>.</td></tr></tbody></table>

**Events:**

```solidity
Withdraw(address indexed by, address indexed to, address indexed owner, uint256 assets, uint256 shares)
ormanceFeeAccrued(uint256 feeShares, address indexed recipient) if accrual charges a performance fee
```

{% hint style="info" %}
If `msg.sender != owner`, the caller must have optimizer-share allowance from `owner`. Reverts if the redeemed shares convert to zero assets, any approved market has redemptions paused, or total available cToken liquidity cannot satisfy the redemption.
{% endhint %}

### Preview and Limit Views

**Contract:** `LendingOptimizer.sol`

| Function                                                                         | Description                                                                                                      |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `function previewDeposit(uint256 assets) public view returns (uint256 shares);`  | Returns the cached-share estimate for an asset deposit.                                                          |
| `function previewMint(uint256 shares) public view returns (uint256 assets);`     | Returns the cached-asset estimate for exact share minting.                                                       |
| `function previewWithdraw(uint256 assets) public view returns (uint256 shares);` | Returns the cached-share estimate for an asset withdrawal.                                                       |
| `function previewRedeem(uint256 shares) public view returns (uint256 assets);`   | Returns the cached-asset estimate for share redemption.                                                          |
| `function maxDeposit(address) public view returns (uint256);`                    | Returns `type(uint256).max` when deposits are active and no approved market has minting paused, otherwise `0`.   |
| `function maxMint(address) public view returns (uint256);`                       | Returns `type(uint256).max` when deposits are active and no approved market has minting paused, otherwise `0`.   |
| `function maxWithdraw(address owner) public view returns (uint256);`             | Returns `convertToAssets(balanceOf(owner))` unless any approved market has redemptions paused, then returns `0`. |
| `function maxRedeem(address owner) public view returns (uint256);`               | Returns `balanceOf(owner)` unless any approved market has redemptions paused, then returns `0`.                  |

{% hint style="info" %}
Preview and conversion functions read cached `_totalAssets`. `maxWithdraw()` and `maxRedeem()` do not fully model cToken idle liquidity. Execution can still revert if the approved markets cannot supply the exit.
{% endhint %}

#### Accounting and Market Views

**Contract:** `LendingOptimizer.sol`

| Function                                                                         | Description                                                                                       |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `function asset() public view returns (address);`                                | Returns the underlying ERC20 asset.                                                               |
| `function name() public view returns (string memory);`                           | Returns the optimizer share token name.                                                           |
| `function symbol() public view returns (string memory);`                         | Returns the optimizer share token symbol.                                                         |
| `function balanceOf(address owner) public view returns (uint256);`               | Returns an account's optimizer share balance.                                                     |
| `function totalAssets() public view returns (uint256);`                          | Returns cached `_totalAssets`.                                                                    |
| `function convertToShares(uint256 assets) public view returns (uint256 shares);` | Converts assets to optimizer shares using cached accounting.                                      |
| `function convertToAssets(uint256 shares) public view returns (uint256 assets);` | Converts optimizer shares to assets using cached accounting.                                      |
| `function exchangeRate() public view returns (uint256);`                         | Returns cached share price in WAD. Returns WAD if supply is zero.                                 |
| `function exchangeRateUpdated() public returns (uint256);`                       | Accrues approved markets, charges fees if applicable, and returns the updated share price in WAD. |
| `function accrueIfNeeded() external;`                                            | Accrues approved markets and absorbs yield into optimizer accounting.                             |
| `function numApprovedMarkets() external view returns (uint256);`                 | Returns the approved market count.                                                                |
| `function getApprovedMarkets() external view returns (address[] memory);`        | Returns the approved market list.                                                                 |
| `function approvedCTokensList(uint256 index) external view returns (address);`   | Returns the approved market at `index`.                                                           |
| `function allocationCaps(address cToken) external view returns (uint256);`       | Returns the market allocation cap in WAD.                                                         |
| `function fee() external view returns (uint256);`                                | Returns the performance fee in BPS.                                                               |
| `function exchangeRateHighWatermark() external view returns (uint256);`          | Returns the fee high watermark in WAD.                                                            |
| `function mintPaused() external view returns (uint8);`                           | Returns `0`, `1`, or `2` for uninitialized, active, or paused deposit state.                      |
| `function MAX_FEE_BPS() external view returns (uint256);`                        | Returns the maximum allowed performance fee in BPS.                                               |
| `function MAX_MARKETS() external view returns (uint256);`                        | Returns the maximum number of approved markets.                                                   |
| `function centralRegistry() external view returns (ICentralRegistry);`           | Returns the central registry used by the optimizer.                                               |

**Events:**

```solidity
// from exchangeRateUpdated()
// or accrueIfNeeded() if accrual charges a fee
PerformanceFeeAccrued(uint256 feeShares, address indexed recipient) 
```

{% hint style="info" %}
`exchangeRateUpdated()` and `accrueIfNeeded()` are state-changing. Use `exchangeRate()` when a cached view is acceptable.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.curvance.com/app/developer-docs/earn-vaults.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
