Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
72ee0cc
Draft Dripper and Reserves
DanielVF Dec 15, 2022
c12b74f
handle zero drip rate
DanielVF Dec 15, 2022
07055b1
handle zero drip rate
DanielVF Dec 15, 2022
b0f92a4
Progress on admin functions and testing
DanielVF Dec 27, 2022
e26823b
Merge branch 'master' into DanielVF/oip-2-dripper
DanielVF Jan 5, 2023
92612a2
Added comments to warn us about doing a split accounting update
DanielVF Feb 6, 2023
838d6a7
Reorder and clarify rebaseOptOut and rebaseOptIn
DanielVF Feb 6, 2023
134f0a3
Merge
DanielVF Feb 6, 2023
f5a55a3
Use perfect rebasing
DanielVF Feb 16, 2023
1b29fba
Switch back to old verison, since it will also be perfectly accurate
DanielVF Feb 16, 2023
ab9da6a
Follow normal fast exit style, rather than nested ifs
DanielVF Feb 16, 2023
958070b
Use before change balance, as before
DanielVF Feb 16, 2023
7d723fb
Merge branch 'master' into DanielVF/oip-2-dripper
DanielVF Feb 23, 2023
89b7e25
Merge branch 'DanielVF/oip-2-dripper' of github.com:OriginProtocol/or…
DanielVF Mar 2, 2023
4316acf
merge
DanielVF Mar 2, 2023
6964877
rev 1
DanielVF Mar 3, 2023
aa53257
rev 2
DanielVF Mar 3, 2023
40a941f
rev 3
DanielVF Mar 3, 2023
b589747
rev 4
DanielVF Mar 3, 2023
c0f7eb5
Add spend reserve
DanielVF Mar 3, 2023
5a5c0a6
Remove inadvertant commit of OUSD WIP
DanielVF Mar 3, 2023
b0686a7
Add configuration of dripper duration
DanielVF Mar 3, 2023
c675000
Cleaner handling of partial dripper funds
DanielVF Mar 16, 2023
2db4abe
Moving back distrobution in allows us to lose some duplicate checks
DanielVF Mar 17, 2023
24c9b92
Goodbye inaccurate postDripperYield
DanielVF Mar 17, 2023
2f47f4c
Cleaner yield calculations
DanielVF Mar 17, 2023
8d4cb42
make view
DanielVF Mar 17, 2023
caa0d04
OIP-5
DanielVF Mar 17, 2023
3309fe6
Refactoring in _dripperAvailableFunds
DanielVF Mar 17, 2023
f144ea2
Set bps to make slither happy
DanielVF Mar 17, 2023
23b3757
Dust will be cleared if dripper disabled
DanielVF Mar 18, 2023
fffaf38
Only supported assets are a part of reserve calculations
DanielVF Mar 18, 2023
281852b
Without the if, no need to cache on stack
DanielVF Mar 18, 2023
bd6e0cf
Flipper N-14 Unused import (#1289)
DanielVF Apr 6, 2023
1b92b30
Dripper N-07 better variable name (#1285)
DanielVF Apr 17, 2023
68efe83
Dripper N-10 explicit visibilitiy (#1286)
DanielVF Apr 17, 2023
1952351
Flipper N-13 Unused functions (#1288)
DanielVF Apr 17, 2023
6d94749
Dripper N-14 Imports (#1332)
DanielVF Apr 17, 2023
e8136bb
Dripper N-12 typos (#1287)
DanielVF Apr 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions contracts/contracts/interfaces/IVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions contracts/contracts/vault/VaultAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
****************************************/
Expand Down
132 changes: 95 additions & 37 deletions contracts/contracts/vault/VaultCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,25 @@ 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 {
using SafeERC20 for IERC20;
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.
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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++) {
Expand All @@ -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++) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 21 additions & 3 deletions contracts/contracts/vault/VaultStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
58 changes: 57 additions & 1 deletion contracts/test/vault/rebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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"
);
});
});
});