🏦VaultManager - Borrowing AgTokens

Angle's Borrowing Module User-Facing Contract

1. Introduction

The VaultManager contract is the main contract of Angle Borrowing Module. It is one of the only user-facing contracts. This contract allows people to deposit collateral and open up loans of a given AgToken. It handles all the loan logic (fees and interest rate) as well as the liquidation logic.

There is one VaultManager contract per collateral type for a given stablecoin. You may have for instance two different VaultManager contract for ETH as a collateral, with for instance two different set of parameters.

This contract is also a standard NFT contract on the ERC721MetadataUpgradeable standard. Vaults (name of the tokens in this contract) are non fungible, but they can be transferred from one address to another, you can also approve another address to interact with you vault or simply check the number of vaults you own.

2. Contract Details

Interfaces

Implements IERC721MetadataUpgradeable, IVaultManager, ReentrancyGuardUpgradeable and Initializable. This contract is upgradeable. This contract implements a version of ERC721Permit which allows to grant allowance to an address for an ensemble of vautlts through a permit signature.

References

  • treasury: Reference to the Treasury contract associated to this VaultManager

  • collateral: Reference to the collateral handled by this VaultManager

  • stablecoin: Stablecoin handled by this contract. Another VaultManager contract could have the same rights as this VaultManager on the stablecoin contract

  • oracle: Oracle contract to get access to the price of the collateral with respect to the stablecoin

  • veBoostProxy: Reference to the contract which computes adjusted veANGLE balances for liquidators boosts

Parameters

  • dust: Minimum amount of debt a vault can have

  • _dustCollateral: Minimum amount of collateral (in stablecoin value) that can be left in a vault during a liquidation where the health factor function is decreasing

  • debtCeiling: Maximum amount of stablecoins that can be issued with this contract

  • xLiquidationBoost, yLiquidationBoost: Threshold veANGLE balance values and value of the boost for liquidators at these threshold values: the length of these arrays should be 2

  • collateralFactor: Encodes the maximum ratio stablecoin/collateral a vault can have before being liquidated. It's what determines the minimum collateral ratio of a position

  • targetHealthFactor: Maximum Health factor at which a vault can end up after a liquidation (unless it's fully liquidated)

  • borrowFee: Upfront fee taken when borrowing stablecoins

  • repayFee: Fee taken when repaying a stablecoin debt

  • interestRate: Per second interest taken to borrowers taking agToken loans

  • liquidationSurcharge: Fee taken by the protocol during a liquidation. Technically, this value is not the fee per se, it's 1 - fee. For instance for a 2% fee, liquidationSurcharge should be 98%.

  • maxLiquidationDiscount: Maximum discount given to liquidators

  • whitelistingActivated: Whether whitelisting is required to own a vault or not

  • paused: Whether the contract is paused or not

Variables

  • lastInterestAccumulatorUpdated: Timestamp at which the interestAccumulator was updated

  • interestAccumulator: Keeps track of the interest that should accrue to the protocol. The stored value is not necessarily the true value: this one is recomputed every time an action takes place within the protocol.

  • totalNormalizedDebt: Total normalized amount of stablecoins borrowed

  • surplus: Surplus accumulated by the contract: surplus is always in stablecoins, and is then reset when the value is communicated to the treasury contract

  • badDebt: Bad debt made from liquidated vaults which ended up having no collateral and a positive amount of stablecoins

Mappings

  • vaultData: Maps a vaultID to its data (namely collateral amount and normalized debt)

  • isWhitelisted: Maps an address to whether it's whitelisted and can open or own a vault

ERC721 Data

This contract has all the mappings and data associated to ERC721 Contracts. Most of them are internal variables which are not accessible externally (like _vaultApprovals, _owners, _balances or _operatorApprovals). The only publicly available is the vaultIDCount which is a counter to generate a unique vaultID for each vault: vaultID acts as tokenID in basic ERC721 contracts.

There is also a _nonces mapping to used to check permit signatures to the contract.

Access Control

Like in all contracts of this Borrowing Module, restricted functions are checked upon calling the Treasury contract which then calls its associated CoreBorrow contract.

3. Key Mechanisms & Concepts

EIP721 Methods

All standard EIP721 methods are implemented, such as balanceOf(), ownerOf(), approve(), transferFrom, etc.

The implementation of these methods is derived from OpenZeppelin's library.

A getControlledVaults function was also added. It allows to check all the vaults controlled by an address. This is an expensive function that should never be called on-chain as it iterates over all vaultIDs. It is here to reduce dependency on an external graph to link a vaultID ID to its owner.

permit

Beyond these base methods, the contract includes a permit method to grant (or revoke) approval to an address for all the associated vaults of the contract.

    function permit(
        address owner,
        address spender,
        bool approved,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external

Parameters:

  • owner: Address which signed the message

  • spender: Address to grant or revoke allowance to

  • approved: Whether to grant or revoke allowance

  • deadline: Timestamp till which the signature is valid

  • v, r, s: Signature data

Examples of how to sign can be found in the open-source borrow-contracts repo.

Main Vault Interaction Methods

Everything in the VaultManager contract was designed to be composable in a single transaction: this can be done with the angle() functions.

There are two different versions of the function, performing in the end the same actions.

    function angle(
        ActionType[] memory actions,
        bytes[] memory datas,
        address from,
        address to,
        address who,
        bytes memory repayData
    ) external payable returns (PaymentData memory paymentData);

And:

    function angle(
        ActionType[] memory actions,
        bytes[] memory datas,
        address from,
        address to
    ) external payable returns (PaymentData memory paymentData);

These functions allow composability between calls to the different entry points of this module. Any user calling this function can perform any of the allowed actions in the order of their choice.

This function is optimized to reduce gas cost due to payment from or to the user and that expensive calls or computations (like oracleValue) are done only once

Parameters:

  • actions: Set of actions to perform. Each action corresponds to a different integer.

  • datas: Array of data to be decoded for each action. Each item in this array is the abi-encoded version of different parameters for each version: parameters can include like the vaultID or the stablecoinAmount to borrow for instance. Each action expects particular data to be given (more detail below). The datas array should have the same length as the actions array.

  • from: Address from which stablecoins will be taken if one action includes burning stablecoins. This address should either be the msg.sender or be approved by the latter

  • to: Address to which stablecoins and/or collateral will be sent in case of

The following parameters are optional and can be avoided if you are using the second version of the function. For about how to use these parameters correctly, you can look at this guide we have built on auto-leverage and capital-efficiency within Angle Borrowing Module.

  • who: Address of the contract to handle in case of repayment of stablecoins from received collateral

  • repayData: Data to pass to the repayment contract in case of

Return Value:

  • paymentData: Struct containing the final transfers executed. This struct contains four different fields (named from the protocol's perspective):

    • stablecoinAmountToGive: Stablecoin amount the contract has given to the to address

    • stablecoinAmountToReceive: Stablecoin amount taken from the from address to be burnt by the contract

    • collateralAmountToGive: Collateral amount the contract has given to the to address

    • collateralAmountToReceive: Collateral amount taken from the msg.sender address Any contract interfacing with the vaultManager

Below, we detail the list of different actions, their corresponding integers and the data that should be passed for actions to be correctly processed. Note that for all actions requiring a vaultID, specifiying a vaultID = 0 will automatically process the last vault created.

createVault - Action 0

This action enables vault creation within the protocol. It just creates the structure without any collateral or stablecoins with it. It should be combined with other actions to borrow (like an addCollateral and a borrow action to get stablecoins).

Type of the parameter:

  • address: address for which the vault should be created

closeVault - Action 1

This action enables vault closing within the protocol. An address closing a vault is expected to reimburse its full debt, and gets in exchange the value of the collateral in it. The protocol is coded so that vault closing can be done capital-efficiently. For more details about this, you can look at this guide.

Only the address that owns the vault or addresses that are approved for it can close a vault.

Type of the parameter:

  • uint256: ID of the vault to close

addCollateral - Action 2

Adds collateral in an existing vault. This function can be done in conjunction with a borrow action to collateralize a vault and get stablecoins in just a single transaction.

Any address can add collateral on any vault.

Type and order of the parameters:

  1. uint256: ID of the vault to add collateral to

  2. uint256: Amount of collateral to add

removeCollateral - Action 3

Removes collateral from an existing vault. Only addresses that are approved for the concerned vault can perform this action. A solvency check is performed after the operation meaning that removing collateral cannot place an address in a liquidation situation.

Type and order of the parameters:

  1. uint256: ID of the vault to remove collateral from

  2. uint256: Amount of collateral to remove

borrow - Action 4

Mints stablecoins from a collateralized vault. A solvency check is performed after the operation meaning that you may be limited by the amount of borrowed stablecoins. This function takes into account borrowing fees. Only addresses that are approved for the concerned vault can perform this action.

If a vault has no stablecoins borrowed, then the first borrow action should make sure to borrow more than the dust of the vaultManager.

Type and order of the parameters:

  1. uint256: ID of the vault for which stablecoins should be borrowed

  2. uint256: Amount of stablecoins to borrow. If there are borrowing fees, the amount of stablecoins actually going to the to address

repayDebt - Action 5

Repays a portion of the debt of a vault. Any address can repay debt on behalf of any vault. During debt repayment, you can specify a stablecoin amount that is voluntarily too high with respect to the debt of the vault: it will repay all the debt remaining. Action will revert though if the amount of debt repaid makes the debt of the vault smaller than the dust.

Type and order of the parameters:

  1. uint256: ID of the vault for which debt should be repaid

  2. uint256: Amount of stablecoins to repay. Setting an amount higher than the debt of the vault will simply reimburse all the outstanding debt in the vault. In this case, stablecoin transfer from the from address will just be equal to the value of the debt, meaning if you use type(uint256).max, you will just be charged the value of the debt

getDebtIn - Action 6

Gets debt in a vault from another vault potentially in another VaultManager contract. Only approved addresses by the source vault owner can perform this action, however any vault from any VaultManager contract of the same stablecoin can see its debt reduced by this means. A solvency check is performed after the debt increase in the source vaultID.

Type and order of the parameters:

  1. uint256: ID of the vault in this contract for which debt should be increased

  2. address: Address of the VaultManager contract where the vault from which debt should be taken is: it can well be the same VaultManager contract. If a wrong VaultManager address is specified (like a contract with specifically the right interface), then this function may just increase the debt in a vault without reducing the debt in another one.

  3. uint256: ID of the vault from which debt should be taken. This operation will technically just reduce the debt of this vault. If there are different minting fees in both contracts, then this action will make sure that everything is as if for the borrowers they had been the same

  4. uint256: Amount of stablecoins to increase the debt of

permit - Action 7

Action to approve the VaultManager for the collateral (if the specific collateral used supports this).

Type and order of the parameters:

  1. address: Owner address which has signed the permit and wants to approve the `VaultManager``

  2. uint256: Amount of collateral to approve

  3. uint256: Deadline parameter for the permit

  4. uint256: v parameter for the permit. This uint256 is casted to uint8 in the contract

  5. bytes32: r parameter

  6. bytes32: s parameter

Other Vault Interaction Methods

There are some wrappers for actions above and other external interaction methods.

createVault

function createVault(address toVault) external returns (uint256);

It creates a vault that is going to be owned by toVault and returns the ID of this vault.

getDebtOut

    function getDebtOut(
        uint256 vaultID,
        uint256 stablecoinAmount,
        uint256 senderBorrowFee
    ) external;

This is the function that is called by other VaultManager contracts if these contracts were requested a getDebtIn action with debt from a vault in the contract. Hence only VaultManager of the same treasury contracts can call it. This function ultimately reduces the debt of vaultID by stablecoinAmount.

The sender of this action specifies the senderBorrowFee to make sure that if the senderBorrowFee is greater than the borrow fee in the contract, then borrow fees are still paid on the debt transferred.

Liquidation Methods

Liquidation plays an essential role in Angle Borrowing Module and liquidations of the vaults of a VaultManager contract are handled directly in the same contract. There are two external access functions for liquidators to liquidate.

    function liquidate(
        uint256[] memory vaultIDs,
        uint256[] memory amounts,
        address from,
        address to,
        address who,
        bytes memory data
    ) public returns (LiquidatorData memory liqData)

And:

    function liquidate(
        uint256[] memory vaultIDs,
        uint256[] memory amounts,
        address from,
        address to
    ) public returns (LiquidatorData memory liqData)

The first function is a more advanced function you can use for capital-efficient liquidations, the second one is a wrapper built on top of the first.

For a complete guide on how to use these functions and how to perform liquidations in Angle Borrowing Module, you can look at this docs.

These functions can be used to liquidate an ensemble of vaults specified by their vault IDs. They revert if one of the ID concerns a vault that does not exist or that should be liquidated. It is recommended to call the checkLiquidation function for each vault to liquidate before calling for a liquidation.

Parameters:

  • vaultIDs: List of the vaults to liquidate

  • amounts: Array of amount of stablecoins to bring for the liquidation of each vault. If an amount is too big, it is going to be rounded down by the contract to the max possible amount to repay for an unhealthy vault

  • from: Address from which stablecoins for the liquidations will be takne

  • to: Address to which collateral will be sent

  • who: This is an optional argument (could be the zero address), and can be used in case a specific contract should handle the repayment

  • data: Optional again, it can help the who contract to know which swap to proceed

Return Values:

  • liqOpp: This is a struct with several info on the liquidations that were made by this function. Note that these returned values are denominated from a protocol perspective.

    • stablecoinAmountToReceive: This is the amount of stablecoins the liquidator has given for the liquidation

    • collateralAmountToGive: Current amount of collateral obtained from the protocol from the contract

    • badDebtFromLiquidation: Bad debt accrued across the liquidation process in the VaultManager

    • oracleValue: Oracle value at which the liquidation took place

    • newInterestAccumulator: Value of the interestAccumulator at the time of the call (this is a value used to track vaults outstanding debt)

External View Functions

checkLiquidation

    function checkLiquidation(uint256 vaultID, address liquidator)
        external
        view
        returns (LiquidationOpportunity memory liqOpp);

As mentionned above, this function checks whether a given vault is liquidable and if yes gives information regarding its liquidation. It is recommended to call it before asking for the liquidation of a given vault.

The address of the liquidator is optional here: it is here to compute the boost on the discount a liquidator could get.

The returned value is a struct called liqOpp. It contains data about how much stablecoins can be repaid from the liquidation, what discount the liquidator would get, what collateral amount the liquidator could at most get. For more details on the output of this function, you can check the guide here.

Debt Checking

There are two other functions to check the debt of a vault and the total debt accross all vaults at the VaultManager level. These functions are: getVaultDebt(uint256 vaultID) and getTotalDebt().

Treasury Management Function

function accrueInterestToTreasury() external returns (uint256 surplusValue, uint256 badDebtValue)

Accrues interest accumulated across all vaults to the surplus and sends the surplus to the treasury: this is the function called by the Treasury contract to pull surplus and bad debt from the contract. The role of the Treasury contract is then to pool all the surplus and bad debt it got from all VaultManager to handle governance distribution.

Surplus is technically minted upon calling this function. In the VaultManager contract, calling this contract resets the surplus and badDebt value in storage to zero.

Governance Functions

Besides the function above which can only be called by the Treasury contract, the contract has several restricted functions used by governance (or the guardian) to set parameters:

  • setUint64: to change parameters stored as uint64

  • setDebtCeiling: to change the debt ceiling of the `VaultManager``

  • setLiquidationBoostParameters: sets the parameters for the liquidation booster which encodes the slope of the discount for liquidators

  • toggleWhitelist: to toggle permission for vault ownership by any account or to allow/disallow an address in case whitelisting is activated. If this is activated, vaults that existed prior to that will not be transferrable to addresses which are not whitelisted, and new vaults will only be openable by whitelisted addresses

  • setOracle: to set the oracle contract

  • setTreasury: to change the reference to the Treasury contract (this function should never be used in practice)

  • togglePause: to pause the contract

  • setBaseURI: to change the ERC721 Metadata URI

Last updated