# Intro

Enzyme Protocol is an Ethereum-based protocol for decentralized on-chain asset management. It is a protocol for people or entities to manage their wealth & the wealth of others within a customizable and safe environment. Enzyme empowers anyone to set up, manage and invest in customized on-chain investment vehicles.<br>

The purpose of this document is to provide a high-level overview of the aims, architectural decisions, governance,  GitHub repo, and other idiosyncrasies of the “Sulu” (v4) protocol release.


# GitHub repo

All audited, deployed contracts are located in the `current` branch of our public repo: <https://github.com/enzymefinance/protocol>

Unaudited code is located in the `next` branch of our private repo (must request access).

## Contracts

All contracts used for both production and testing are located in `contracts/`

`mocks/` - Mock contracts used only by tests

`persistent/` - Production contracts that will not be changed after their first deployment. These are contracts available to be used across multiple releases, e.g., the overarching structure that facilitates fund migration from an old release to the current release.

`release/` - Production contracts whose lifetime does not extend beyond this release.

`test/` - Other contracts used only by tests

## Tests

All tests are in the `tests/` directory.


# Persistent

## Core

### Philosophy

Enzyme has upgradable vaults, structured to require fund owners to opt-in (migrate) to subsequent major versions.

\[Note: previously, the core contracts also gave investors a period during which to opt-out by redeeming shares before an upgrade is final. As vault owners are now considered trusted in core, this period has been eliminated, thus any opportunity to opt-out must be created peripherally.]

### Approach

Essential state lives in a `VaultProxy` , which is moved between releases by upgrading its `VaultLib` and permissioned `accessor` via a global `Dispatcher` contract.

The essential state for a fund is:

* holdings
* shares
* roles

### VaultProxy

The "essential state" described above lives in per-fund `VaultProxy` contract instances, which are upgradable contracts following the EIP-1822 proxy pattern.

The `VaultProxy` specifies a `VaultLib` as its target logic, and these logic contracts are deployed per release, and swapped out during a migration.

A `VaultLibBaseCore` contract defines the absolutely essential state and logic for every VaultProxy. This includes:

* a standard ERC20 implementation called `SharesTokenBase`&#x20;
* the functions required of a `IProxiableVault` called by the `Dispatcher`&#x20;
* core access control roles: `owner`, `accessor`, `creator`, and `migrator`&#x20;

The `owner` is the fund's owner.

The `migrator` is an optional role to allow a non-owner to migrate the fund.

The `creator` is the `Dispatcher` contract, which is allowed to update the `accessor` and `vaultLib` values.

The `accessor` is the primary account that can make state-changing calls to the `VaultProxy` . In practice, this is the release-level contract that interacts with a vault's assets, updates shares balances, etc.

This extremely abstract interface - in which a `VaultProxy` needs no knowledge about a release other than which caller can write state - allows for nearly limitless possibilities for release-level architecture.

### Dispatcher

&#x20;An overarching, non-upgradable `Dispatcher` contract is charged with:

* deploying new instances of `VaultProxy`&#x20;
* migrating a `VaultProxy` from an old release to the current release
* maintaining global state such as the current release, the global owner (i.e., the Enzyme Council) and the default `symbol` value for fund shares tokens

The `Dispatcher` stores the `currentFundDeployer` (a generic reference to the latest release's contract that is responsible for deploying and migrating funds), and only a `msg.sender` with that value is allowed to call functions to deploy or migrate a `VaultProxy` .&#x20;

This release-level `FundDeployer` can optionally implement migration hooks provided by `IMigrationHookHandler`, which give the release an opportunity to run arbitrary logic as the `Dispatcher` invokes those hooks at every step of the migration process.

As was the case described above with the `VaultLibBaseCore` , this abstracted notion of a `FundDeployer` - in which the `Dispatcher` only cares about its identity for access and for optional callbacks - is totally unrestrictive to the shape of the release-level protocol.

\---

Release-level contracts, then, are mostly arbitrary from the standpoint of these persistent contracts, offering maximum flexibility for future iterations and changes.

## Other

In addition to the core persistent architecture that endures across all releases, there are also persistent components whose lifetimes can span one or many releases.

Used in this release:

### **Protocol Fee Reserve**

The `ProtocolFeeReserveProxy` serves as a repository that stores collected fees, with upgradable logic for what can be done with the collected assets.

### External Position Factory

Deploys new `ExternalPositionProxy` instances, which are upgradable proxy contracts used to manage positions that live outside of the vault, e.g., CDPs.


# Release

## Core

### FundDeployer

The `FundDeployer` can be considered the top-level contract of the release. It serves two roles.

Its primary purpose is the gateway to creating, migrating, and reconfiguring funds.

This is the contract that the `Dispatcher` considers as the `currentFundDeployer`, thus allowing it to deploy and migrate `VaultProxy` instances.

It also handles intra-release migration (completely changing fund config by swapping out a new ComptrollerProxy, referred to as "reconfiguration"), mimicking the `Dispatcher` 's paradigm for signaling and executing timelocked "reconfiguration requests."

The `FundDeployer` deploys configuration contracts ( `ComptrollerProxy` ) per-fund that are then attached to `VaultProxy` instances (more in the next section).

The `FundDeployer` is also used as a release-wide registry for limiting allowed values for permissioned calls in a uniform way for all funds.

There is 1 shared `FundDeployer` for the release.

### ComptrollerProxy

A `ComptrollerProxy` is deployed per-fund, and it is the canonical contract for interacting with a fund in this release. It stores core release-level configuration and is attached to a `VaultProxy` via the latter's `accessor` role.

All state-changing calls to the `VaultProxy` related to the fund's holdings and shares balances must thus pass through the `ComptrollerProxy`, making it a critically important bottleneck of access control.

The storage and logic of the `ComptrollerProxy` are defined by the `ComptrollerLib` and its associated libraries. It is not upgradable, but a fund can be "reconfigured" by deploying a new `ComptrollerProxy` instance to replace the previous one, via the `FundDeployer`.

### VaultLib

The `VaultLib` contract contains the storage layout, event signatures, and logic for `VaultProxy` instances that are attached to this release.

There is 1 shared `VaultLib` for the release.

## Extensions

Extensions extend the logic of the core contracts by adding additional kinds of functionality.

They are semi-trusted, in that they are selectively granted access to state-changing calls to `VaultProxy` instances.

In order to make such a state-changing call, two conditions must be met:

1. The Extension function must have been called by a `ComptrollerProxy` via a function with the `allowsPermissionedVaultAction` modifier, which opens the calling `ComptrollerProxy` to `VaultProxy` state changes.
2. The state-changing call must pass back through the `ComptrollerProxy`, and is delegated to the `PermissionedVaultActionLib` to determine whether the calling Extension is allowed to perform such an action.

This paradigm assures that an Extension can only perform a state-changing action to a `VaultProxy` if it was called by that `VaultProxy`'s corresponding `ComptrollerProxy` and if the Extension is permitted to make such a change at all.

Each extension manages a particular type of plugin:

* `IntegrationManager` - adapters
* `PolicyManager` - policies
* `FeeManager` - fees
* `ExternalPositionManager` - external positions

Plugins do not have authority to act on a vault's state, unless explicitly granted via the core system. e.g., a `SynthetixAdapter` can trade on Synthetix on behalf of a vault by enabling that action via a `ComptrollerLib.vaultCallOnContract()`

In this release, there are four Extensions. All funds share one contract per Extension.

### PolicyManager

The `PolicyManager` allows a fund owner to set up and manage a stack of "policies," which are used to perform bespoke validations during various function calls.

The protocol invokes "policy hooks" during actions where it is deemed important to give the fund owner customizability of what should be allowed/disallowed according to their particular needs.

Policies define which hooks they implement. When a hook is reached, it loops over all policies that a fund has enabled that run on that particular hook and validate whether the particular call is allowed.

See `IPolicyManager.PolicyHook` for the available hooks.

Policies themselves can only fail or pass, so the `PolicyManager` has no need or access to state-changing vault actions.

All policies are addable during fund creation, migration, or reconfiguration.

A policy defines for itself whether or not it is updatable or removable.

A policy can be added at any time, unless it runs on a policy hook that restricts current investors (i.e., shares redemption and shares transfer).

### FeeManager

The `FeeManager` allows for "fees" to dictate the minting, burning, or transferal of fund shares, according to their internal logics.

Like policies, fees implement "fee hooks," which are invoked during particular actions.

Fees can either settle and payout immediately, or they can accrue upon settlement as "shares outstanding," and only unlock for payout when specific conditions (defined by the fee) are met.

Fees can only be added or removed during fund setup (creation / migration / reconfiguration).

### IntegrationManager

The `IntegrationManager` allows:

* exchanging a fund's assets for other assets via "adapters" to DeFi protocols (e.g., Uniswap, Kyber, Compound, Chai)
* tracking assets
* untracking assets&#x20;

Each of these actions contains a policy hook.

### ExternalPositionManager

The `ExternalPositionManager` allows creating and managing "external positions," proxy contracts that represent non-ERC20 holdings of the fund, e.g., a Compound CDP.

The `ExternalPositionManager` maintains a registry of "libs" and "parsers" per external position, which serves as the "beacon" in this release for the `ExternalPositionProxy` 's specific beacon proxy pattern.

Each of the available actions for interacting with an external position contains a policy hook.

See "External Positions" section.

## Plugins

Each of the Extensions above make use of plugins. The `IntegrationManager` uses "adapters", the `PolicyManager` uses "policies", and the `FeeManager` uses "fees", and the `ExternalPositionManager` uses "external positions."

Arbitrary (third party) plugins are allowed for fees, policies, and integrations. Fund owners can decide whether or not to use third party plugins, and investors will be able to determine if fund configurations are safe for their risk tolerance.

## Infrastructure

In addition to "core" and "extension" release-level contracts, there is a broad category of "infrastructure" contracts, which are misc dependencies of the release-level protocol. Unlike extensions, they do not receive any permissions to alter fund state.

### AssetFinalityResolver

The `AssetFinalityResolver` settles Synths in an efficient manner for operations that depend on accurate Synth balances.

### **Gas Relayer**

This release allows for funds to optionally use Gas Station Network relayers to pay for calls to specific contracts and functions.

See "Gas Relayer" section.

### **ProtocolFeeTracker**

The `ProtocolFeeTracker` tracks protocol fee payments and is queried to determine the amount of shares that a fund should mint to the `ProtocolFeeReserve` to bring its fees up-to-date.

See "Protocol Fees" section.

### ValueInterpreter

The `ValueInterpreter` is the single point of aggregation of various "price feeds" (an additional type of "plugin" that is only managed by the Enzyme Technical Committee) that are used to calculate the value of one or many input asset amounts in terms of an output asset.

There are two categories of assets in this release:

* "primitives" - assets for which we have rates via Chainlink-like aggregators that are either quoted in ETH or USD
* "derivatives" - assets for which we have rates via custom price feeds that are quoted in one or multiple underlying assets (e.g., Compound cTokens, Uniswap v2 pool tokens, etc)

The `ValueInterpreter` determines whether an asset is a primitive or derivative, and executes logic to use corresponding price feeds to determine the value in the output asset.

## Interfaces

All interfaces to external contracts are contained in the `release/interfaces/` directory.

Interfaces for internal contracts (e.g., `IFundDeployer` ) are kept beside the contracts to which they refer. These are narrow interfaces that only contain the functions required by other non-plugin, release-level contracts (i.e., those in the "core" and "extensions" sections above).


# End users

There are two categories of end users of the Enzyme Protocol: fund managers and investors.

## Fund managers

There are three primary fund management roles in this release, all of which are stored on the `VaultProxy` and thus will persist to subsequent releases (if those releases decide to use them):

* Owner
* Migrator
* Asset Manager

### Owner

Each fund has one owner, who can perform any administrative action on the fund.

Ownership can be changed via a pair of nominate-claim transactions.

Owners are considered fully-trusted.

### **Migrator**

Each fund can have a single migrator, who can call any migration or reconfiguration action on the `FundDeployer`.

Only the owner can set or unset a migrator.

### **Asset Manager**

Each fund can have many asset managers, who can call any action on the `IntegrationManager` or `ExternalPositionManager` , and also buyback protocol fee shares. Allowed calls to the `IntegrationManager` and `ExternalPositionManager` can be further narrowed per-asset manager via policies.

Only the owner can add or remove asset managers.

## Investors

A fund can theoretically have unlimited investors, who get exposure to a fund's performance by buying, redeeming, or receiving a transfer of fund shares.

## Relationship between owners, asset managers, and investors

Different funds have different needs and trust assumptions. The core contracts are left as unrestrictive as possible by default, with configuration options, policies, and peripheral tooling available to help craft bespoke levels of trust.

Fund owners are considered fully-trusted by all contracts created and configured by the Enzyme team. Any trust limitations must be done externally via peripheral contracts (e.g., timelocked owner contracts).

Fund owners delegate portfolio management actions to asset managers, whose available actions and action results can be restricted by fund owners. Ultimately, it is the responsibility of the fund owner to adequately restrict asset manager permissions to achieve their desired trust threshold.

Similarly, roles in peripheral contracts (e.g., executors of deposit and redemption queues) are assigned by the fund owner, who must assess whether the role is safe to assign to a given account.

It is assumed that investors will continuously assess their own trust of the fund owner and setup, as-needed.


# Administrators

There are two primary parties who administrate privileged functions in Enzyme Protocol: Avantgarde Core (deployer) and the Enzyme Council (admin).

## Avantgarde Core (deployer)

As the lead developer of the protocol, Avantgarde Core deploys all contracts, and configures all of them prior to taking a release live. Once the release is taken live, full access control of protected functions is handed over to the Enzyme Council.

This procedure will be detailed in a later section.

## Enzyme Council DAO (admin)

The Enzyme Council DAO is made up of two sub-committees; the Enzyme Technical Council (ETC) and the Enzyme Exposed Businesses (EEB). The ETC is a confederation of technically skilled appointed parties who together have sole access to voting on all protected, protocol-wide functions once a release is taken live.

The Enzyme Council is a fully trusted entity, which is core to the security assumptions of the protocol.


# Access Control Handoff

## Dispatcher ownership

The owner of the `Dispatcher` is the canonical global admin for the protocol, persisting across releases (how the release implements that authority is up to the release).

The owner is already the Enzyme Council.

The Enzyme Council is able to transfer ownership (e.g., to a new multisig wallet, as has taken place) via a nomination-claim procedure.

## FundDeployer ownership

For this release, the owner of the `FundDeployer` is taken to be the admin of release-level protocol contracts.

The owner of `FundDeployer` is set dynamically:

* when `isLive` is `false`, the owner is the contract's deployer, i.e., Avantgarde Core
* when `isLive` is set to `true` (by Avantgarde Core), the contract then defers ownership to the owner of `Dispatcher`

This setup allows for easily setting up all contract configuration prior to freezing ownership for verification.

## Extensions and plugins ownership

Extensions (`FeeManager`, `PolicyManager`, `IntegrationManager`) and plugins (fees, policies, integration adapters) that require ownership for access control defer ownership to the current `FundDeployer` owner. This is accomplished by inheriting a `FundDeployerOwnerMixin` contract.

Thus when the owner of the `FundDeployer` becomes the ETC, so does the owner of all contracts that implement this mixin.

\---

These patterns of handing-off access control gives maximum flexibility for deployment and configuration, while assuring that the ETC will end up with complete admin privileges once the protocol is taken liven.


# Fund Lifecycle

Globally, new funds can be created on the latest release according to the `Dispatcher`. Once created, funds can be migrated to the newest release. The global logic for these actions is located in the `Dispatcher` and additional release-level logic is located in the `FundDeployer`.

The `FundDeployer` operates as the top-level release contract, governing:

* **creation**: how new funds are created on this release
* **migration**: how funds on previous releases are upgraded to this release
* **reconfiguration**: how funds on this release can change all release-level configuration (core settings, policies, and fees)

A new `ComptrollerProxy` is deployed and attached to the `VaultProxy` as its `accessor` via each of these actions. It stores core config options, and the config relative to extensions and their plugins are stored in those particular contracts by reference to the `ComptrollerProxy`. The `ComptrollerProxy` thus serves as the primary configuration object of a fund.

See "Architecture: Release" for more detail on these contracts.

## Creation

In order to create a new fund, CallerA (any account) can call `FundDeployer.createNewFund()` and all components and configuration of the fund are created atomically. The steps taken by this function are:

1. The `FundDeployer` deploys a new `ComptrollerProxy` instance, which sets the caller-provided core and extension config.
2. `FundDeployer` calls to the `Dispatcher` to deploy a new `VaultProxy` with the CallerA-provided `fundOwner` and `fundName` (note that `fundOwner` does not need to be CallerA), along with the release's `VaultLib` and the newly-created `ComptrollerProxy` that will become the `VaultProxy` 's `accessor`.
3. The `FundDeployer` sets the newly-deployed `VaultProxy` on the `ComptrollerProxy` and calls`ComptrollerProxy.activate()` to perform the final setup logic and give extensions a final chance to validate and update state.
4. The fund is now live on this release.

## Migration

### **Migration to this release from a previous release**

1. &#x20;MigratorA calls `FundDeployer.createMigrationRequest()` , which deploys a new `ComptrollerProxy` instance, setting the caller-provided core and extension config, and the `VaultProxy` that will be migrated.
2. MigratorA waits for the `migrationTimelock` defined on the `Dispatcher` to pass. \[NOTE: This is no longer used and is set to \`0\`]
3. MigratorA calls `FundDeployer.executeMigration()` , which calls up to `Dispatcher.executeMigration()`
4. The `Dispatcher` updates the `VaultProxy` 's `VaultLib` and assigns the newly-created `ComptrollerProxy` as its `accessor` .
5. The `FundDeployer` calls `ComptrollerProxy.activate()` to give extensions a final chance to validate and update state.
6. The fund is now live on this release.

### Migration from this release to a new release

The `Dispatcher` invokes hooks at each stage of the migration that call down to the outbound `FundDeployer` , giving this release the chance to execute arbitrary code, reacting to the migration.

This release implements only the `invokeMigrationOutHook` that runs immediately prior to executing the migration. It:

* pays the due protocol fee
* pays out any fee shares outstanding
* calls `selfdestruct()` on the `ComptrollerProxy`&#x20;

## Reconfiguration

A reconfiguration is very similar to a migration (on both a high and low level) in that a new `ComptrollerProxy` is created to replace the old `ComptrollerProxy` , within the same release. It can even be called an "intra-release migration."

The difference is mostly that rather than passing calls up to an authoritative `Dispatcher`, the `FundDeployer` itself stores a `reconfigurationRequest` and defines a local`reconfigurationTimelock` \[NOTE: This is no longer used and is set to \`0\`].

1. &#x20;MigratorA calls `FundDeployer.createReconfigurationRequest()` , which deploys a new `ComptrollerProxy` instance, setting the caller-provided core and extension config, and the `VaultProxy` that will be moved.
2. MigratorA waits for the `reconfigurationTimelock` to pass. \[NOTE: This is no longer used and is set to \`0\`]
3. MigratorA calls `FundDeployer.executeReconfiguration()` , which assigns the newly-created `ComptrollerProxy` as its `accessor` .
4. The `FundDeployer` deactivates the old `ComptrollerProxy` as described in "Migration from this release to a new release".
5. The `FundDeployer` calls `ComptrollerProxy.activate()` to give extensions a final chance to validate and update state.
6. The fund now has a new `ComptrollerProxy` with new configuration live on this release.


# Holdings and Shares

The core functionality of a fund is:

* accepting **deposits** from investors
* using those deposits to build a portfolio of on-chain asset **holdings**
* facilitating the **redemption** of shares for access to portfolio holdings

Each fund is configured with a "denomination asset," which is the unit of account for calculating GAV and share price.

## **Holdings**

There are two types of holdings that are accounted for in GAV: "tracked assets" and "external positions."

### Tracked Assets

"Tracked assets" are fungible, ERC20-compliant assets that belong to the `VaultProxy` , which are stored in its state as `trackedAssets` .

E.g., WETH, MLN, Compound cTokens, Uniswap V2 LP tokens, or any other asset the comprises the asset universe.

An asset is added as a tracked asset whenever the protocol recognizes that a new asset has been transferred to the `VaultProxy`, e.g., via a trade or DeFi action in an `IntegrationManager` adapter or when withdrawing assets from an external position into the `VaultProxy`.

An asset manager can also explicitly add or remove tracked assets via dedicated `IntegrationManager` actions (each with its own policy hook), though the denomination asset of a fund is always a tracked asset.

Through Enzyme Protocol v3, a fund's holdings consisted only of tracked assets.

### **External Positions**

Starting with Enzyme Protocol v4, "external positions" are available as a second type of holding for cases where an action does not result in a simple exchange of ERC20 assets.

E.g., Compound CDPs, Uniswap v3 LP positions

External positions live outside of the `VaultProxy` as distinct `ExternalPositionProxy` instances, which are not ERC20-compliant and are not divisible. These positions can hold valued assets themselves (e.g., Compound cTokens that serve as collateral for a CDP) or simply represent ownership of a position where value is held outside of the protocol, e.g., Uniswap v3 LP positions or staked assets.

See the "External Positions" page.

### Not Included: Untracked Assets and External Rewards

It is important to note that some assets that "belong" to a fund are not included in its GAV and share price.

"Untracked assets" are those ERC20 assets belonging to the `VaultProxy` that are not included in "tracked assets" state.

"External rewards" are unclaimed assets accrued in external protocols for lending, staking or otherwise participating, for example unclaimed `COMP` accrued for lending and borrowing on Compound.

## Shares

Shares are fungible ERC20 tokens that represent a claim to fund holdings in proportion to the total shares supply.

The canonical value of any amount of shares is the total fund GAV multiplied by the proportion of shares / total supply.

Shares are normalized to 18 decimals.

### Deposits

To deposit, a user calls `ComptrollerProxy.buyShares()` with an amount of the denomination asset to deposit, and the `ComptrollerProxy` transfers the denomination asset amount into the `VaultProxy`, where it is absorbed into the holdings. Shares are then minted to the depositor relative to the current share price.

There is a second, access-controlled `ComptrollerProxy.buySharesOnBehalf()` used by peripheral contracts (i.e., the `DepositWrapper`) that wrap end-user actions during a deposit, such as trading from AssetA into the denomination asset and then depositing. It is important to carefully gate access to depositing on behalf of others, so as to not expose a griefing attack due to the `sharesActionTimelock` (see "Transfers" below).

There is one policy hook (`PolicyHook.PostBuyShares` ) and two fee hooks (`FeeHook.PreBuyShares` and `FeeHook.PostBuyShares`) that run during the common `__buyShares()` logic shared by these two functions, allowing policies to validate the buyer and investment amount and fees to be charged prior to and immediately after changes to the fund holdings and shares supply.

### **Redemptions**

There are two redemption mechanisms available.

In both cases, shares are burned in exchange for access to proportionate underlying holdings.

In both cases, fees can be run prior to the redemption via `FeeHook.PreRedeemShares`.

#### **`redeemSharesInKind()`**&#x20;

The designated `_recipient` receives a proportionate slice of the ERC20 assets in the `VaultProxy` , relative to the amount of shares being redeemed.&#x20;

By default, these assets are limited to the "tracked assets" of the `VaultProxy`, but the redeemer can specify tracked assets to ignore (i.e., forfeit) or untracked assets to include (i.e., ERC20 tokens that belong to the `VaultProxy` but are not "tracked assets").

E.g., FundA has 10 shares units issued and holds 20 WETH and 10 ZRX. UserA redeems 1 share uint (10% of total supply). UserA receives 2 WETH and 1 ZRX.

No policies can run on this function, as it should be continuously available as a guaranteed redemption option, though there are cases where redeeming for full shares value would not be possible:

1. An asset in the fund holdings is not transferable (e.g., due to a pause on the ERC20 asset itself, due to a Synth balance not yet being settleable after a trade on Synthetix, etc)
2. The fund holds value in "external positions," which are not divisible, ERC20 representations, and are not included

This latter point is critical: with rare exception, users in funds that are allowed to use external positions (enforced by policies) should not redeem shares in-kind, as they will only receive a proportion of the ERC20 assets held by the `VaultProxy`, and forfeit the claim to value held in external positions.

#### **`redeemSharesForSpecificAssets()`**

The redeemer specifies one or multiple of the `VaultProxy`'s ERC20 holdings along with the relative values of each to receive (for a total of 100%).

E.g., FundA is denominated in DAI, has 10 shares units issued, and has a total GAV of 1000 DAI. UserA redeems 1 share unit (10% of total supply, worth 100 DAI) and specifies to receive 75% of owed value in DAI and 25% in ZRX. UserA receives 75 DAI and 25 DAI worth of ZRX.

Policies can implement `PolicyHook.RedeemSharesForSpecificAssets`  to define, for example, limits on assets that can be redeemed for.

Importantly, unlike `redeemSharesInKind()`, this option pays the redemption `_recipient` their owed proportion of value *inclusive* of value stored in external positions. This function should thus be used as the canonical method of redemption for any fund that uses external positions.

### **Shares Action Timelock**

Each fund configures its own `sharesActionTimelock`, which defines the number of seconds that must pass after UserA's last receipt of shares via deposit, before being allowed to either redeem or transfer any shares.

This is an arbitrage protection, and funds that have untrusted investors should use a non-zero value.

### Transfers

Shares are ERC20-compliant and are transferable by default, though there are a couple of validations that can block transfers:

1. UserA cannot transfer shares to any user until UserA's "shares action timelock" has expired
2. A `PolicyHook.PreTransferShares` enables policies to validate the conditions of a transfer (e.g., a whitelist of allowed recipients)

Because fund configuration (including policies) is changeable via a migration or a reconfiguration, this second point is particularly problematic for secondary markets or any other smart contract holder of shares tokens: if a fund were to add a policy that blocked transfers out of a Uniswap pool, for example, LP providers would be stuck in un-withdrawable positions.

It would further be impractical to liquidity providers or builders if there were no core guarantees that once their contracts receive shares via a transfer in, they would always be able to transfer them out.

For those funds that would to provide such a guarantee to builders or users of secondary applications, there is thus a persistent `freelyTransferableShares` config option on the `VaultProxy` . This configuration option will not run `PolicyHook.PreTransferShares` upon shares transfer, and will persist between migrations and reconfigurations. Once set, it cannot be unset.


# External Positions

An "external position" is a type of fund holding introduced in v4 that:

* is not a fungible ERC20 token
* exists outside of the vault as a distinct contract (one per external position instance)
* reports its value to the fund in terms of its underlying holdings and liabilities
* cannot have value withdrawn during a shares redemption (investors in funds that use external positions should almost never redeem in-kind, as any value inside of external positions will effectively be forfeited)
* is a "beacon" proxy contract that uses the library dictated by the fund's current release (e.g., Sulu)

## External Position Types

Each external position is associated with a "type" that is written to `ExternalPositionProxy.EXTERNAL_POSITION_TYPE` upon its deployment. This type (e.g., a Compound debt position) instructs the system as to which library and parser contracts to use with proxy interactions.

A vault can have many active external positions of the same type.

There are no globally-enforced limitations on the asset counts that can be managed within any external position.

## Persistent architecture

An `ExternalPositionProxy` is deployed by a persistent `ExternalPositionFactory` , which serves as an enduring registry of external position "types" (e.g., Compound CDP) and all valid `ExternalPositionProxy` instances (i.e., those created via the factory).

The `ExternalPositionProxy` is deployed with a `VaultProxy` as its owner.

Upon each call, `ExternalPositionProxy` fetches its library via a `getExternalPositionType()` callback to its `VaultProxy`.

Protected / trusted calls to state-changing actions in the `ExternalPositionProxy` should pass through a protected `receiveCallFromVault()` , guaranteeing the call passed through the owning `VaultProxy` .

## Release architecture

The lifecycle of an external position is managed via `ExternalPositionManagerActions` on the `ExternalPositionManager` extension:

* `CreateExternalPosition` -- create (deploy) a new external position and activate it on the `VaultProxy`
* `CallOnExternalPosition` -- make allowed arbitrary calls to interact with the external position, and transfer assets to / from the `VaultProxy`
* `RemoveExternalPosition` -- deactivate an external position from the `VaultProxy`
* `ReactivateExternalPosition` -- reactivate an existing external position on the `VaultProxy`

Each of these actions has its own policy hook, to allow granular control of risk management.

The `ExternalPositionManager` stores two updatable reference contracts per external position type, which contain all logic specific to that type:

* `lib` is the target library of the beacon `ExternalPositionProxy`, and contains the business logic for its local accounting and interactions with external protocols
* `parser` is a contract used by the `ExternalPositionManager` to validate and parse call data for each specific action on its external position type

For example, VaultA has a Compound CDP external position already deployed. To add collateral to the CDP:

* an asset manager calls the `ExternalPositionManager` 's `CallOnExternalPosition` action via `ComptrollerProxy.callOnExtension()` with the desired payload to add 100 cDAI as collateral
* the `ExternalPositionManager` looks up its stored `CompoundDebtPositionParser` and forwards it the payload
* the `CompoundDebtPositionParser` validates the call, e.g., that cDAI is a valid cToken
* the `CompoundDebtPositionParser` formats the outgoing 100 cDAI into `assetsToTransfer` and `amountsToTransfer` arrays, which it returns to the `ExternalPositionManager`
* the `ExternalPositionManager` passes the payload along with the data for assets to transfer (100 cDAI) and to receive (none) to the `VaultProxy`
* the `VaultProxy` transfers 100 cDAI to the target `ExternalPositionProxy`
* the `VaultProxy` calls `ExternalPositionProxy.receiveCallFromVault()` , passing in the payload
* &#x20;`ExternalPositionProxy` calls back to the `VaultProxy` for the library to use for its type (Compound CDP), which it pulls from the `ExternalPositionManager`
* the `ExternalPositionProxy` uses the returned `CompoundDebtPositionLib` to parse and execute the `AddCollateralAssets` action, adding cDAI to its internal accounting of its managed assets and interacting with Compound to use cDAI as collateral as needed.

&#x20;


# Position Pricing

Enzyme funds and their shares (along with certain policies and fees) rely on trustlessly calculating the values of [positions](/topics/fund-holdings#holdings) held. These values come from sources specific to each position that carry their own assumptions, idiosyncrasies, and risks. Fund owners and asset managers should be aware of which assets are appropriate for their fund setup.

## Pricing sources

Position value is derived from two sources:

1. External feeds registered on the [`ValueInterpreter`](/architecture/release#valueinterpreter)  contract
2. Internal feeds of individual [external positions](/topics/external-positions)

All Enzyme funds use the same `ValueInterpreter` , which is maintained by the Enzyme Technical Committee.

**Asset** **positions** (e.g., WETH, MLN, many LP tokens) are priced directly by `ValueInterpreter` only.

**External positions** (e.g., CDPs, non-fungible LP) contain an internal price feed, which reports its holdings in terms of virtual managed (positive value) and debt (negative value) asset positions. Generally - e.g., when calculating a fund's share price, these managed and debt assets are then aggregated into a target quote asset via `ValueInterpreter`.

See the [price feeds list](broken://pages/-Mh-pFFslWcc-kqVBET5) section for the specific feeds used in `ValueInterpreter`, and the [external positions list](broken://pages/-Mh-qSFUX-iamv23CLYn) section for the available external position types, all of which have an internal pricing feed, as discussed above.

## Pricing risk

**Fund owners and asset managers must be aware of the pricing mechanism assumptions and vulnerabilities involved in the assets they hold, especially if their investors are unknown/untrusted entities.** This is because a fund over or under-prices its shares to the extent that Enzyme's understanding of the value of its holdings deviates from what can actually be acquired by trading or unwinding those positions. (see "[Known Risks & Mitigations](/topics/known-risks-and-mitigations)").

For asset positions, fund owners and asset managers must assess whether the feeds used by `ValueInterpreter` are safe for their fund setup.

For external positions, they must assess whether the internal price feeds are safe for the fund setup, in addition to whether the holdings of the external position result in virtual managed and debt asset positions that are themselves safe to price (by the same logic as standard asset positions).

For the most part, price feeds added to the `ValueInterpreter` are considered generally safe for use, though most carry risks of different natures (e.g., front-running, freshness, data sources quality, protocol risk, etc).

### Example: Wrapped or synthetic assets using a Chainlink price

With many wrapped assets (e.g., wBTC, stETH) and potentially synthetic assets (none currently), the protocol assumes that the asset (e.g., wBTC) maintains a 1:1 price with its underlying (e.g., BTC).

In the case of wrapped assets, the underlyings are held in custody by a third party (whether an EOA or contract). If access to the assets is lost by the custodying entity (e.g., contract vulnerability or private key compromise), the protocol will continue to treat the wrapper as 1:1 with its underlying, even though its real value would be between 1 and 0.

The same would be true of a collateralized synthetic asset that became under collateralized.

\[A full list of assets where a 1:1 assumption is used will soon be available in documentation and/or the Enzyme app]

### Example: Assets that rely on external protocol assumptions

For example, the [`CurvePriceFeed`](broken://pages/-Mh-pFFslWcc-kqVBET5#curvepricefeed) that is used for pricing staked and unstaked Curve pool tokens would become unstable should any of the assets in the pool lose its 1:1 peg with the pool invariant (which would lead to a "bank run" of sorts, imbalancing the Curve pool).

### Example: Assets that rely on external protocol security

For example, the [YearnVaultV2PriceFeed](broken://pages/-Mh-pFFslWcc-kqVBET5#yearnvaultv2pricefeed) that is used for pricing yVault tokens relies on its yVault contract correctly reporting its value in a way that cannot be manipulated by price oracle manipulation attacks.

### Caution: Riskier price feeds

The Enzyme Technical Committee routinely adds (and updates) price feeds to `ValueInterpreter` . Most of these feeds hold to some basic standards, such as:

* Uses Chainlink and Redstone aggregators (no internal assessment is made into freshness or data quality of specific aggregators)
* Reports fresh prices (or prices that have insignificant deviations between updates)
* Interacts only with well-audited external protocols
* Interacts with external protocol logic that only trusted entities can update&#x20;
* Has received a full audit or rigorous QA by an auditing firm

In order to facilitate a larger asset universe for funds to use, it is sometimes necessary for the Enzyme Technical Council to add price feeds that do not hold to a general standard of correctness or security, e.g.,

* Weighted-averages (or otherwise lagging prices)
* Discontinuous oracle updates
* Nacient / unaudited protocols
* Pegged prices (e.g., LRTs)

#### Mitigation: List of assets that use riskier price feeds

To assist in identifying assets that use price feeds with risks that exceed what is generally acceptable for default fund setups, the Enzyme Technical Committee maintains a list of riskier price feeds in its `AddressListRegistry`, with the following `listId`:

* Arbitrum: `16`
* Ethereum: `650`
* Polygon: `1383`

This address list can be referenced by asset managers (to avoid acquiring them) and/or within [policies](/architecture/release#policymanager) (to enforce not acquiring them).&#x20;

e.g., `DisallowedAdapterIncomingAssetsPolicy` can be used to prevent asset managers from acquiring such asset positions via properly-constructed adapters (meaning that they correctly report the incoming assets of the interaction).&#x20;


# Protocol Access

This release implements a "protocol fee," which essentially:

* is a tax on AUM
* is levied continuously relative to an annualized target percentage (initially 25 bps)
* results in burning a corresponding amount of $MLN

## Approach

The protocol fee is charged to a fund by minting new shares to an Enzyme Council-administered contract (`ProtocolFeeReserveProxy`). This occurs anytime that a fund:

* receives a new deposit
* have shares redeemed
* migrates to a new release or reconfigures to a new `ComptrollerProxy` (see "Fund Lifecycle" page)

This approach of minting shares rather than directly collecting $MLN or other assets was thoroughly vetted and resulted in the least user friction, leanest architecture, and highest reliability.

It does, however, come with the drawback that the Council-administered `ProtocolFeeReserveProxy` continuously receives shares from *n* funds, which then need to somehow be converted to $MLN to be burned. Doing this manually via shares redemptions would be work-intensive, cost-inefficient (i.e., gas for redemptions and swaps), and in some cases not even possible (e.g., funds with all value locked in external positions).

To optimise the ease of protocol access and of passively collecting $MLN, this release uses an **Auto-Access** mechanism:&#x20;

Protocol shares are minted at an inflated rate (e.g., 50 bps) above the effective target access rate (e.g., 25 bps). Funds can access the protocol with $MLN to pay the target rate and avoid being penalised.

## Contracts

In order to implement this mechanism, two decoupled contracts with different lifespans and separate concerns are used in tandem:

* `ProtocolFeeTracker` is a non-upgradable, release-level contract that handles state and logic related to tracking the amount of shares that each fund owes at any moment in time
* `ProtocolFeeReserveProxy` is an upgradable, persistent contract that serves as the repository to which protocol fee shares are minted, and its current `ProtocolFeeReserveLib` handles logic for how those shares can be acted upon, i.e., bought back at a discount by the fund in exchange for $MLN

Importantly, all state-changing actions upon `VaultProxy` $MLN holdings (i.e., burn) and shares (i.e., mint and burn) are executed by the `VaultProxy` rather than via these external contracts:

* the `ProtocolFeeTracker` does not have permission to call a minting function on the `VaultProxy`&#x20;
* the `ProtocolFeeReserve` does not have permission to call a burning function on the `VaultProxy`&#x20;
* the `ProtocolFeeReserve` does not actually receive and burn its own $MLN, this is handled inside of the `VaultProxy`

The `ProtocolFeeTracker` and `ProtocolFeeReserve` simply provide the `VaultProxy` with the data it needs handle minting and burning.

This works because the `ProtocolFeeTrakcer` and `ProtocolFeeReserve` operate in complete trust of the `VaultProxy`, and this pattern keeps logic simple and clean, does not expose `VaultProxy` functions to additional state-changing callers, and is gas efficient.


# Gas Relayer

This release supports relayed transactions via Open GSN.

Fund managers can leverage this support to pay for gas costs of allowed transactions with the `VaultProxy` 's WETH balance.

From a high level, this works by deploying a GSN-compliant "paymaster" contract per-fund that is allowed to withdraw WETH from its associated `VaultProxy` to top up a deposit balance for that fund.

## Architecture

The gas relayer architecture is specific to this release (i.e., it is not "persistent" between releases).

The main contracts involved are:

* `GasRelayPaymasterLib` - the canonical library contract for all "paymaster" instances, providing the logic for interacting with GSN contracts, maintaining a healthy WETH deposit, and defining the rules for calls that can be relayed
* `GasRelayPaymasterFactory` - deploys new "paymaster" (`BeaconProxy`) instances, and is the reference for the current beacon library (i.e., `GasRelayPaymasterLib`)
* `GasRelayRecipientMixin` - shared logic that is inherited by all gateway contracts for relayable transactions

## Usage

**To use gas relaying:**

1. There must be enough WETH in the `VaultProxy` to cover the deposit amount specified by the current `GasRelayPaymasterLib` .&#x20;
2. The fund owner calls `deployGasRelayPaymaster()` on the `ComptrollerProxy`
3. The `ComptrollerProxy` deploys a new "paymaster" ( `BeaconProxy` instance) via the `GasRelayPaymasterFactory` and deposits WETH into the newly-deployed paymaster.
4. The fund should maintain enough WETH balance in the `VaultProxy` to top up the deposit before it runs out.

**To turn off the gas relayer**, the fund owner can call `shutdownGasRelayPaymaster()` on the `ComptrollerProxy`, which withdraws the WETH deposit back to the `VaultProxy`.

**When a fund migrates to a new release**, the fund owner can call `withdrawBalance()` on the "paymaster" to withdraw the WETH deposit back to the `VaultProxy`.

## Allowed calls

Any account with a permissioned role (owner, migrator, or asset manager) on the fund can use the internal gas relayer architecture. Additionally, the fund owner can specify additional arbitrary users (e.g., the executors of deposit and redemption queues).

Any calls by these users is allowed to be relayed. The fund owner is responsible for monitoring for any abuse.


# Policies

Policies are intended to provide particular guarantees that help establish trust between current investors and fund managers:

* actions of fund managers
* actions of current investors
* actions of potential investors

Policy rules and behavior:

* Implement one or many "policy hooks", e.g., policies that implement the `PostBuyShares` hook are called to validate state immediately following the minting of new shares by deposit
* Can define whether or not it is disableable
* Can define whether or not it is updatable
* Can be added at any time, *if the hook cannot aversely limit the actions of current investors* (i.e., policies that implement `PreTransferShares` or `RedeemSharesForSpecificAssets` hooks cannot be added outside of fund setup / migration / reconfiguration)

## Policies: Potential investor actions

These policies limit by whom and on what terms new shares can be received

### AllowedDepositRecipientsPolicy

* Hook: `PostBuyShares`
* Disableable: Yes
* Updatable: Yes (but the list defines if list items are updatable)
* Description: Limits the recipients of new deposits to a list of addresses

### AllowedSharesTransferRecipientsPolicy

* Hook: `PreTransferShares`
* Disableable: Yes
* Updatable: Yes (but the list defines if list items are updatable)
* Description: Limits the recipients of shares transfers to a list of addresses

### MinMaxInvestmentPolicy

* Hook: `PostBuyShares`
* Disableable: Yes
* Updatable: Yes
* Description: Sets bounds on the investment amount of a single deposit
* A max amount of 0 can be used to disable all new deposits

E.g., minimum of 100 USDC and max 10,000 USDC

E.g., minimum of 100 USDC and no max

## Policies: Fund manager actions

These policies limit fund manager actions that might be used to drain, hide value, or act outside of the fund's mandate

### AllowedAdapterIncomingAssetsPolicy

* Hook: `PostCallOnIntegration`
* Disableable: No
* Updatable: No (but the list defines if list items are updatable)
* Description: Limits the assets that can be received via an adapter action

### AllowedAdaptersPolicy

* Hook: `PostCallOnIntegration`
* Disableable: No
* Updatable: No (but the list defines if list items are updatable)
* Description: Limits the `IntegrationManager` adapters that can be used
* Intended purpose: Prevent fund managers from using arbitrary adapters. Most funds should elect to use the Council-maintained list of known adapters.

### AllowedAdaptersPerManagerPolicy

* Hook: `PostCallOnIntegration`
* Disableable: Yes
* Updatable: Yes
* Description: Limits the `IntegrationManager` adapters that can be used, per manager
* Intended purpose: Limit the adapters that each asset manager can use (the owner can use any). Intended for cases where the any asset manager is not fully trusted by the owner.

### AllowedExternalPositionTypesPolicy

* Hooks:
  * `CreateExternalPosition`
  * `ReactivateExternalPosition`
* Disableable: No
* Updatable: No
* Description: Limits the external position "types" (e.g., Compound CDP) that can be used, by blocking adding external positions to the vault.
* Intended purpose: The primary purpose is to prevent a manager from using external positions at all, though it can also be used to limit the kinds of external positions allowed

E.g., No external positions are allowed

E.g., Only Compound CDPs are allowed

### AllowedExternalPositionTypePerManagerPolicy

* Hooks:
  * `CreateExternalPosition`
  * `PostCallOnExternalPosition`
  * `ReactivateExternalPosition`
  * `RemoveExternalPosition`
* Disableable: Yes
* Updatable: Yes
* Description: Limits the external position "types" (e.g., Compound CDP) that can be used, per manager
* Intended purpose: Limit the external position types that each asset manager can use (the owner can use any). Intended for cases where the any asset manager is not fully trusted by the owner.

### CumulativeSlippageTolerancePolicy

* Hook: `PostCallOnIntegration`
* Disableable: No
* Updatable: No
* Description: Limits value loss (i.e., slippage) that can occur via adapter actions over a "tolerance period" (7 days). Funds define their own tolerance amount (e.g., 5%, 10%, etc). When an adapter action results in slippage, that slippage amount is added to a cumulative slippage total. The accumulated slippage then diminishes over the "tolerance period duration" at a constant rate based on the fund's chosen tolerance. This policy allows bypassing the slippage checks entirely if the adapter being called is in a Council-maintained list on the `AddressListRegistry` (adapters that cannot be manipulated by asset managers to steal fund value).
* Intended purpose: Slow the rate at which a malicious manager can drain a fund enough to allow alerting and exiting

E.g., a fund with 10% tolerance can suffer a maximum slippage in any one trade of 10%, and then a maximum slippage of the replenished amount (10% \* secondsPassed / oneWeekInSeconds) until the end of the 7 day tolerance period.

### OnlyRemoveDustExternalPositionPolicy

* Hook: `RemoveExternalPosition`
* Disableable: No
* Updatable: N/A (no settings)
* Description: Allows removing an external position from the vault's `activeExternalPositions` only if its value can be considered negligible (i.e., dust). The dust threshold is maintained by the Council. This policy allows properly-signaled underlying assets of the external position without a valid price to be valued as `0`
* Intended purpose: Prevent a manager from hiding significant value in untracked external positions, while allowing the removal of negligible-valued positions that count towards the vault's `POSITIONS_LIMIT` and add significant gas costs to fund functionality

### OnlyUntrackDustOrPricelessAssetsPolicy

* Hook: `RemoveTrackedAssets`
* Disableable: No
* Updatable: N/A (no settings)
* Description: Allows removing an asset from the vault's `trackedAssets` only if a) it does not have a valid price or b) its value can be considered negligible (i.e., dust). The dust threshold is maintained by the Council.
* Intended purpose: Prevent a manager from hiding significant value in the vault as untracked assets, while allowing the removal of negligible-valued positions that count towards the vault's `POSITIONS_LIMIT` and add significant gas costs to fund functionality, and also allowing the removal of assets with invalid prices that block deposits and other functionality.

## Policies: Current investor actions

These policies prevent investors from intentionally or unintentionally disrupting fund strategies and processes

### AllowedAssetsForRedemptionPolicy

* Hook: `RedeemSharesForSpecificAssets`
* Disableable: Yes
* Updatable: No (but the list defines if list items are updatable)
* Description: Manager defines the assets that are allowed to be included in specific asset redemption

E.g., do not allow any assets (i.e., do not allow specific asset redemption)

E.g., allow only WETH and MLN

### MinAssetBalancesPostRedemptionPolicy

* Hook: `RedeemSharesForSpecificAssets`
* Disableable: Yes
* Updatable: No
* Description: Manager defines the minimum asset balances that must remain in the vault after a specific asset redemption
* Intended purpose: Guaranteed continued functionality of gas relayer (WETH) and auto-shares buyback (MLN) by maintaining min balances

E.g., At least 1 WETH and 10 MLN must remain in the vault after each redemption

### NoDepegOnRedeemSharesForSpecificAssetsPolicy

* Hook: `RedeemSharesForSpecificAssets`
* Disableable: Yes
* Updatable: Yes
* Description: Manager defines a list of assets with acceptable price deviations from a specified value (e.g., USDC can deviate up-to 1% from $1)
* Intended purpose: Do not allow specific-asset redemptions during a depeg event, when share price would be artificially high for LP tokens whose pricing assumes adherence to the pool invariant (e.g., USD)

###

###

###

###

###

###


# Known Risks & Mitigations

Fund owners are fully-trusted.

Migrators (assigned by the fund owner) are also fully-trusted since they can arbitrarily change all fund configuration.

Both of these roles can drain funds by default.

Beyond those, there are primarily two risk categories (not including griefing) in terms of the behavior of various actors:

* Opportunistic investors
* Opportunistic asset managers

Different fund setups will have different levels of trust for these parties.

E.g., a DAO treasury might only have a single investor who is ostensibly the same entity as the owner. Or, they might delegate asset management to an EOA that should only be trusted to operate within specific parameters.

E.g., an individual fund owner might be a well-known party for whom reputation serves as a natural mitigation for investors. Or, they might be completely anonymous and untrusted.

In order to not sweepingly apply the strictest risk mitigations to all funds, the protocol is largely unopinionated about what constitutes a "safe" setup, but offers various configuration options and policies to craft bespoke setups that meet particular trust requirements.

## Opportunistic investors (arbitrage)

Investors can arbitrage temporarily mispriced shares or mispriced assets held by a fund by depositing and/or redeeming from a fund under opportune conditions.

*General mitigations for the below opportunities:*

* A `sharesActionTimelock` config option defines the seconds that must pass between a user's most recent deposit and their next transfer or redemption. Though 1 second is enough to prevent flash and sandwich exploits, the longer the `sharesActionTimelock`, the less of a guarantee that an arbitrage opportunity will remain open at the allowed time of redemption.
* Fees can run when depositing or redeeming shares that (conditionally) deduct an amount of shares, thus increasing their effective share price. E.g., for mitigating deposit arbitrage, funds that wish to use an additional arbitrage protection can use the `EntranceRateBurnFee` , which burns a % of the shares minted during deposit.

### **Opportunity: mispriced shares due to untracked value during deposit**

There may be value that "belongs" to a fund that is not included in its share price, i.e., "untracked assets" in the `VaultProxy` (e.g., an airdrop) and unclaimed "external rewards" (e.g., accrued `COMP` rewards) (see "Holdings and Shares").

*Extra mitigations*:

* New assets acquired to the VaultProxy via the protocol are automatically added as tracked assets
* Asset managers should track any untracked assets (e.g., via airdrops or publically-callable rewards claiming functions) as quickly as possible
* In the case of accruing external rewards (e.g., `COMP`), asset managers are advised to track any reward token that they expect to earn ASAP, e.g., track `COMP` as soon as you lend or borrow via Compound for the first time.

### **Opportunity: mispriced shares due to on-chain prices during deposit**

Due to the exclusive use of on-chain asset prices, there may be occasions where there is a deviation between on-chain and off-chain values of assets, and thus also share price.

Though the price feeds used in the protocol should be considered manipulation resistant, there are still occasions where prices are updated via transactions and can therefore be frontrun (e.g., Chainlink aggregator price updates). Furthermore, price updates might only occur when deviation thresholds are crossed (i.e., Chainlink aggregators), and even tight thresholds of <1% in a large enough fund could result in significant arbitrage opportunities.

If share price is "too low" (i.e., the total value of assets according to on-chain, internally-used prices is lower than the total value according to canonical prices), then new investors can deposit and essentially receive a discount.

### **Opportunity: mispriced assets due to on-chain prices during specific asset redemption**

Similarly, to the extent that internally-used asset values deviate from their canonical values, the `redeemSharesForSpecificAssets()` redemption option can be arbitraged, by withdrawing one or more assets that are priced "too low" relative to other assets in the fund.

Though the use of a `sharesActionTimelock` prevents a user who is yet to hold shares from exercising this arbitrage opportunity, current investors for whom the timelock has expired can redeem at any time.

*Extra mitigations:*

* An exit fee charged only on `FeeHook.RedeemSharesForSpecificAssets` (i.e., not on in-kind redemption) that burns a % of shares being redeemed.

## Opportunistic Asset Managers

Asset managers can outright steal value from a fund through bad configurations or bad actions with holdings.

A key concept with asset manager risk mitigation in the protocol is that it is extremely difficult to stop them from stealing value altogether without overly restricting their actions. The goal is to slow down the stealing of value in such a manner as to give fund owners and investors sufficient time to notice (or be notified) and remove the asset manager (owner) or exit a fund (investor) as necessary.

### **Opportunity: drain a fund via adapters**

It is possible via some adapters to trade in an opportunistic manner that results in value leaking from the fund into external accounts (i.e., to an asset manager).

For example, a multi-hop trade on Uniswap can be routed via an arbitrary intermediary pool in which the asset manager is the sole LP provider. Similar exploits would be possible through various routes on ParaSwap.

*Mitigation:* Use a policy that limits share price value loss allowed over a given period, e.g., 5% over 24 hours

### **Opportunity: untrack assets**

An asset manager can untrack any tracked assets in the fund (other than the denomination asset), effectively exposing a shares arbitrage opportunity.

*Mitigations:*

* use a policy that limits removing tracked assets to negligible amounts


# Known Issues

These are issues that have been reported by whitehats, auditors, or internal team members, for which no fix will be made. All issues have been assessed for severity in consultation with our regular auditors and technical committee.

## Incorrect share price for depositors and redeemers if "auto-buyback of protocol fee shares" is used

Issue: During deposit and redeem, if a fund has `autoProtocolFeeSharesBuyback` turned on, an incorrect share price is constructed as: `pre-buyback GAV / post-buyback total supply`.

Effect: Results in too-high share price used to deposit or redeem shares

Mitigation: while mis-pricing should generally be small, unless you are confident in assessing impact, avoid using `autoProtocolFeeSharesBuyback` .

### Incorrect redemption amount and beneficiary share loss if a Shares Splitter's `redeemShares()` is called

Issue: When `redeemShares()` is called on a Shares Splitter, the splitter's shares are redeemed through the vault's redemption hook. The Comptroller infers the exit-fee charge from the redeemer's net share-balance change across that hook. If a fee that mints shares (e.g. `ManagementFee` or `PerformanceFee`) and an exit fee (`ExitRateBurnFee` or `ExitRateDirectFee`) settle in the same redemption, the incoming mint offsets the outgoing exit-fee shares, so the net-delta heuristic under-counts the exit charge and up to `min(minted shares, exit-fee shares)` extra shares are redeemed.

Effect: The extra redeemed shares are drawn from the Shares Splitter's shared pool, reducing the other beneficiaries' claimable balances by up to `min(minted shares, exit-fee shares)` per call. Only reachable via a Shares Splitter's `redeemShares()` path; the standard claim path is unaffected.

Mitigation: Do not call a Shares Splitter's `redeemShares()` — directly, programmatically, or via a block explorer; it is intentionally not exposed in the admin UI. Beneficiaries should claim their shares with `claimToken` and then, if they want the underlying assets, redeem those shares from their own account like any other holder.


# ManagementFee

## Some definitions

Management fee rate (annual, in percent):`x`

Effective management fee rate (annual, in percent, after dilution): `k`

Since management fee is not paid out (as a percentage of assets), but is allocated as newly minted shares in the fund, we need to use the effective management fee rate. This ensures that the manager receives the correct ratio of shares.

The two fee rates are related as follows:

$$x = \frac{k}{1+k}$$

or, alternatively

$$k = \frac{x}{1-x}$$

## Continuous compounding

Management fee accrual happens at irregular and unknown intervals, so we have to resort to continuous compounding. The continuous management fee rate `z` is related to the annual effective management fee rate `k` as follows:

$$e^{z} = 1 + k$$

or, alternatively

$$z = ln(1+k)$$

Substituting for the effective management fee rate `k` yields the relation between the continuous management fee rate and the annual management fee rate:

$$e^{z} = \frac{1}{1-x}$$

or, alternatively

$$z=-ln(1-x)$$

## Management fee allocation

Whenever management fee is due after a time period `t` (expressed as a fraction of a year), the number of shares changes as follows

$$S' = e^{z\cdot t} S$$

`S` is the total supply of shares before the allocation of the management fee shares, and `S'` is the total supply of shares after the allocation of the management fee shares.

The share allocation to the manager is `S_{manager} = S'-S`, and it is calculated as follows:

$$S\_{manager} = \left( \frac{1}{(1-x)^{t}} -1\right) S$$

or

$$S\_{manager} = \left( (1+k)^{t} -1\right) S$$

Using `t = \Delta t / N`, we can rewrite this as

$$S\_{manager} = ( f^{\Delta t} -1) S$$

where

$$f = (1+k)^{1/N}$$

`f` is calculated off-chain when configuring the fee, and it is stored on-chain as `scaledPerSecondRate` . The on-chain computation is then

`sharesDue = (rpow(scaledPerSecondRate, numberOfSeconds, 10**27) - 10**27) * totalSupply / 10**27`


# Performance Fee

### Principles

* Performance fee is paid after a period of constant share supply. Share supply changes on the following actions:
  * buy shares
  * redeem shares
  * claim fees
* Performance fee is only paid if the share price at the end of a share period is larger than the high watermark.
* Only the wealth created for the share price above high watermark is taken into account.
* Performance fee is paid out in shares, as all other fees.
* Order of fee registrations: Management Fee, Performance Fee, Entrance Fees, Exit Fees

### Formulas

* Call `totalSupply` = $$TS\_i$$ i.e. totalSupply before minting or burning shares for the action)
* Read `highWatermark` from storage (this is the share price after the previous performance fee calculation, see below), $$hwm$$
* Current gross share price $$g\_i = GAV\_i / TS\_i$$
* Wealth created during period: $$W\_i = max(g\_i - hwm, 0) \cdot TS\_i$$
* Value of performance fee during period $$F\_i = W\_i \cdot x%$$, where $$x$$is the performance fee percentage
* Performance fee shares (dilute existing shares): $$f\_i = \frac{F\_i \cdot TS\_i}{GAV\_i - F\_i}$$
* Calculate share price (after all fees have been minted or burnt): $$g\_i^\prime = GAV\_i / TS\_i^\prime$$ where $$TS^\prime\_i$$ is the new total supply after all fees have been settled. If $$g^\prime\_i > hwm$$ (i.e. also $$W\_i$$ and $$F\_i$$ will be larger than zero), then update storage $$hwm = g^\prime\_i$$.

{% hint style="info" %}
On Sulu(v4) we removed the crystallisation period. Without a "crystallisation period", the manager can potentially earn more performance fees through continuous accrual instead of quarterly or yearly accrual. Managers should, therefore, set the rate for the new simplified performance fee lower than the rate of the previously used performance fee.
{% endhint %}


# Shares Wrappers

Sometimes, fund participation (depositing, redeeming, and transferring shares) requires rules that are different from the core logic. In such a case, a contract can be deployed that wraps the shares token with the desired logic.

A shares wrapper:

* is an ERC20 token
* is associated with one vault
* accepts end user requests to deposit and redeem to its associated vault (wrapper logic dictates whether these requests pass through atomically, or are queued for subsequent transactions)&#x20;
* holds all shares that are received through its deposits
* issues wrapped shares to the end user 1:1 for the shares received from their deposits&#x20;
* implements any arbitrary additional logic

Generally, a fund using a shares wrapper would use [Policies](/topics/policies#alloweddepositrecipientspolicy) to only allow the shares wrapper and no other depositors, thus requiring that all deposit, redemption, and transfer interactions enter through the wrapper. (Note that shares created or transferred by the protocol fee or any fund-level fee would not be subject to the same restriction).

## GatedRedemptionQueueSharesWrapper

A shares wrapper that:

* is compatible with Enzyme v4 and future versions
* defines recurring windows for (user) requesting and (wrapper manager) executing redemptions
* holds redemption requests and allows their execution within the redemption window, up to a specified, collective, per-window relative cap (e.g., 25% of wrapped shares supply)
* optionally requires per-user pre-approvals for wrapped shares actions (deposit, redeem, transfer)
* allows the wrapper manager to force redemptions, i.e., kick depositors from the fund


