DEVINE GROUP

.png)
Collateral Allocation Vaults
The ERC4626 contract implements the Tokenized Vault Standard for representing shares of collateral assets in a vault. This is achieved through mathematical operations that define the relationship between deposited collateral assets and the minted shares that represent the user's underlying collateral deposited. See Inherited ERC4626 OpenZeppelin contract overview here outlining potential risks of contract architecture, see Openzeppelin repository for inherited ERC4626 contract used in our implementation here.
​
Collateral Accepted: Each Vault has only one accepted denomination collateral type to for users to deposit, this is a standard function within ERC4626 each financial product will have several vaults allowing for multiple collateral types and segregated management of funds.
​
Overview Of ERC4626 Framework and our specific implementations:
​
Let:
​
-
A: The number of assets to be converted.
-
S: The number of shares to be converted.
-
T_A: The total assets in the vault.
-
T_S​: The total shares in circulation.
-
V_S: The virtual shares (added to mitigate inflation attacks).
-
V_A​: The virtual assets (to ensure robustness in computation, often +1.
​
Assets to Shares:
​
S = A * (T_S + V_S) / (T_A + V_A)​
​
Mapped Function:
​
_convertToShares(uint256 assets, Math.Rounding rounding) — This function implements the formula for converting assets to shares.
​
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
}
​
Mapped Variables:
​
-
A (assets) — The number of assets passed as an argument to the function.
-
T_A (total assets) — totalAssets(), which gives the total assets in the vault.
-
T_S (total shares) — totalSupply(), the total number of shares in circulation.
V_A (virtual assets) — 1 (constant value added to avoid inflation attacks).
-
V_S (virtual shares) — Calculated via _decimalsOffset(), which effectively adds a virtual shares offset (often set to 10 ** _decimalsOffset()).
​
return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); Shares to Assets
​
Shares to Assets
​
A = S * (T_A + V_A) / (T_S + V_S)
​
Mapped Function:
​
_convertToAssets(uint256 shares, Math.Rounding rounding) — This function implements the formula for converting shares to assets.
​
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) {
return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding); }
​
Mapped Variables:
​
-
S (shares) — The number of shares passed to the function.​
-
T_A (total assets) — totalAssets(), the total assets in the vault.
T_S (total shares) — totalSupply(), the total number of shares in circulation.
-
V_A (virtual assets) — 1 (constant value added to avoid inflation attacks).
-
V_S (virtual shares) — 10 ** _decimalsOffset(), the virtual shares offset, depending on _decimalsOffset().
​
Rounding:​
​
The formula for assets is applied with rounding determined by the method used (Math.Rounding.Floor or Math.Rounding.Ceil).
​
Deposit: Assets to Shares
​
Shares_Received = floor(A * (T_S + V_S) / (T_A + V_A))​
​
Mapped Function:
​
deposit(uint256 assets, address receiver) — This function calculates the number of shares to mint based on the amount of assets being deposited into the vault.
​
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual {
SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);
_mint(receiver, shares);
​
emit Deposit(caller, receiver, assets, shares);
}​
​
Mapped Variables:
​
-
A (assets) — The number of assets to be deposited.
-
T_S (total shares) — totalSupply(), the total number of shares in circulation.
-
T_A (total assets) — totalAssets(), the total number of assets in the vault.
-
V_A (virtual assets) — Constant 1.
-
V_S (virtual shares) — Calculated via _decimalsOffset().
​
Rounding:
-
The result is rounded down using Math.Rounding.Floor when shares are calculated during deposit.
​
Mint: Shares to Assets
​
Assets_Required = ceil(S * (T_A + V_A) / (T_S + V_S))
​
Mapped Function:
​
-
mint(uint256 shares, address receiver) — This function calculates the amount of assets required to mint the specified number of shares.
​​​
/** @dev See {IERC4626-mint}. */
function mint(uint256 shares, address receiver) public virtual returns (uint256) {
uint256 maxShares = maxMint(receiver);
if (shares > maxShares) {
revert ERC4626ExceededMaxMint(receiver, shares, maxShares);
}
​
uint256 assets = previewMint(shares);
_deposit(_msgSender(), receiver, assets, shares);
​
return assets;
}
​​
Mapped Variables:
​
-
S (shares) — The number of shares to be minted.
-
T_A (total assets) — totalAssets(), the total number of assets in the vault.
-
T_S (total shares) — totalSupply(), the total number of shares in circulation.
-
V_A (virtual assets) — Constant 1.
-
V_S (virtual shares) — Calculated via _decimalsOffset().
Rounding:
​
-
The result is rounded up using Math.Rounding.Ceil when calculating the required assets for minting.
​
Withdraw: Assets to Shares
​​
Shares_Redeemed = ceil(A * (T_S + V_S) / (T_A + V_A))
​
-
withdraw(uint256 assets, address receiver, address owner) — This function calculates the number of shares to redeem when withdrawing assets.
​
/** @dev See {IERC4626-withdraw}. */
function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) {
uint256 maxAssets = maxWithdraw(owner);
if (assets > maxAssets) {
revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
}
​
uint256 shares = previewWithdraw(assets);
_withdraw(_msgSender(), receiver, owner, assets, shares);
​
return shares;
}
​​
Mapped Variables:
​
-
A (assets) — The number of assets to be withdrawn.
-
T_S (total shares) — totalSupply(), the total number of shares in circulation.
-
T_A (total assets) — totalAssets(), the total number of assets in the vault.
-
V_A (virtual assets) — Constant 1.
-
V_S (virtual shares) — Calculated via _decimalsOffset().
​
Rounding:
​
-
The result is rounded up using Math.Rounding.Ceil when calculating the number of shares redeemed during a withdrawal.
​
Redeem: Shares to Assets
​​
Assets_Received = floor(S * (T_A + V_A) / (T_S + V_S))
​
Mapped Function:
​
redeem(uint256 shares, address receiver, address owner) — This function calculates the amount of assets a user will receive when redeeming shares.
​
/** @dev See {IERC4626-redeem}. */
function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) {
uint256 maxShares = maxRedeem(owner);
if (shares > maxShares) {
revert ERC4626ExceededMaxRedeem(owner, shares, maxShares);
}
​
uint256 assets = previewRedeem(shares);
_withdraw(_msgSender(), receiver, owner, assets, shares);
​
return assets;
}
​
Mapped Variables:
​
-
S (shares) — The number of shares to be redeemed.
-
T_A (total assets) — totalAssets(), the total number of assets in the vault.
-
T_S (total shares) — totalSupply(), the total number of shares in circulation.
-
V_A (virtual assets) — Constant 1.
V_S (virtual shares) — Calculated via _decimalsOffset().
​
Rounding:
​
-
The result is rounded down using Math.Rounding.Floor when calculating the number of assets received during redemption.
​
Efficiency and Precision:
​
General Precision Operation
​
R = (A * B) / D
​
Where:
​
-
A = Numerator multiplier 1​​​
-
B = Numerator multiplier 2
-
D = Denominator
​
Mapped Function:
-
This formula is reflected in various functions where multiplication and division operations are used, such as _convertToShares and _convertToAssets.
​
Mapped Variables:
-
A — Represents the numerator multiplier (assets or shares).
-
B — The second part of the numerator (total shares or total assets).
-
D — The denominator (total assets or total shares in circulation).
​
Decimals and Exchange Rate Adjustment
​
Vault Decimals
​
D_V = D_A + d
​
Mapped Variables:
​
-
D_A — The decimals of the underlying asset (_underlyingDecimals).
Comment
-
d — The offset constant, which is defined in the function _decimalsOffset().
Comment
-
D_V — The vault's decimal representation, calculated by adding the offset to the underlying asset's decimals.
​
Potential Risks Of Vaults:
​
An inflation attack in an ERC-4626 vault occurs when an attacker manipulates the vault's share issuance process, causing an excessive inflation of vault shares without providing proportional underlying assets. This dilutes the value of existing shares and leads to unfair distribution of yield, as the attacker can receive more yield than they should. Essentially, the attacker artificially increases their stake in the vault, exploiting the minting or redemption process, governance flaws, or price manipulation, resulting in a loss of value and trust for other users.
​
See here: https://solidity-by-example.org/hacks/vault-inflation/
​
Inflation Attack Mitigation
​
Adjusted Exchange Rate
​
Exchange_Rate = (T_S + V_S) / (T_A + V_A)
​
Mapped Variables:
​
-
T_S — totalSupply(), the total number of shares in circulation.
T_A — totalAssets(), the total number of assets in the vault.
-
V_S — The virtual shares (10 ** _decimalsOffset()).
-
V_A — The virtual assets (1).
​
Virtual Shares and Assets
​
V_S = 10^d
V_A = 1
​
Virtual shares and virtual assets are introduced to help mitigate potential inflation attacks and maintain precision during share issuance and asset redemption. The calculation for virtual shares (V_S) and virtual assets (V_A) is as follows:
​
Virtual Shares (V_S):
Virtual shares are typically calculated as V_S = 10 ** _decimalsOffset(). This formula helps to adjust the precision of share issuance in the vault, ensuring that the system remains robust and mitigates manipulation risks. The value of _decimalsOffset() determines how much virtual shares are factored into the vault’s operations, providing more granularity in share issuance and redemption.
​
Virtual Assets (V_A):
Virtual assets are defined as V_A = 1. This constant value ensures that the vault calculations are not skewed by rounding errors. By setting virtual assets to 1, we avoid any complications that could arise from rounding issues in the contract's mathematical functions.
​
These virtual values play a crucial role in maintaining fairness in the vault’s operations, especially when there are deposits and withdrawals with varying precision. The implementation of virtual shares and assets ensures that the share price remains stable, even if the vault experiences significant asset changes.
​
Inherited Contract and Devine Group's modifications To The Vaults:
​
This function has been overridden in the inherited contract:
​
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override {
_mint(receiver, shares);
emit Deposit(caller, receiver, assets, shares);
}