Skip to main content

Vault Implementation Considerations

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Vault Specification

A vault, in order to be compatible with the EVC, MUST implement IVault interface. Additionally, the implementation of the following specifications MUST be considered:

  1. A Vault MAY either be allowed to be called through the EVC or MAY be allowed to be called both through the EVC and directly. When the Vault is called through the EVC, it MUST rely on the getCurrentOnBehalfOfAccount function for the currently authenticated Account on behalf of which the operation is being performed. The Vault MUST consider this the true value of msg.sender for authorization purposes. If the Vault is called directly, the Vault MAY perform the authentication itself, and OPTIONALLY use the call function on the EVC in the callback manner.

    NOTE: You can use the _msgSender and _msgSenderForBorrow functions from the EVCUtil contract to retrieve the correct sender address depending on the operation.

  2. In more complex cases, to avoid status-check-related issues, it is advised to use the call function in the callback manner (meaning a Vault can call back itself using call) when the Vault is called directly. It will ensure the Account and Vault Status Checks are always deferred, at least until the end of the call.

    NOTE: You can use the callThroughEVC modifier from the EVCUtil contract to ensure that the Account and Vault Status Checks are always deferred.

  3. If not using the callThroughEVC modifier from the EVCUtil contract, before using call in the callback manner, the Vault is REQUIRED to authenticate the msg.sender. It is strongly advised to pass the msg.sender address as onBehalfOfAccount input of the call function unless there's a legitimate reason not to do so.

    NOTE: There is a subtle complication that vault implementations should consider if they use re-entrancy guards (which is recommended). When a vault is invoked without Status Checks being deferred (i.e. vault called directly, not via the EVC), if it calls require(Account|Vault)StatusCheck on the EVC, the EVC will immediately call back into the vault's check(Account|Vault)Status function. A normal re-entrancy guard would fail upon re-entering at this point. To avoid this, vaults may wish to use the call EVC function or the vault implementation should relax the re-entrancy modifier to allow check(Account|Vault)Status call while invoking require(Account|Vault)StatusCheck.

  4. In order to support sub-accounts, operators, collateral control, and permits, a Vault MUST be invoked via the EVC.

    WARNING: Care MUST be taken not to transfer any assets to the Accounts other than the Account Owner (ID 0). Otherwise, the assets may be lost. If unsure, a vault SHOULD call getAccountOwner() function that returns an address of the Account Owner if it's already registered on the EVC.

  5. When a user requests to perform an operation such as borrow, a Vault MUST call into the EVC to ensure that the Account has enabled this Vault as a Controller. For that purpose getCurrentOnBehalfOfAccount or isControllerEnabled functions MUST be used (this is done by the _msgSenderForBorrow function from the EVCUtil contract).

  6. Due to the fact that only the Controller can disable itself for the Account, a Vault MUST implement a standard disableController function that can be called by a user to disable the Controller if vault-specific conditions are met (typically, the Vault SHOULD check whether the Account has repaid its debt in full). If the vault-specific conditions are not met, the function MUST revert.

    WARNING: If a vault attempted to read the collateral or controller sets of an account in order to enforce some policy of its own, then it is possible that a user could defer checks in a batch to be able to violate them to satisfy the vault's policy transiently. For this reason, vaults SHOULD NOT rely on an account's controller or collateral sets if checks are deferred for this account. This also applies to the amount of deposits and borrows the account has.

  7. After each operation potentially affecting the Account's solvency (also on a non-borrowable Vault), a Vault MUST invoke the EVC in order to require the Account Status Check. The EVC determines whether the check should be deferred or performed immediately.

  8. Vault MUST implement checkAccountStatus function, which gets called for the accounts that have enabled this Vault as Controller. The function receives the Account address and the list of Collateral Vaults enabled for the Account. The function MUST determine whether or not the Account is in an acceptable state by returning the magic value or throwing an exception. If the Vault is deposit-only, the function SHOULD always return the appropriate magic value unless there's a need to implement custom logic.

    NOTE: It is recommended to decorate the checkAccountStatus function with the onlyEVCWithChecksInProgress modifier from the EVCUtil contract to ensure that the function is only called in the expected context.

  9. checkAccountStatus is not called if there is no Controller enabled for the Account at the time of the checks or when the only Controller was disabled before the checks were performed.

    WARNING: Allowing a large set of collateral assets for a vault may be tempting. However, vault creators MUST be careful about which assets they accept. A malicious vault could lie about the amount of assets it holds or reject liquidations when a user is in violation. For this reason, vaults should restrict allowed collaterals to a known-good set of audited addresses or look up the addresses in a registry or factory contract to ensure they were created by known-good, audited contracts.

  10. Vault MAY implement checkVaultStatus function, which gets called if the Vault requires that via the EVC. The EVC determines whether the check should be deferred or performed immediately. This OPTIONAL functionality allows the Vault to enforce its constraints (like supply/borrow caps, invariants checks, etc.). If implemented, the Vault Status Check MUST always be required by the Vault after each operation affecting the Vault's state. The checkVaultStatus function MUST determine whether or not the Vault is in an acceptable state by returning the magic value or throwing an exception. If the Vault doesn't implement this function, the Vault MUST NOT require the Vault Status Check.

    NOTE: It is recommended to decorate the checkVaultStatus function with the onlyEVCWithChecksInProgress modifier from the EVCUtil contract to ensure that the function is only called in the expected context.

  11. To evaluate the Vault Status, checkVaultStatus MAY need access to a snapshot of the initial Vault state, which SHOULD be taken at the beginning of the action that requires the check.

  12. If the onlyEVCWithChecksInProgress modifier from the EVCUtil contract is not used, it MAY be critical to protect check(Account|Vault)Status functions against re-entrancy (depends on individual implementation). Additionally, the following check MUST be included as well:

    require(msg.sender == address(evc) && evc.areChecksInProgress());
  13. Both Account and Vault Status Check MUST be required at the very end of the operation unless the Vault enforces that Checks are always deferred when required (i.e. by using callThroughEVC modifier). This is in order to ensure that the checks are performed after the operation is complete.

  14. The Controller MAY need to forgive the Account Status Check in order for the operation to succeed, i.e. during liquidations. However, the Account Status Check forgiveness MUST be done with care by ensuring that the account for which the Account Status Check will be forgiven hadn't been unexpectedly deferred before the forgiving operation. This is to ensure that the account did not perform any unexpected/malicious operations before the forgiveness.

  15. To ensure only the standard authentication path, if the call is made through the EVC, all the access-controlled functions SHOULD enforce that the caller is the EVC account owner.

    NOTE: It is recommended to decorate the access-controlled functions with the onlyEVCAccountOwner modifier from the EVCUtil contract to ensure that the function is only called by the account owner in the EVC context.

  16. Vault MUST NOT allow calls to arbitrary contracts in an uncontrolled manner, including the EVC.