diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index 1514fa4904..39be7be71e 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -60,6 +60,10 @@ interface IVault { function maxSupplyDiff() external view returns (uint256); + function setProtocolReserveBps(uint256 _basis) external; + + function protocolReserve() external view returns (uint256); + function setTrusteeAddress(address _address) external; function trusteeAddress() external view returns (address); diff --git a/contracts/contracts/vault/VaultAdmin.sol b/contracts/contracts/vault/VaultAdmin.sol index bedf84c789..1dd29341eb 100644 --- a/contracts/contracts/vault/VaultAdmin.sol +++ b/contracts/contracts/vault/VaultAdmin.sol @@ -11,6 +11,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { StableMath } from "../utils/StableMath.sol"; import { IOracle } from "../interfaces/IOracle.sol"; +import { IStrategy } from "../interfaces/IStrategy.sol"; import "./VaultStorage.sol"; contract VaultAdmin is VaultStorage { @@ -352,6 +353,26 @@ contract VaultAdmin is VaultStorage { emit MaxSupplyDiffChanged(_maxSupplyDiff); } + /** + * @dev Sets the percent of top-line yield that should be + * set aside as a reserve. + * @param _basis yield reserved, in basis points + */ + function setProtocolReserveBps(uint256 _basis) external onlyGovernor { + require(_basis <= 5000, "basis cannot exceed 50%"); + protocolReserveBps = _basis; + emit ProtocolReserveBpsChanged(_basis); + } + + /** + * @dev Sets the driper duration + * @param _durationSeconds length of time to drip out dripper reserves over + */ + function setDripDuration(uint64 _durationSeconds) external onlyGovernor { + dripper.dripDuration = _durationSeconds; + emit DripperDurationChanged(_durationSeconds); + } + /** * @dev Sets the trusteeAddress that can receive a portion of yield. * Setting to the zero address disables this feature. @@ -437,6 +458,15 @@ contract VaultAdmin is VaultStorage { IERC20(_asset).safeTransfer(governor(), _amount); } + function spendReserve(address _asset, uint256 _amount) + external + onlyGovernor + { + require(assets[_asset].isSupported, "Only supported assets"); + protocolReserve -= _amount; + IERC20(_asset).safeTransfer(governor(), _amount); + } + /*************************************** Pricing ****************************************/ diff --git a/contracts/contracts/vault/VaultCore.sol b/contracts/contracts/vault/VaultCore.sol index 3ee58e96b4..4075a51553 100644 --- a/contracts/contracts/vault/VaultCore.sol +++ b/contracts/contracts/vault/VaultCore.sol @@ -13,12 +13,12 @@ pragma solidity ^0.8.0; 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 { IOracle } from "../interfaces/IOracle.sol"; -import { IVault } from "../interfaces/IVault.sol"; import { IBuyback } from "../interfaces/IBuyback.sol"; +import { IStrategy } from "../interfaces/IStrategy.sol"; +import "../utils/Helpers.sol"; import "./VaultStorage.sol"; contract VaultCore is VaultStorage { @@ -26,10 +26,12 @@ contract VaultCore is VaultStorage { using StableMath for uint256; using SafeMath for uint256; // max signed int - uint256 constant MAX_INT = 2**255 - 1; + uint256 internal constant MAX_INT = 2**255 - 1; // max un-signed int - uint256 constant MAX_UINT = + uint256 internal constant MAX_UINT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + // impl contract address + address internal immutable SELF = address(this); /** * @dev Verifies that the rebasing is not paused. @@ -382,34 +384,102 @@ contract VaultCore is VaultStorage { } /** - * @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. + * @dev Update the supply of ousd + * + * 1. Calculate new gains, splitting gains between the dripper and protocol reserve + * 2. Drip out from the dripper and update the dripper storage + * 3. Distribute yield, splitting between trustee fees and rebasing + * the remaining post dripper funds to users + * + * After running: + * - If the protocol started solvent then: + * the protocol should end solvent + * - If the protocol started solvent and had yield then: + * ousd supply + reserves should equal vault value. + * - If the protocol started insolvent then: + * no funds should be distributed, or reserves changed + * - All cases: + * ending OUSD total supply should not be less than starting totalSupply */ function _rebase() internal whenNotRebasePaused { - uint256 ousdSupply = oUSD.totalSupply(); + // Load data used for rebasing + uint256 ousdSupply = oUSD.totalSupply(); // gas savings + uint256 vaultValue = _totalValue(); // gas savings if (ousdSupply == 0) { - return; + return; // If there is no OUSD supply, we will not rebase + } + if (vaultValue < ousdSupply) { + return; // Do not distribute funds if assets < liabilities + } + uint256 _dripperReserve = dripperReserve; // gas savings + + // 1. Calculate new gains, then split them between the dripper and + // protocol reserve + uint256 usedValue = ousdSupply + protocolReserve + _dripperReserve; + if (vaultValue > usedValue) { + uint256 newYield = vaultValue - usedValue; + uint256 toProtocolReserve = (newYield * protocolReserveBps) / 10000; + protocolReserve += toProtocolReserve; + _dripperReserve += newYield - toProtocolReserve; + emit YieldReceived(newYield); } - 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) { + // 2. Drip out from the dripper and update the dripper storage + Dripper memory _dripper = dripper; // gas savings + uint256 _dripDuration = _dripper.dripDuration; // gas, not written back + if (_dripDuration == 0) { + _dripDuration = 1; // Prevent divide by zero later + _dripperReserve = 0; // Dripper disabled, distribute all now + } else { + _dripperReserve -= _dripperAvailableFunds( + _dripperReserve, + _dripper + ); + } + + // Write dripper state + dripperReserve = _dripperReserve; + dripper = Dripper({ + perSecond: uint128(_dripperReserve / _dripDuration), + lastCollect: uint64(block.timestamp), + dripDuration: _dripper.dripDuration // must use stored value + }); + + // 3. Distribute fees then rebase to users + usedValue = ousdSupply + protocolReserve + _dripperReserve; + if (vaultValue > usedValue) { + uint256 yield = vaultValue - usedValue; + + // Mint trustee fees + address _trusteeAddress = trusteeAddress; // gas savings + uint256 fee = 0; + if (_trusteeAddress != address(0)) { + fee = (yield * trusteeFeeBps) / 10000; + require(fee < yield, "Fee must be less than yield"); oUSD.mint(_trusteeAddress, fee); } + + // Rebase remaining to users + // Invariant: must only increase OUSD supply. + // Can only increase because: + // ousdSupply + yield >= ousdSupply and yield > fee + oUSD.changeSupply(ousdSupply + yield); 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); - } + function dripperAvailableFunds() external view returns (uint256) { + return _dripperAvailableFunds(dripperReserve, dripper); + } + + function _dripperAvailableFunds(uint256 _reserve, Dripper memory _drip) + internal + view + returns (uint256) + { + uint256 elapsed = block.timestamp - _drip.lastCollect; + uint256 allowed = (elapsed * _drip.perSecond); + return (allowed > _reserve) ? _reserve : allowed; } /** @@ -432,7 +502,7 @@ contract VaultCore is VaultStorage { /** * @dev Internal to calculate total value of all assets held in Vault. - * @return value Total value in ETH (1e18) + * @return value Total value in USD (1e18) */ function _totalValueInVault() internal view returns (uint256 value) { for (uint256 y = 0; y < allAssets.length; y++) { @@ -447,7 +517,7 @@ contract VaultCore is VaultStorage { /** * @dev Internal to calculate total value of all assets held in Strategies. - * @return value Total value in ETH (1e18) + * @return value Total value in USD (1e18) */ function _totalValueInStrategies() internal view returns (uint256 value) { for (uint256 i = 0; i < allStrategies.length; i++) { @@ -507,19 +577,6 @@ contract VaultCore is VaultStorage { } } - /** - * @notice Get the balance of all assets held in Vault and all strategies. - * @return balance Balance of all assets (1e18) - */ - function _checkBalance() internal view returns (uint256 balance) { - for (uint256 i = 0; i < allAssets.length; i++) { - uint256 assetDecimals = Helpers.getDecimals(allAssets[i]); - balance = balance.add( - _checkBalance(allAssets[i]).scaleBy(18, assetDecimals) - ); - } - } - /** * @notice Calculate the outputs for a redeem function, i.e. the mix of * coins that will be returned @@ -677,6 +734,7 @@ contract VaultCore is VaultStorage { */ // solhint-disable-next-line no-complex-fallback fallback() external payable { + require(SELF != address(this), "Must be proxied"); bytes32 slot = adminImplPosition; // solhint-disable-next-line no-inline-assembly assembly { diff --git a/contracts/contracts/vault/VaultStorage.sol b/contracts/contracts/vault/VaultStorage.sol index fef80b07b2..979706016f 100644 --- a/contracts/contracts/vault/VaultStorage.sol +++ b/contracts/contracts/vault/VaultStorage.sol @@ -12,11 +12,9 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { SafeMath } from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import { Address } from "@openzeppelin/contracts/utils/Address.sol"; -import { IStrategy } from "../interfaces/IStrategy.sol"; import { Governable } from "../governance/Governable.sol"; import { OUSD } from "../token/OUSD.sol"; import { Initializable } from "../utils/Initializable.sol"; -import "../utils/Helpers.sol"; import { StableMath } from "../utils/StableMath.sol"; contract VaultStorage is Initializable, Governable { @@ -44,7 +42,10 @@ contract VaultStorage is Initializable, Governable { event RebaseThresholdUpdated(uint256 _threshold); event StrategistUpdated(address _address); event MaxSupplyDiffChanged(uint256 maxSupplyDiff); + event YieldReceived(uint256 _yield); event YieldDistribution(address _to, uint256 _yield, uint256 _fee); + event ProtocolReserveBpsChanged(uint256 _basis); + event DripperDurationChanged(uint256 _seconds); event TrusteeFeeBpsChanged(uint256 _basis); event TrusteeAddressChanged(address _address); event NetOusdMintForStrategyThresholdChanged(uint256 _threshold); @@ -109,7 +110,7 @@ contract VaultStorage is Initializable, Governable { // Deprecated: Tokens that should be swapped for stablecoins address[] private _deprecated_swapTokens; - uint256 constant MINT_MINIMUM_ORACLE = 99800000; + uint256 internal constant MINT_MINIMUM_ORACLE = 99800000; // Meta strategy that is allowed to mint/burn OUSD without changing collateral address public ousdMetaStrategy = address(0); @@ -120,6 +121,23 @@ contract VaultStorage is Initializable, Governable { // How much net total OUSD is allowed to be minted by all strategies uint256 public netOusdMintForStrategyThreshold = 0; + // Reserve funds held by the protocol + uint256 public protocolReserve = 0; + + // Amount of reserve collected in Bps + uint256 public protocolReserveBps = 0; + + // Dripper funds held by the protocol + uint256 public dripperReserve = 0; + + // Dripper config/state + struct Dripper { + uint64 lastCollect; + uint128 perSecond; + uint64 dripDuration; + } + Dripper public dripper; + /** * @dev set the implementation for the admin, this needs to be in a base class else we cannot set it * @param newImpl address of the implementation diff --git a/contracts/test/vault/rebase.js b/contracts/test/vault/rebase.js index 3416702bcb..bed678da26 100644 --- a/contracts/test/vault/rebase.js +++ b/contracts/test/vault/rebase.js @@ -219,7 +219,7 @@ describe("Vault rebasing", async () => { }); }); -describe("Vault yield accrual to OGN", async () => { +describe("Vault yield accrual to trustee", async () => { [ { _yield: "1000", basis: 100, expectedFee: "10" }, { _yield: "1000", basis: 5000, expectedFee: "500" }, @@ -252,3 +252,59 @@ describe("Vault yield accrual to OGN", async () => { }); }); }); + +describe("Vault protocol reserve accrual", async () => { + [ + { + _yield: "1000", + reserveBasis: 100, + expectedReserveIncrease: "10", + expectedRebase: "990", + }, + { + _yield: "1000", + reserveBasis: 1000, + expectedReserveIncrease: "100", + expectedRebase: "900", + }, + { + _yield: "10000", + reserveBasis: 5000, + expectedReserveIncrease: "5000", + expectedRebase: "5000", + }, + ].forEach((options) => { + const { _yield, reserveBasis, expectedReserveIncrease, expectedRebase } = + options; + it(`should collect ${expectedReserveIncrease} reserve from ${_yield} yield at ${reserveBasis}bp `, async function () { + const fixture = await loadFixture(defaultFixture); + const { matt, governor, ousd, usdt, vault } = fixture; + // const trustee = mockNonRebasing; + + // Setup reserve rate + await vault.connect(governor).setProtocolReserveBps(reserveBasis); + + // Create yield for the vault + await usdt.connect(matt).mint(usdcUnits(_yield)); + await usdt.connect(matt).transfer(vault.address, usdcUnits(_yield)); + // Do rebase + const supplyBefore = await ousd.totalSupply(); + const protocolReserveBefore = await vault.protocolReserve(); + + await vault.rebase(); + // OUSD supply increases correctly + await expectApproxSupply( + ousd, + supplyBefore.add(ousdUnits(expectedRebase)), + "Supply" + ); + // Reserve increased + const protocolReserveAfter = await vault.protocolReserve(); + await expect(protocolReserveAfter).to.be.approxEqualTolerance( + protocolReserveBefore.add(ousdUnits(expectedReserveIncrease)), + 1, + "Reserve" + ); + }); + }); +});