Skip to content

Instantly share code, notes, and snippets.

@DanielVF
Last active May 15, 2023 13:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielVF/7c4b270d7dafd31b1b8662f4a19cd649 to your computer and use it in GitHub Desktop.
Save DanielVF/7c4b270d7dafd31b1b8662f4a19cd649 to your computer and use it in GitHub Desktop.
VaultCore Upgrade
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title OUSD Vault Contract
* @notice The Vault contract stores assets. On a deposit, OUSD will be minted
and sent to the depositor. On a withdrawal, OUSD will be burned and
assets will be sent to the withdrawer. The Vault accepts deposits of
interest from yield bearing strategies which will modify the supply
of OUSD.
* @author Origin Protocol Inc
*/
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import { StableMath } from "../utils/StableMath.sol";
import { IVault } from "../interfaces/IVault.sol";
import { IOracle } from "../interfaces/IOracle.sol";
import { IBasicToken } from "../interfaces/IBasicToken.sol";
import { IGetExchangeRateToken } from "../interfaces/IGetExchangeRateToken.sol";
import "./VaultStorage.sol";
contract VaultCore is VaultStorage {
using SafeERC20 for IERC20;
using StableMath for uint256;
using SafeMath for uint256;
// max signed int
uint256 constant MAX_INT = 2**255 - 1;
// max un-signed int
uint256 constant MAX_UINT =
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
/**
* @dev Verifies that the rebasing is not paused.
*/
modifier whenNotRebasePaused() {
require(!rebasePaused, "Rebasing paused");
_;
}
/**
* @dev Verifies that the deposits are not paused.
*/
modifier whenNotCapitalPaused() {
require(!capitalPaused, "Capital paused");
_;
}
modifier onlyOusdMetaStrategy() {
require(
msg.sender == ousdMetaStrategy,
"Caller is not the OUSD meta strategy"
);
_;
}
/**
* @dev Deposit a supported asset and mint OUSD.
* @param _asset Address of the asset being deposited
* @param _amount Amount of the asset being deposited
* @param _minimumOusdAmount Minimum OUSD to mint
*/
function mint(
address _asset,
uint256 _amount,
uint256 _minimumOusdAmount
) external whenNotCapitalPaused nonReentrant {
require(assets[_asset].isSupported, "Asset is not supported");
require(_amount > 0, "Amount must be greater than 0");
uint256 units = _toUnits(_amount, _asset);
uint256 unitPrice = _toUnitPrice(_asset, true);
uint256 priceAdjustedDeposit = (units * unitPrice) / 1e18;
if (_minimumOusdAmount > 0) {
require(
priceAdjustedDeposit >= _minimumOusdAmount,
"Mint amount lower than minimum"
);
}
emit Mint(msg.sender, priceAdjustedDeposit);
// Rebase must happen before any transfers occur.
if (priceAdjustedDeposit >= rebaseThreshold && !rebasePaused) {
_rebase();
}
// Mint matching OUSD
oUSD.mint(msg.sender, priceAdjustedDeposit);
// Transfer the deposited coins to the vault
IERC20 asset = IERC20(_asset);
asset.safeTransferFrom(msg.sender, address(this), _amount);
if (priceAdjustedDeposit >= autoAllocateThreshold) {
_allocate();
}
}
/**
* @dev Mint OUSD for OUSD Meta Strategy
* @param _amount Amount of the asset being deposited
*
* Notice: can't use `nonReentrant` modifier since the `mint` function can
* call `allocate`, and that can trigger `ConvexOUSDMetaStrategy` to call this function
* while the execution of the `mint` has not yet completed -> causing a `nonReentrant` collision.
*
* Also important to understand is that this is a limitation imposed by the test suite.
* Production / mainnet contracts should never be configured in a way where mint/redeem functions
* that are moving funds between the Vault and end user wallets can influence strategies
* utilizing this function.
*/
function mintForStrategy(uint256 _amount)
external
whenNotCapitalPaused
onlyOusdMetaStrategy
{
require(_amount < MAX_INT, "Amount too high");
emit Mint(msg.sender, _amount);
// Rebase must happen before any transfers occur.
// TODO: double check the relevance of this
if (_amount >= rebaseThreshold && !rebasePaused) {
_rebase();
}
// safe to cast because of the require check at the beginning of the function
netOusdMintedForStrategy += int256(_amount);
require(
abs(netOusdMintedForStrategy) < netOusdMintForStrategyThreshold,
"Minted ousd surpassed netOusdMintForStrategyThreshold."
);
// Mint matching OUSD
oUSD.mint(msg.sender, _amount);
}
// In memoriam
/**
* @dev Withdraw a supported asset and burn OUSD.
* @param _amount Amount of OUSD to burn
* @param _minimumUnitAmount Minimum stablecoin units to receive in return
*/
function redeem(uint256 _amount, uint256 _minimumUnitAmount)
external
whenNotCapitalPaused
nonReentrant
{
_redeem(_amount, _minimumUnitAmount);
}
/**
* @dev Withdraw a supported asset and burn OUSD.
* @param _amount Amount of OUSD to burn
* @param _minimumUnitAmount Minimum stablecoin units to receive in return
*/
function _redeem(uint256 _amount, uint256 _minimumUnitAmount) internal {
// Calculate redemption outputs
(
uint256[] memory outputs,
uint256 _backingValue
) = _calculateRedeemOutputs(_amount);
// Check that OUSD is backed by enough assets
uint256 _totalSupply = oUSD.totalSupply();
if (maxSupplyDiff > 0) {
// Allow a max difference of maxSupplyDiff% between
// backing assets value and OUSD total supply
uint256 diff = _totalSupply.divPrecisely(_backingValue);
require(
(diff > 1e18 ? diff.sub(1e18) : uint256(1e18).sub(diff)) <=
maxSupplyDiff,
"Backing supply liquidity error"
);
}
emit Redeem(msg.sender, _amount);
// Send outputs
for (uint256 i = 0; i < allAssets.length; i++) {
if (outputs[i] == 0) continue;
IERC20 asset = IERC20(allAssets[i]);
if (asset.balanceOf(address(this)) >= outputs[i]) {
// Use Vault funds first if sufficient
asset.safeTransfer(msg.sender, outputs[i]);
} else {
address strategyAddr = assetDefaultStrategies[allAssets[i]];
if (strategyAddr != address(0)) {
// Nothing in Vault, but something in Strategy, send from there
IStrategy strategy = IStrategy(strategyAddr);
strategy.withdraw(msg.sender, allAssets[i], outputs[i]);
} else {
// Cant find funds anywhere
revert("Liquidity error");
}
}
}
if (_minimumUnitAmount > 0) {
uint256 unitTotal = 0;
for (uint256 i = 0; i < outputs.length; i++) {
unitTotal += _toUnits(outputs[i], allAssets[i]);
}
require(
unitTotal >= _minimumUnitAmount,
"Redeem amount lower than minimum"
);
}
oUSD.burn(msg.sender, _amount);
// Until we can prove that we won't affect the prices of our assets
// by withdrawing them, this should be here.
// It's possible that a strategy was off on its asset total, perhaps
// a reward token sold for more or for less than anticipated.
if (_amount >= rebaseThreshold && !rebasePaused) {
_rebase();
}
}
/**
* @dev Burn OUSD for OUSD Meta Strategy
* @param _amount Amount of OUSD to burn
*
* Notice: can't use `nonReentrant` modifier since the `redeem` function could
* require withdrawal on `ConvexOUSDMetaStrategy` and that one can call `burnForStrategy`
* while the execution of the `redeem` has not yet completed -> causing a `nonReentrant` collision.
*
* Also important to understand is that this is a limitation imposed by the test suite.
* Production / mainnet contracts should never be configured in a way where mint/redeem functions
* that are moving funds between the Vault and end user wallets can influence strategies
* utilizing this function.
*/
function burnForStrategy(uint256 _amount)
external
whenNotCapitalPaused
onlyOusdMetaStrategy
{
require(_amount < MAX_INT, "Amount too high");
emit Redeem(msg.sender, _amount);
// safe to cast because of the require check at the beginning of the function
netOusdMintedForStrategy -= int256(_amount);
require(
abs(netOusdMintedForStrategy) < netOusdMintForStrategyThreshold,
"Attempting to burn too much OUSD."
);
// Burn OUSD
oUSD.burn(msg.sender, _amount);
// Until we can prove that we won't affect the prices of our assets
// by withdrawing them, this should be here.
// It's possible that a strategy was off on its asset total, perhaps
// a reward token sold for more or for less than anticipated.
if (_amount >= rebaseThreshold && !rebasePaused) {
_rebase();
}
}
/**
* @notice Withdraw a supported asset and burn all OUSD.
* @param _minimumUnitAmount Minimum stablecoin units to receive in return
*/
function redeemAll(uint256 _minimumUnitAmount)
external
whenNotCapitalPaused
nonReentrant
{
_redeem(oUSD.balanceOf(msg.sender), _minimumUnitAmount);
}
/**
* @notice Allocate unallocated funds on Vault to strategies.
* @dev Allocate unallocated funds on Vault to strategies.
**/
function allocate() external whenNotCapitalPaused nonReentrant {
_allocate();
}
/**
* @notice Allocate unallocated funds on Vault to strategies.
* @dev Allocate unallocated funds on Vault to strategies.
**/
function _allocate() internal {
uint256 vaultValue = _totalValueInVault();
// Nothing in vault to allocate
if (vaultValue == 0) return;
uint256 strategiesValue = _totalValueInStrategies();
// We have a method that does the same as this, gas optimisation
uint256 calculatedTotalValue = vaultValue.add(strategiesValue);
// We want to maintain a buffer on the Vault so calculate a percentage
// modifier to multiply each amount being allocated by to enforce the
// vault buffer
uint256 vaultBufferModifier;
if (strategiesValue == 0) {
// Nothing in Strategies, allocate 100% minus the vault buffer to
// strategies
vaultBufferModifier = uint256(1e18).sub(vaultBuffer);
} else {
vaultBufferModifier = vaultBuffer.mul(calculatedTotalValue).div(
vaultValue
);
if (1e18 > vaultBufferModifier) {
// E.g. 1e18 - (1e17 * 10e18)/5e18 = 8e17
// (5e18 * 8e17) / 1e18 = 4e18 allocated from Vault
vaultBufferModifier = uint256(1e18).sub(vaultBufferModifier);
} else {
// We need to let the buffer fill
return;
}
}
if (vaultBufferModifier == 0) return;
// Iterate over all assets in the Vault and allocate to the appropriate
// strategy
for (uint256 i = 0; i < allAssets.length; i++) {
IERC20 asset = IERC20(allAssets[i]);
uint256 assetBalance = asset.balanceOf(address(this));
// No balance, nothing to do here
if (assetBalance == 0) continue;
// Multiply the balance by the vault buffer modifier and truncate
// to the scale of the asset decimals
uint256 allocateAmount = assetBalance.mulTruncate(
vaultBufferModifier
);
address depositStrategyAddr = assetDefaultStrategies[
address(asset)
];
if (depositStrategyAddr != address(0) && allocateAmount > 0) {
IStrategy strategy = IStrategy(depositStrategyAddr);
// Transfer asset to Strategy and call deposit method to
// mint or take required action
asset.safeTransfer(address(strategy), allocateAmount);
strategy.deposit(address(asset), allocateAmount);
emit AssetAllocated(
address(asset),
depositStrategyAddr,
allocateAmount
);
}
}
}
/**
* @dev Calculate the total value of assets held by the Vault and all
* strategies and update the supply of OUSD.
*/
function rebase() external virtual nonReentrant {
_rebase();
}
/**
* @dev Calculate the total value of assets held by the Vault and all
* strategies and update the supply of OUSD, optionally sending a
* portion of the yield to the trustee.
*/
function _rebase() internal whenNotRebasePaused {
uint256 ousdSupply = oUSD.totalSupply();
if (ousdSupply == 0) {
return;
}
uint256 vaultValue = _totalValue();
// Yield fee collection
address _trusteeAddress = trusteeAddress; // gas savings
if (_trusteeAddress != address(0) && (vaultValue > ousdSupply)) {
uint256 yield = vaultValue.sub(ousdSupply);
uint256 fee = yield.mul(trusteeFeeBps).div(10000);
require(yield > fee, "Fee must not be greater than yield");
if (fee > 0) {
oUSD.mint(_trusteeAddress, fee);
}
emit YieldDistribution(_trusteeAddress, yield, fee);
}
// Only rachet OUSD supply upwards
ousdSupply = oUSD.totalSupply(); // Final check should use latest value
if (vaultValue > ousdSupply) {
oUSD.changeSupply(vaultValue);
}
}
/**
* @dev Determine the total value of assets held by the vault and its
* strategies.
* @return value Total value in USD (1e18)
*/
function totalValue() external view virtual returns (uint256 value) {
value = _totalValue();
}
/**
* @dev Internal Calculate the total value of the assets held by the
* vault and its strategies.
* @return value Total value in USD (1e18)
*/
function _totalValue() internal view virtual returns (uint256 value) {
return _totalValueInVault().add(_totalValueInStrategies());
}
/**
* @dev Internal to calculate total value of all assets held in Vault.
* @return value Total value in ETH (1e18)
*/
function _totalValueInVault() internal view returns (uint256 value) {
for (uint256 y = 0; y < allAssets.length; y++) {
IERC20 asset = IERC20(allAssets[y]);
uint256 balance = asset.balanceOf(address(this));
if (balance > 0) {
value += _toUnits(balance, allAssets[y]);
}
}
}
/**
* @dev Internal to calculate total value of all assets held in Strategies.
* @return value Total value in ETH (1e18)
*/
function _totalValueInStrategies() internal view returns (uint256 value) {
for (uint256 i = 0; i < allStrategies.length; i++) {
value = value.add(_totalValueInStrategy(allStrategies[i]));
}
}
/**
* @dev Internal to calculate total value of all assets held by strategy.
* @param _strategyAddr Address of the strategy
* @return value Total value in ETH (1e18)
*/
function _totalValueInStrategy(address _strategyAddr)
internal
view
returns (uint256 value)
{
IStrategy strategy = IStrategy(_strategyAddr);
for (uint256 y = 0; y < allAssets.length; y++) {
if (strategy.supportsAsset(allAssets[y])) {
uint256 balance = strategy.checkBalance(allAssets[y]);
if (balance > 0) {
value += _toUnits(balance, allAssets[y]);
}
}
}
}
/**
* @notice Get the balance of an asset held in Vault and all strategies.
* @param _asset Address of asset
* @return uint256 Balance of asset in decimals of asset
*/
function checkBalance(address _asset) external view returns (uint256) {
return _checkBalance(_asset);
}
/**
* @notice Get the balance of an asset held in Vault and all strategies.
* @param _asset Address of asset
* @return balance Balance of asset in decimals of asset
*/
function _checkBalance(address _asset)
internal
view
virtual
returns (uint256 balance)
{
IERC20 asset = IERC20(_asset);
balance = asset.balanceOf(address(this));
for (uint256 i = 0; i < allStrategies.length; i++) {
IStrategy strategy = IStrategy(allStrategies[i]);
if (strategy.supportsAsset(_asset)) {
balance = balance.add(strategy.checkBalance(_asset));
}
}
}
/**
* @notice Calculate the outputs for a redeem function, i.e. the mix of
* coins that will be returned
*/
function calculateRedeemOutputs(uint256 _amount)
external
view
returns (uint256[] memory)
{
(uint256[] memory outputs, ) = _calculateRedeemOutputs(_amount);
return outputs;
}
/**
* @notice Calculate the outputs for a redeem function, i.e. the mix of
* coins that will be returned.
* @return outputs Array of amounts respective to the supported assets
* @return totalUnits Total balance of Vault in units
*/
function _calculateRedeemOutputs(uint256 _amount)
internal
view
returns (uint256[] memory outputs, uint256 totalUnits)
{
// We always give out coins in proportion to how many we have,
// Now if all coins were the same value, this math would easy,
// just take the percentage of each coin, and multiply by the
// value to be given out. But if coins are worth more than $1,
// then we would end up handing out too many coins. We need to
// adjust by the total value of coins.
//
// To do this, we total up the value of our coins, by their
// percentages. Then divide what we would otherwise give out by
// this number.
//
// Let say we have 100 DAI at $1.06 and 200 USDT at $1.00.
// So for every 1 DAI we give out, we'll be handing out 2 USDT
// Our total output ratio is: 33% * 1.06 + 66% * 1.00 = 1.02
//
// So when calculating the output, we take the percentage of
// each coin, times the desired output value, divided by the
// totalOutputRatio.
//
// For example, withdrawing: 30 OUSD:
// DAI 33% * 30 / 1.02 = 9.80 DAI
// USDT = 66 % * 30 / 1.02 = 19.60 USDT
//
// Checking these numbers:
// 9.80 DAI * 1.06 = $10.40
// 19.60 USDT * 1.00 = $19.60
//
// And so the user gets $10.40 + $19.60 = $30 worth of value.
uint256 assetCount = allAssets.length;
uint256[] memory assetUnits = new uint256[](assetCount);
uint256[] memory assetBalances = new uint256[](assetCount);
outputs = new uint256[](assetCount);
// Calculate redeem fee
if (redeemFeeBps > 0) {
uint256 redeemFee = _amount.mul(redeemFeeBps).div(10000);
_amount = _amount.sub(redeemFee);
}
// Calculate assets balances and decimals once,
// for a large gas savings.
for (uint256 i = 0; i < assetCount; i++) {
uint256 balance = _checkBalance(allAssets[i]);
assetBalances[i] = balance;
assetUnits[i] = _toUnits(balance, allAssets[i]);
totalUnits = totalUnits.add(assetUnits[i]);
}
// Calculate totalOutputRatio
uint256 totalOutputRatio = 0;
for (uint256 i = 0; i < assetCount; i++) {
uint256 unitPrice = _toUnitPrice(allAssets[i], false);
uint256 ratio = assetUnits[i].mul(unitPrice).div(totalUnits);
totalOutputRatio = totalOutputRatio.add(ratio);
}
// Calculate final outputs
uint256 factor = _amount.divPrecisely(totalOutputRatio);
for (uint256 i = 0; i < assetCount; i++) {
outputs[i] = assetBalances[i].mul(factor).div(totalUnits);
}
}
/***************************************
Pricing
****************************************/
/**
* @dev Returns the total price in 18 digit units for a given asset.
* Never goes above 1, since that is how we price mints.
* @param asset address of the asset
* @return price uint256: unit (USD / ETH) price for 1 unit of the asset, in 18 decimal fixed
*/
function priceUnitMint(address asset)
external
view
returns (uint256 price)
{
/* need to supply 1 asset unit in asset's decimals and can not just hard-code
* to 1e18 and ignore calling `_toUnits` since we need to consider assets
* with the exchange rate
*/
uint256 units = _toUnits(
uint256(1e18).scaleBy(_getDecimals(asset), 18),
asset
);
price = (_toUnitPrice(asset, true) * units) / 1e18;
}
/**
* @dev Returns the total price in 18 digit unit for a given asset.
* Never goes below 1, since that is how we price redeems
* @param asset Address of the asset
* @return price uint256: unit (USD / ETH) price for 1 unit of the asset, in 18 decimal fixed
*/
function priceUnitRedeem(address asset)
external
view
returns (uint256 price)
{
/* need to supply 1 asset unit in asset's decimals and can not just hard-code
* to 1e18 and ignore calling `_toUnits` since we need to consider assets
* with the exchange rate
*/
uint256 units = _toUnits(
uint256(1e18).scaleBy(_getDecimals(asset), 18),
asset
);
price = (_toUnitPrice(asset, false) * units) / 1e18;
}
/***************************************
Utils
****************************************/
/**
* @dev Convert a quantity of a token into 1e18 fixed decimal "units"
* in the underlying base (USD/ETH) used by the vault.
* Price is not taken into account, only quantity.
*
* Examples of this conversion:
*
* - 1e18 DAI becomes 1e18 units (same decimals)
* - 1e6 USDC becomes 1e18 units (decimal conversion)
* - 1e18 rETH becomes 1.2e18 units (exchange rate conversion)
*
* @param _raw Quantity of asset
* @param _asset Core Asset address
* @return value 1e18 normalized quantity of units
*/
function _toUnits(uint256 _raw, address _asset)
internal
view
returns (uint256)
{
UnitConversion conversion = assets[_asset].unitConversion;
if (conversion == UnitConversion.DECIMALS) {
return _raw.scaleBy(18, _getDecimals(_asset));
} else if (conversion == UnitConversion.GETEXCHANGERATE) {
uint256 exchangeRate = IGetExchangeRateToken(_asset)
.getExchangeRate();
return (_raw * exchangeRate) / 1e18;
} else {
require(false, "Unsupported conversion type");
}
}
/**
* @dev Returns asset's unit price accounting for different asset types
* and takes into account the context in which that price exists -
* - mint or redeem.
*
* Note: since we are returning the price of the unit and not the one of the
* asset (see comment above how 1 rETH exchanges for 1.2 units) we need
* to make the Oracle price adjustment as well since we are pricing the
* units and not the assets.
*
* The price also snaps to a "full unit price" in case a mint or redeem
* action would be unfavourable to the protocol.
*
*/
function _toUnitPrice(address _asset, bool isMint)
internal
view
returns (uint256 price)
{
UnitConversion conversion = assets[_asset].unitConversion;
price = IOracle(priceProvider).price(_asset);
if (conversion == UnitConversion.GETEXCHANGERATE) {
uint256 exchangeRate = IGetExchangeRateToken(_asset)
.getExchangeRate();
price = (price * 1e18) / exchangeRate;
} else if (conversion != UnitConversion.DECIMALS) {
require(false, "Unsupported conversion type");
}
/* At this stage the price is already adjusted to the unit
* so the price checks are agnostic to underlying asset being
* pegged to a USD or to an ETH or having a custom exchange rate.
*/
require(price <= MAX_UNIT_PRICE_DRIFT, "Vault: Price exceeds max");
require(price >= MIN_UNIT_PRICE_DRIFT, "Vault: Price under min");
if (isMint) {
/* Never price a normalized unit price for more than one
* unit of OETH/OUSD when minting.
*/
if (price > 1e18) {
price = 1e18;
}
require(price >= MINT_MINIMUM_UNIT_PRICE, "Asset price below peg");
} else {
/* Never give out more than 1 normalized unit amount of assets
* for one unit of OETH/OUSD when redeeming.
*/
if (price < 1e18) {
price = 1e18;
}
}
}
function _getDecimals(address _asset) internal view returns (uint256) {
uint256 decimals = assets[_asset].decimals;
require(decimals > 0, "Decimals not cached");
return decimals;
}
/**
* @dev Return the number of assets supported by the Vault.
*/
function getAssetCount() public view returns (uint256) {
return allAssets.length;
}
/**
* @dev Return all asset addresses in order
*/
function getAllAssets() external view returns (address[] memory) {
return allAssets;
}
/**
* @dev Return the number of strategies active on the Vault.
*/
function getStrategyCount() external view returns (uint256) {
return allStrategies.length;
}
/**
* @dev Return the array of all strategies
*/
function getAllStrategies() external view returns (address[] memory) {
return allStrategies;
}
function isSupportedAsset(address _asset) external view returns (bool) {
return assets[_asset].isSupported;
}
/**
* @dev Falldown to the admin implementation
* @notice This is a catch all for all functions not declared in core
*/
// solhint-disable-next-line no-complex-fallback
fallback() external payable {
bytes32 slot = adminImplPosition;
// solhint-disable-next-line no-inline-assembly
assembly {
// Copy msg.data. We take full control of memory in this inline assembly
// block because it will not return to Solidity code. We overwrite the
// Solidity scratch pad at memory position 0.
calldatacopy(0, 0, calldatasize())
// Call the implementation.
// out and outsize are 0 because we don't know the size yet.
let result := delegatecall(
gas(),
sload(slot),
0,
calldatasize(),
0,
0
)
// Copy the returned data.
returndatacopy(0, 0, returndatasize())
switch result
// delegatecall returns 0 on error.
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
function abs(int256 x) private pure returns (uint256) {
require(x < int256(MAX_INT), "Amount too high");
return x >= 0 ? uint256(x) : uint256(-x);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment