Euler Vault Connector Basics

Yield DevYield Dev
23 min read

Euler vaults implement an innovative new primitive for DeFi lending markets allowing for modular and open innovation in financial instruments on chain. Allowing developers to extend lending markets by creating vaults which wrap arbitrary assets and allow them to be used as collateral in loans for other assets.

A Modular Financial Engineering Platform

An EVC contract is deployed to act as the mediator for our modular financial system. Acting as the authentication mechanism, the EVC mediates interactions between users and the various vaults in our system. The EVC is implemented as a robustly featured multi-call contract, authenticating user operations and ensuring the resulting state of these operations conform to the constraints defined by our vaults.

Composable nodes in the EVC framework are implemented as EVCClient vaults extending the ERC4626 standard with a set of utilities required to allow the EVC to act as an authentication mechanism for operation between vaults in on our platform. These client vaults, are extended further to define the bespoke financial logic pertaining to a specific assets on the platform and allowing for a highly composable set of custom financial transaction to be implemented without fragmenting liquidity or siloing capital within a monolithic DeFi protocol.

These vaults are meant to act as an entry point to a modular financial system, wrapping user assets in a highly flexible financial primitive which can be used to create complex agreements between vaults on the platform or extended, in an open and permission-less way, by other platforms and developers.

Basics

The most basic EVC setup involves two initial vaults; a deposit only vault specifically for accepting a collateral asset and a borrowable vault which accepts deposits for an asset meant to be lent out against the collateral asset. These vaults, along with the EVC, create the basis for a simple lending market.

This example illustrates the simplicity of the EVC's design, as a simple lending market can be implemented utilizing two vaults. Borrowers would deposit their collateral into the deposit vault and liquidity providers can deposit into the borrowable vault, while the EVC mediates interactions between these two vaults ensuring that they conform to the rules of our lending market.

Walkthrough

In this guide we will be walking through reimplementing the VaultSimple contract from the EVC Playground repository. The full code can be found in the EVC playground repository. Further reading on the design and concepts around the EVC can be found in the whitepaper

While the deposit only vault wraps an asset to be utilized as collateral by the EVC infrastructure, the borrowable vault can be extended to implement the specific terms of the lending market's loans. Including the interest rate, price oracle, collateral ratio requirements and, in the case of multiple collaterals, the allowable collateral assets.

First we will break down the implementation of the simplest borrow-only collateral vault.

EVCClient

All vaults interacting with the EVC must implement an EVCClient interface exposing a set of utilities for authenticating the contract callers in the context of the EVC which includes scheduling status check and implementing the ability to unilaterally liquidate collateral shares when appropriate.

These utilities include a getter for determining which collaterals are enabled for an account as well as getters for determining which controllers(Vaults) are enabled for executing on account. It also exposes functionality for disabling a controller.

The client also implements a set of checks, which vault contracts that inherit the EVCClient can schedule when appropriate to ensure conformity to the requirements of the EVC. Including:

requireAccountStatusCheck requireVaultStatusCheck requireAccountAndVaultStatusCheck forgiveAccountStatusCheck isAccountStatusCheckDeferred isVaultStatusCheckDeferred

These checks are used to ensure the solvency of an account or vault after any action that may result in transfer of funds.

Finally, the client implements a liquidateCollateralShares function, allowing the controller to unilaterally seize shares of collateral in the event of a liquidation.

This is the collateral vault of our lending market, and in order to conform to the requirements of the vault controller must implement a couple of basic functions and checks. These requirements can be inherited from the VaultBase contract which itself is a EVCClient contract. Here we have reentrancy locks, vault and account status checks and snapshots.

    /// @notice Creates a snapshot of the vault state
    function createVaultSnapshot() internal {
        // We delete snapshots on `checkVaultStatus`, which can only happen at the end of the EVC batch. Snapshots are
        // taken before any action is taken on the vault that affects the cault asset records and deleted at the end, so
        // that asset calculations are always based on the state before the current batch of actions.
        if (snapshot.length == 0) {
            snapshot = doCreateVaultSnapshot();
        }
    }

    /// @notice Checks the vault status
    /// @dev Executed as a result of requiring vault status check on the EVC.
    function checkVaultStatus() external onlyEVCWithChecksInProgress returns (bytes4 magicValue) {
        doCheckVaultStatus(snapshot);
        delete snapshot;

        return IVault.checkVaultStatus.selector;
    }

    /// @notice Checks the account status
    /// @dev Executed on a controller as a result of requiring account status check on the EVC.
    function checkAccountStatus(
        address account,
        address[] calldata collaterals
    ) external view onlyEVCWithChecksInProgress returns (bytes4 magicValue) {
        doCheckAccountStatus(account, collaterals);

        return IVault.checkAccountStatus.selector;
    }

Here we see the creatVaultSnapshot implementation which saves the state of the vault so that any action taken on the vault that might effect asset calculation are calculated based on the state of the vault before the functions execution started. Preventing many exploits.

We also see the implementation of checkVaultStatus, this calls the doCheckVaultStatus function and deletes the the current snapshot

Finally, checkAccountStatus calls the doCheckAccountStatus function when the EVC requires a check of the accounts health.

In addition to implementing certain basic functionality required by the vault controller, the VaultBase also defines several functions which must be implemented in our borrowable vault contract to configure the lending market.

    /// @notice Creates a snapshot of the vault state
    /// @dev Must be overridden by child contracts
    function doCreateVaultSnapshot() internal virtual returns (bytes memory snapshot);

    /// @notice Checks the vault status
    /// @dev Must be overridden by child contracts
    function doCheckVaultStatus(bytes memory snapshot) internal virtual;

    /// @notice Checks the account status
    /// @dev Must be overridden by child contracts
    function doCheckAccountStatus(address, address[] calldata) internal view virtual;

    /// @notice Disables a controller for an account
    /// @dev Must be overridden by child contracts. Must call the EVC.disableController() only if it's safe to do so
    /// (i.e. the account has repaid their debt in full)
    function disableController() external virtual;

In order that we may build out our VaultSimple contract, we will be importing and inheriting the VaultBase.sol so that our resulting vault is a compliant EVCClient vault. The code for these abstract base contracts can be found here:

VaultBase.sol EVCClient.sol

Implementing VaultSimple.sol: A Deposit Only, Collateral Vault

Now that we understand the Base Vault and the EVCClient implementation, we can begin building our simple vault by inheriting the BaseVault contract.

// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.19;

import "solmate/auth/Owned.sol";
import "solmate/tokens/ERC4626.sol";
import "solmate/utils/SafeTransferLib.sol";
import "solmate/utils/FixedPointMathLib.sol";
import "../VaultBase.sol";

/// @title VaultSimple
/// @dev It provides basic functionality for vaults.
/// @notice In this contract, the EVC is authenticated before any action that may affect the state of the vault or an
/// account. This is done to ensure that if it's EVC calling, the account is correctly authorized. Unlike solmate,
/// VaultSimple implementation prevents from share inflation attack by using virtual assets and shares. Look into
/// Open-Zeppelin documentation for more details. This vault implements internal balance tracking. This contract does
/// not take the supply cap into account when calculating max deposit and max mint values.
contract VaultSimple is VaultBase, Owned, ERC4626 {
    using SafeTransferLib for ERC20;
    using FixedPointMathLib for uint256;

    event SupplyCapSet(uint256 newSupplyCap);

    error SnapshotNotTaken();
    error SupplyCapExceeded();

    uint256 internal _totalAssets;
    uint256 public supplyCap;

    constructor(
        address _evc,
        ERC20 _asset,
        string memory _name,
        string memory _symbol
    ) VaultBase(_evc) Owned(msg.sender) ERC4626(_asset, _name, _symbol) {}
}

We also inherit from ERC4626, as our Simple Vault will wrap an ERC4626 standard interface for use in the EVC context.

Now we can implement a Simple, Deposit only vault meant to wrap collateral assets for use by the EVC.

Firstly, our vault must implement the vault snapshot function, in the case of a deposit only vault we simply return the _totalAssets managed by the vault. This snapshot is meant to ensure that any calculation involving the vaults state are based off of the state of the vault before any operations occur, this is important for maintaining consistency in vaults with more complex logic.

    /// @notice Creates a snapshot of the vault.
    /// @dev This function is called before any action that may affect the vault's state.
    /// @return A snapshot of the vault's state.
    function doCreateVaultSnapshot() internal virtual override returns (bytes memory) {
        // make total assets snapshot here and return it:
        return abi.encode(_totalAssets);
    }

Next, we define the doCheckVaultStatus function required by the BaseVault interface. here we ensure the vault snapshot was taken before validating the vault state and implementing the vault specific check required by our application. In the case of our deposit only collateral vault this is simply implementing a check to enforce the supply cap.

    /// @notice Checks the vault's status.
    /// @dev This function is called after any action that may affect the vault's state.
    /// @param oldSnapshot The snapshot of the vault's state before the action.
    function doCheckVaultStatus(bytes memory oldSnapshot) internal virtual override {
        // sanity check in case the snapshot hasn't been taken
        if (oldSnapshot.length == 0) revert SnapshotNotTaken();

        // validate the vault state here:
        uint256 initialSupply = abi.decode(oldSnapshot, (uint256));
        uint256 finalSupply = _convertToAssets(totalSupply, false);

        // the supply cap can be implemented like this:
        if (supplyCap != 0 && finalSupply > supplyCap && finalSupply > initialSupply) {
            revert SupplyCapExceeded();
        }
    }

The next function required by our BaseVault parent contract's interface is the doCheckAccountStatus check. This function is actually not required to implement our deposit only functionality so we can leave it blank. However, typically any transaction that may effect the solvency of a user's account will require this check to be called at the end of any set of operations to ensure that the preceding operation did not result in a state that violates the rules of our lending market (i.e insolvency, bad debt etc)

    /// @notice Checks the status of an account.
    /// @dev This function is called after any action that may affect the account's state.
    function doCheckAccountStatus(address, address[] calldata) internal view virtual override {
        // no need to do anything here because the vault does not allow borrowing
    }

Now we are also required to implement a disableController function to allow an account to disassociate with a vault. However our deposit only vault, since it does not allow borrowing, should not ever be a controller.

    /// @notice Disables the controller.
    /// @dev The controller is only disabled if the account has no debt.
    function disableController() external virtual override nonReentrant {
        // this vault doesn't allow borrowing, so we can't check that the account has no debt.
        // this vault should never be a controller, but user errors can happen
        EVCClient.disableController(_msgSender());
    }

Now that we've implemented the VaultBase interface, we move on to typical view functions required to implement a ERC4626 vault. These getter/view functions return an internal calculation for converting between assets(deposited into the vault) and shares (the wrapped representation of these assets).

    /// @notice Returns the total assets of the vault.
    /// @return The total assets.
    function totalAssets() public view virtual override returns (uint256) {
        return _totalAssets;
    }

    /// @notice Converts assets to shares.
    /// @dev That function is manipulable in its current form as it uses exact values. Considering that other vaults may
    /// rely on it, for a production vault, a manipulation resistant mechanism should be implemented.
    /// @dev Considering that this function may be relied on by controller vaults, it's read-only re-entrancy protected.
    /// @param assets The assets to convert.
    /// @return The converted shares.
    function convertToShares(uint256 assets) public view virtual override nonReentrantRO returns (uint256) {
        return _convertToShares(assets, false);
    }

    /// @notice Converts shares to assets.
    /// @dev That function is manipulable in its current form as it uses exact values. Considering that other vaults may
    /// rely on it, for a production vault, a manipulation resistant mechanism should be implemented.
    /// @dev Considering that this function may be relied on by controller vaults, it's read-only re-entrancy protected.
    /// @param shares The shares to convert.
    /// @return The converted assets.
    function convertToAssets(uint256 shares) public view virtual override nonReentrantRO returns (uint256) {
        return _convertToAssets(shares, false);
    }

    /// @notice Simulates the effects of depositing a certain amount of assets at the current block.
    /// @param assets The amount of assets to simulate depositing.
    /// @return The amount of shares that would be minted.
    function previewDeposit(uint256 assets) public view virtual override returns (uint256) {
        return _convertToShares(assets, false);
    }

    /// @notice Simulates the effects of minting a certain amount of shares at the current block.
    /// @param shares The amount of shares to simulate minting.
    /// @return The amount of assets that would be deposited.
    function previewMint(uint256 shares) public view virtual override returns (uint256) {
        return _convertToAssets(shares, true);
    }

    /// @notice Simulates the effects of withdrawing a certain amount of assets at the current block.
    /// @param assets The amount of assets to simulate withdrawing.
    /// @return The amount of shares that would be burned.
    function previewWithdraw(uint256 assets) public view virtual override returns (uint256) {
        return _convertToShares(assets, true);
    }

    /// @notice Simulates the effects of redeeming a certain amount of shares at the current block.
    /// @param shares The amount of shares to simulate redeeming.
    /// @return The amount of assets that would be redeemed.
    function previewRedeem(uint256 shares) public view virtual override returns (uint256) {
        return _convertToAssets(shares, false);
    }

These views need to further call the conversion calculation of shares to assets and assets to shares as required by ERC4262:

    function _convertToShares(uint256 assets, bool roundUp) internal view virtual returns (uint256) {
        return roundUp
            ? assets.mulDivUp(totalSupply + 1, _totalAssets + 1)
            : assets.mulDivDown(totalSupply + 1, _totalAssets + 1);
    }

    function _convertToAssets(uint256 shares, bool roundUp) internal view virtual returns (uint256) {
        return roundUp
            ? shares.mulDivUp(_totalAssets + 1, totalSupply + 1)
            : shares.mulDivDown(_totalAssets + 1, totalSupply + 1);
    }

Now we need an approval method in order for our vaults shares to conform to the ERC20 standard. We override this function to set msgSender to the result of _msgSender() a function exposed by the EVCUtil contract and inherited from the EVCClient base contract. This allows this function to be called and authenticated via the EVC on behalf of a given account if this call is being made by the EVC, which is not required as it does not implement the callThroughEVC modifier:

    /// @notice Approves a spender to spend a certain amount.
    /// @param spender The spender to approve.
    /// @param amount The amount to approve.
    /// @return A boolean indicating whether the approval was successful.
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address msgSender = _msgSender();

        allowance[msgSender][spender] = amount;

        emit Approval(msgSender, spender, amount);

        return true;
    }

In the same vein, our ERC20 compliant shares of this vault will require a transfer mechanism. However, since this vault must be used as a deposit vault for collateral we need to implement a vault snapshot as well as an account and vault status check to ensure that both the user's account and the vault remain solvent and consistent after each transfer.

To do this we simply call the requireAccountAndVaultStatusCheck defined on the EVCClient contract we have inherited. This function further calls the EVC to schedule these checks.

Note that, again, we have set the msgSender to the _msgSender() in the context of the EVC. So even thought the EVC is the canonical caller of this operation, the logic is operating on the account authenticated in the EVC's onBehalfOf context. Since this function's operation always results in the transfer of funds, the callThroughEVC modifier is implemented requiring that the this call is delegated via the evc ensuring the scheduled account and vault checks are triggered at the end of its operations.

    /// @notice Transfers a certain amount of shares to a recipient.
    /// @param to The recipient of the transfer.
    /// @param amount The amount shares to transfer.
    /// @return A boolean indicating whether the transfer was successful.
    function transfer(address to, uint256 amount) public virtual override callThroughEVC nonReentrant returns (bool) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        balanceOf[msgSender] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(msgSender, to, amount);

        // despite the fact that the vault status check might not be needed for shares transfer with current logic, it's
        // added here so that if anyone changes the snapshot/vault status check mechanisms in the inheriting contracts,
        // they will not forget to add the vault status check here
        requireAccountAndVaultStatusCheck(msgSender);

        return true;
    }

Similarly to our transfer, our mint function also needs to check the consistency of our vault. Which, if you recall, simply enforces our supply cap.

    /// @notice Mints a certain amount of shares for a receiver.
    /// @param shares The shares to mint.
    /// @param receiver The receiver of the mint.
    /// @return assets The assets equivalent to the minted shares.
    function mint(
        uint256 shares,
        address receiver
    ) public virtual override callThroughEVC nonReentrant returns (uint256 assets) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        assets = _convertToAssets(shares, true); // No need to check for rounding error, previewMint rounds up.

        // Need to transfer before minting or ERC777s could reenter.
        asset.safeTransferFrom(msgSender, address(this), assets);

        _totalAssets += assets;

        _mint(receiver, shares);

        emit Deposit(msgSender, receiver, assets, shares);

        requireVaultStatusCheck();
    }

Similar to the transfer function our withdraw function will need to check the Account and vault status to ensure the solvency of accounts, I.E make sure users with open debts don't withdraw more collateral than the minimum amount required to sustain their loan's margin requirements.

    /// @notice Withdraws a certain amount of assets for a receiver.
    /// @param assets The assets to withdraw.
    /// @param receiver The receiver of the withdrawal.
    /// @param owner The owner of the assets.
    /// @return shares The shares equivalent to the withdrawn assets.
    function withdraw(
        uint256 assets,
        address receiver,
        address owner
    ) public virtual override callThroughEVC nonReentrant returns (uint256 shares) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        shares = _convertToShares(assets, true); // No need to check for rounding error, previewWithdraw rounds up.

        if (msgSender != owner) {
            uint256 allowed = allowance[owner][msgSender]; // Saves gas for limited approvals.

            if (allowed != type(uint256).max) {
                allowance[owner][msgSender] = allowed - shares;
            }
        }

        _burn(owner, shares);

        emit Withdraw(msgSender, receiver, owner, assets, shares);

        asset.safeTransfer(receiver, assets);

        _totalAssets -= assets;

        requireAccountAndVaultStatusCheck(owner);
    }

Redeem, an ERC4626 compliant function allows a user to get the amount of assets out of the vault in return for a given number of shares. This function, as you can probably imagine, also requires checks on the account and vault to ensure solvency.

    /// @notice Redeems a certain amount of shares for a receiver.
    /// @param shares The shares to redeem.
    /// @param receiver The receiver of the redemption.
    /// @param owner The owner of the shares.
    /// @return assets The assets equivalent to the redeemed shares.
    function redeem(
        uint256 shares,
        address receiver,
        address owner
    ) public virtual override callThroughEVC nonReentrant returns (uint256 assets) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        if (msgSender != owner) {
            uint256 allowed = allowance[owner][msgSender]; // Saves gas for limited approvals.

            if (allowed != type(uint256).max) {
                allowance[owner][msgSender] = allowed - shares;
            }
        }

        // Check for rounding error since we round down in previewRedeem.
        require((assets = _convertToAssets(shares, false)) != 0, "ZERO_ASSETS");

        _burn(owner, shares);

        emit Withdraw(msgSender, receiver, owner, assets, shares);

        asset.safeTransfer(receiver, assets);

        _totalAssets -= assets;

        requireAccountAndVaultStatusCheck(owner);
    }

With that, we have implemented an EVC compliant ERC4626 vault for use as a deposit only collateral vault in our lending market.

Next we will need to implement a Borrowable vault. Our Borrowable Vault will extend our SimpleVault with the necessary functionality to allow the creation of loans in our market.

Here's the full code for the SimpleVault deposit only collateral vault.

// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.19;

import "solmate/auth/Owned.sol";
import "solmate/tokens/ERC4626.sol";
import "solmate/utils/SafeTransferLib.sol";
import "solmate/utils/FixedPointMathLib.sol";
import "../VaultBase.sol";

/// @title VaultSimple
/// @dev It provides basic functionality for vaults.
/// @notice In this contract, the EVC is authenticated before any action that may affect the state of the vault or an
/// account. This is done to ensure that if it's EVC calling, the account is correctly authorized. Unlike solmate,
/// VaultSimple implementation prevents from share inflation attack by using virtual assets and shares. Look into
/// Open-Zeppelin documentation for more details. This vault implements internal balance tracking. This contract does
/// not take the supply cap into account when calculating max deposit and max mint values.
contract VaultSimple is VaultBase, Owned, ERC4626 {
    using SafeTransferLib for ERC20;
    using FixedPointMathLib for uint256;

    event SupplyCapSet(uint256 newSupplyCap);

    error SnapshotNotTaken();
    error SupplyCapExceeded();

    uint256 internal _totalAssets;
    uint256 public supplyCap;

    constructor(
        address _evc,
        ERC20 _asset,
        string memory _name,
        string memory _symbol
    ) VaultBase(_evc) Owned(msg.sender) ERC4626(_asset, _name, _symbol) {}

    /// @notice Sets the supply cap of the vault.
    /// @param newSupplyCap The new supply cap.
    function setSupplyCap(uint256 newSupplyCap) external onlyOwner {
        supplyCap = newSupplyCap;
        emit SupplyCapSet(newSupplyCap);
    }

    /// @notice Creates a snapshot of the vault.
    /// @dev This function is called before any action that may affect the vault's state.
    /// @return A snapshot of the vault's state.
    function doCreateVaultSnapshot() internal virtual override returns (bytes memory) {
        // make total assets snapshot here and return it:
        return abi.encode(_totalAssets);
    }

    /// @notice Checks the vault's status.
    /// @dev This function is called after any action that may affect the vault's state.
    /// @param oldSnapshot The snapshot of the vault's state before the action.
    function doCheckVaultStatus(bytes memory oldSnapshot) internal virtual override {
        // sanity check in case the snapshot hasn't been taken
        if (oldSnapshot.length == 0) revert SnapshotNotTaken();

        // validate the vault state here:
        uint256 initialSupply = abi.decode(oldSnapshot, (uint256));
        uint256 finalSupply = _convertToAssets(totalSupply, false);

        // the supply cap can be implemented like this:
        if (supplyCap != 0 && finalSupply > supplyCap && finalSupply > initialSupply) {
            revert SupplyCapExceeded();
        }
    }

    /// @notice Checks the status of an account.
    /// @dev This function is called after any action that may affect the account's state.
    function doCheckAccountStatus(address, address[] calldata) internal view virtual override {
        // no need to do anything here because the vault does not allow borrowing
    }

    /// @notice Disables the controller.
    /// @dev The controller is only disabled if the account has no debt.
    function disableController() external virtual override nonReentrant {
        // this vault doesn't allow borrowing, so we can't check that the account has no debt.
        // this vault should never be a controller, but user errors can happen
        EVCClient.disableController(_msgSender());
    }

    /// @notice Returns the total assets of the vault.
    /// @return The total assets.
    function totalAssets() public view virtual override returns (uint256) {
        return _totalAssets;
    }

    /// @notice Converts assets to shares.
    /// @dev That function is manipulable in its current form as it uses exact values. Considering that other vaults may
    /// rely on it, for a production vault, a manipulation resistant mechanism should be implemented.
    /// @dev Considering that this function may be relied on by controller vaults, it's read-only re-entrancy protected.
    /// @param assets The assets to convert.
    /// @return The converted shares.
    function convertToShares(uint256 assets) public view virtual override nonReentrantRO returns (uint256) {
        return _convertToShares(assets, false);
    }

    /// @notice Converts shares to assets.
    /// @dev That function is manipulable in its current form as it uses exact values. Considering that other vaults may
    /// rely on it, for a production vault, a manipulation resistant mechanism should be implemented.
    /// @dev Considering that this function may be relied on by controller vaults, it's read-only re-entrancy protected.
    /// @param shares The shares to convert.
    /// @return The converted assets.
    function convertToAssets(uint256 shares) public view virtual override nonReentrantRO returns (uint256) {
        return _convertToAssets(shares, false);
    }

    /// @notice Simulates the effects of depositing a certain amount of assets at the current block.
    /// @param assets The amount of assets to simulate depositing.
    /// @return The amount of shares that would be minted.
    function previewDeposit(uint256 assets) public view virtual override returns (uint256) {
        return _convertToShares(assets, false);
    }

    /// @notice Simulates the effects of minting a certain amount of shares at the current block.
    /// @param shares The amount of shares to simulate minting.
    /// @return The amount of assets that would be deposited.
    function previewMint(uint256 shares) public view virtual override returns (uint256) {
        return _convertToAssets(shares, true);
    }

    /// @notice Simulates the effects of withdrawing a certain amount of assets at the current block.
    /// @param assets The amount of assets to simulate withdrawing.
    /// @return The amount of shares that would be burned.
    function previewWithdraw(uint256 assets) public view virtual override returns (uint256) {
        return _convertToShares(assets, true);
    }

    /// @notice Simulates the effects of redeeming a certain amount of shares at the current block.
    /// @param shares The amount of shares to simulate redeeming.
    /// @return The amount of assets that would be redeemed.
    function previewRedeem(uint256 shares) public view virtual override returns (uint256) {
        return _convertToAssets(shares, false);
    }

    /// @notice Approves a spender to spend a certain amount.
    /// @param spender The spender to approve.
    /// @param amount The amount to approve.
    /// @return A boolean indicating whether the approval was successful.
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        address msgSender = _msgSender();

        allowance[msgSender][spender] = amount;

        emit Approval(msgSender, spender, amount);

        return true;
    }

    /// @notice Transfers a certain amount of shares to a recipient.
    /// @param to The recipient of the transfer.
    /// @param amount The amount shares to transfer.
    /// @return A boolean indicating whether the transfer was successful.
    function transfer(address to, uint256 amount) public virtual override callThroughEVC nonReentrant returns (bool) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        balanceOf[msgSender] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(msgSender, to, amount);

        // despite the fact that the vault status check might not be needed for shares transfer with current logic, it's
        // added here so that if anyone changes the snapshot/vault status check mechanisms in the inheriting contracts,
        // they will not forget to add the vault status check here
        requireAccountAndVaultStatusCheck(msgSender);

        return true;
    }

    /// @notice Transfers a certain amount of shares from a sender to a recipient.
    /// @param from The sender of the transfer.
    /// @param to The recipient of the transfer.
    /// @param amount The amount of shares to transfer.
    /// @return A boolean indicating whether the transfer was successful.
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override callThroughEVC nonReentrant returns (bool) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        uint256 allowed = allowance[from][msgSender]; // Saves gas for limited approvals.

        if (allowed != type(uint256).max) {
            allowance[from][msgSender] = allowed - amount;
        }

        balanceOf[from] -= amount;

        // Cannot overflow because the sum of all user
        // balances can't exceed the max uint256 value.
        unchecked {
            balanceOf[to] += amount;
        }

        emit Transfer(from, to, amount);

        // despite the fact that the vault status check might not be needed for shares transfer with current logic, it's
        // added here so that if anyone changes the snapshot/vault status check mechanisms in the inheriting contracts,
        // they will not forget to add the vault status check here
        requireAccountAndVaultStatusCheck(from);

        return true;
    }

    /// @notice Deposits a certain amount of assets for a receiver.
    /// @param assets The assets to deposit.
    /// @param receiver The receiver of the deposit.
    /// @return shares The shares equivalent to the deposited assets.
    function deposit(
        uint256 assets,
        address receiver
    ) public virtual override callThroughEVC nonReentrant returns (uint256 shares) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        // Check for rounding error since we round down in previewDeposit.
        require((shares = _convertToShares(assets, false)) != 0, "ZERO_SHARES");

        // Need to transfer before minting or ERC777s could reenter.
        asset.safeTransferFrom(msgSender, address(this), assets);

        _totalAssets += assets;

        _mint(receiver, shares);

        emit Deposit(msgSender, receiver, assets, shares);

        requireVaultStatusCheck();
    }

    /// @notice Mints a certain amount of shares for a receiver.
    /// @param shares The shares to mint.
    /// @param receiver The receiver of the mint.
    /// @return assets The assets equivalent to the minted shares.
    function mint(
        uint256 shares,
        address receiver
    ) public virtual override callThroughEVC nonReentrant returns (uint256 assets) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        assets = _convertToAssets(shares, true); // No need to check for rounding error, previewMint rounds up.

        // Need to transfer before minting or ERC777s could reenter.
        asset.safeTransferFrom(msgSender, address(this), assets);

        _totalAssets += assets;

        _mint(receiver, shares);

        emit Deposit(msgSender, receiver, assets, shares);

        requireVaultStatusCheck();
    }

    /// @notice Withdraws a certain amount of assets for a receiver.
    /// @param assets The assets to withdraw.
    /// @param receiver The receiver of the withdrawal.
    /// @param owner The owner of the assets.
    /// @return shares The shares equivalent to the withdrawn assets.
    function withdraw(
        uint256 assets,
        address receiver,
        address owner
    ) public virtual override callThroughEVC nonReentrant returns (uint256 shares) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        shares = _convertToShares(assets, true); // No need to check for rounding error, previewWithdraw rounds up.

        if (msgSender != owner) {
            uint256 allowed = allowance[owner][msgSender]; // Saves gas for limited approvals.

            if (allowed != type(uint256).max) {
                allowance[owner][msgSender] = allowed - shares;
            }
        }

        _burn(owner, shares);

        emit Withdraw(msgSender, receiver, owner, assets, shares);

        asset.safeTransfer(receiver, assets);

        _totalAssets -= assets;

        requireAccountAndVaultStatusCheck(owner);
    }

    /// @notice Redeems a certain amount of shares for a receiver.
    /// @param shares The shares to redeem.
    /// @param receiver The receiver of the redemption.
    /// @param owner The owner of the shares.
    /// @return assets The assets equivalent to the redeemed shares.
    function redeem(
        uint256 shares,
        address receiver,
        address owner
    ) public virtual override callThroughEVC nonReentrant returns (uint256 assets) {
        address msgSender = _msgSender();

        createVaultSnapshot();

        if (msgSender != owner) {
            uint256 allowed = allowance[owner][msgSender]; // Saves gas for limited approvals.

            if (allowed != type(uint256).max) {
                allowance[owner][msgSender] = allowed - shares;
            }
        }

        // Check for rounding error since we round down in previewRedeem.
        require((assets = _convertToAssets(shares, false)) != 0, "ZERO_ASSETS");

        _burn(owner, shares);

        emit Withdraw(msgSender, receiver, owner, assets, shares);

        asset.safeTransfer(receiver, assets);

        _totalAssets -= assets;

        requireAccountAndVaultStatusCheck(owner);
    }

    function _convertToShares(uint256 assets, bool roundUp) internal view virtual returns (uint256) {
        return roundUp
            ? assets.mulDivUp(totalSupply + 1, _totalAssets + 1)
            : assets.mulDivDown(totalSupply + 1, _totalAssets + 1);
    }

    function _convertToAssets(uint256 shares, bool roundUp) internal view virtual returns (uint256) {
        return roundUp
            ? shares.mulDivUp(_totalAssets + 1, totalSupply + 1)
            : shares.mulDivDown(_totalAssets + 1, totalSupply + 1);
    }
}
0
Subscribe to my newsletter

Read articles from Yield Dev directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yield Dev
Yield Dev