Skip to content

Commit

Permalink
Volt System Oracle (volt-protocol#82)
Browse files Browse the repository at this point in the history
* Volt System Oracle

* remove reference to scaling price oracle, and instead set start price in the constructor

* add helper method to fuzz tests to avoid duplicate logic in tests

* add new fuzz tests

* fix failing tests from merge with develop

* fix failing integration test

* add fuzz test for multiple sequential compounding periods

* fix fuzz test for multiple sequential compounding periods

* add oracle integration tests

* add integration tests for PSM swaps

* fix PR feedback review

* fix nits

* remove cycles terminology

* make period max 6,193 in fuzz test

* remove duplicate contract calls in unit test

* remove fuzz test and instead test all 6,192 states instead

* add redeem flow for integration test

* VIP-2, deploy new Oracle Pass Through, Volt System Oracle, and set PSM Oracle to point to new Oracle Pass Through

* add arbitrum integration tests

* Add newlines in VoltOracle doc, remove whitespace in config.yml

* fix unit tests, add additional test coverage around oracle end time, update test run script in package.json

* Update VoltOracle.md

Co-authored-by: Kyle <kyletrusler@gmail.com>

* remove word minified from VoltOracle

* Update to use LaTex

* update LaTeX to include original formula, and left justify content

* LaTeX

* remove non LaTeX from github formula

* add arbitrum simulation framework

* add linear interpolation formula and unit tests to verify algorithm

* streamline tests

* test lerp across each day in a whole year

* vip 2 additions to add eoa guardian on arbitrum, cleanup revoked role on arbitrum proposer, add new eoa1 proposer to timelock

* grant eoa 2 guardian on mainnet

* Governance Docs

* Move Volt System Oracle to monthly compounding

* Update name in package.json

* Docs on known contract limitations

* set mint fee basis points to 0 on L1, and 5 basis points on L2

* Add integration tests to lower mint fee on L1 to 0 and L2 to 5 bps

* fix Russel's feedback on PR, corrected vip_2 typo, updated arbitrum integration test to pass after upgrade, removed magic address and switched to ArbitrumAddresses

* remove unneeded override in VoltSystemOracle.sol state variables

* fix arbitrum test comments and variable names

* remove all references to previous 1 year compounding period

* update docs to use the month instead of year for period, renamed variable in unit test

* add todo in config

* update governance actions to not add or revoke system roles

* New Volt System Oracle, Oracle Pass Through all owned by Timelock on mainnet and arbitrum. Oracle will go live August 2nd

* vip_2 validation in solidity, matching ts and solidity timelock proposal data

* Add simulation framework into integration tests, extend timelock simulation framework to optionally log calldata to make integration tests not log unnecessary data

* remove unused variables in integration tests

* update to allow only 1 basis point of deviation in mainnet tests

* assert oracle is correctly updated by governance proposal, bump deviation back up to 5 bips in mainnet validation

* add fuzzing to integration testing for get mint and redeem amount out on both mainnet and arbitrum

* add command to generate arbitrum calldata to package.json

* fix integration tests to have correct end time based on actual proposal

* fix timelock simulation framework to not schedule already executed proposals

* remove assertion that price is greater after upgrade

Co-authored-by: Kyle <kyletrusler@gmail.com>
  • Loading branch information
ElliotFriedman and Namaskar-1F64F authored Aug 3, 2022
1 parent 928a902 commit 8f8ead6
Show file tree
Hide file tree
Showing 31 changed files with 2,115 additions and 86 deletions.
21 changes: 21 additions & 0 deletions VoltOracle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Volt System Oracle

The Volt System Oracle is an oracle that tracks the yield earned in the underlying venues of PCV, and then passes that yield on to Volt holders by setting a rate and increasing the Volt target price.

## Volt Oracle Architecture

The Volt System Oracle sits behind an Oracle Pass Through contract that the time-lock owns. This allows for a changing out of the underlying PCV venues in which the Volt System Oracle points to. A change will occur "at-will" given significant enough deviation of the rates.

The following is the Volt System Oracle Formula where p equals price at the start of the period, p<sub>1</sub> equals new price after all changes are applied, and t equals time.

$$
\begin{align*}
Δt &= min(currentTimestamp - startTime, compoundingPeriod) \\
Δp &= p \cdot \frac{interestRate}{10,000} \\
p_{1} &= p + (\frac{Δp \cdot Δt}{compoundingPeriod})
\end{align*}
$$

Compounding period for the Volt System Oracle is 30.42 days and does not take leap years into account.

Interest accrues per second as long as block.timestamp is greater than start time. After the period is over, the function `compoundInterest` can be called, which sets the start time to the previous start time plus the period length. This means that if a previous period had not been compounded, `compoundInterest` can be called multiple times to catch the oracle price up to where it should be. Interest will not be factored into the current price if `compoundInterest` is not called after the period ends. This is expected behavior as this contract is meant to be as simple as possible.
40 changes: 40 additions & 0 deletions contracts/oracle/IVoltSystemOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.4;

/// @notice interface for the Volt System Oracle
interface IVoltSystemOracle {
// ----------- Getters -----------

/// @notice function to get the current oracle price for the entire system
function getCurrentOraclePrice() external view returns (uint256);

/// @notice start time at which point interest will start accruing, and the
/// current ScalingPriceOracle price will be snapshotted and saved
function periodStartTime() external view returns (uint256);

/// @notice oracle price. starts off at 1e18 and compounds monthly
/// acts as an accumulator for interest earned in previous epochs
/// returns the oracle price from the end of the last period
function oraclePrice() external view returns (uint256);

/// @notice current amount that oracle price is inflating by monthly in basis points
/// does not support negative rates because PCV will not be deposited into negatively
/// yielding venues.
function monthlyChangeRateBasisPoints() external view returns (uint256);

/// @notice the time frame over which all changes in the APR are applied
/// one month was chosen because this is a temporary oracle
function TIMEFRAME() external view returns (uint256);

// ----------- Public State Changing API -----------

/// @notice public function that allows compounding of interest after duration has passed
/// Sets accumulator to the current accrued interest, and then resets the timer.
function compoundInterest() external;

// ----------- Event -----------

/// @notice event emitted when the Volt system oracle compounds
/// emits the end time of the period that completed and the new oracle price
event InterestCompounded(uint256 periodStart, uint256 newOraclePrice);
}
90 changes: 90 additions & 0 deletions contracts/oracle/VoltSystemOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.4;

import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Constants} from "./../Constants.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {IVoltSystemOracle} from "./IVoltSystemOracle.sol";

/// @notice contract that receives a fixed interest rate upon construction,
/// and then linearly interpolates that rate over a 30.42 day period into the VOLT price
/// after the oracle start time.
/// Interest can compound annually. Assumption is that this oracle will only be used until
/// Volt 2.0 ships. Maximum amount of compounding periods on this contract at 2% APR
/// is 6192 years, which is more than enough for this use case.
/// @author Elliot Friedman
contract VoltSystemOracle is IVoltSystemOracle {
/// ---------- Mutable Variables ----------

/// @notice acts as an accumulator for interest earned in previous periods
/// returns the oracle price from the end of the last period
uint256 public oraclePrice;

/// @notice start time at which point interest will start accruing, and the
/// current ScalingPriceOracle price will be snapshotted and saved
uint256 public periodStartTime;

/// ---------- Immutable Variables ----------

/// @notice current amount that oracle price is inflating by monthly in basis points
uint256 public immutable monthlyChangeRateBasisPoints;

/// @notice the time frame over which all changes in the APR are applied
/// one month was chosen because this is a temporary oracle
uint256 public constant TIMEFRAME = 30.42 days;

/// @param _monthlyChangeRateBasisPoints monthly change rate in the Volt price
/// @param _periodStartTime start time at which oracle starts interpolating prices
/// @param _oraclePrice starting oracle price
constructor(
uint256 _monthlyChangeRateBasisPoints,
uint256 _periodStartTime,
uint256 _oraclePrice
) {
monthlyChangeRateBasisPoints = _monthlyChangeRateBasisPoints;
periodStartTime = _periodStartTime;
oraclePrice = _oraclePrice;
}

// ----------- Getter -----------

/// @notice get the current scaled oracle price
/// applies the change rate smoothly over a 30.42 day period
/// scaled by 18 decimals
// prettier-ignore
function getCurrentOraclePrice() public view override returns (uint256) {
uint256 cachedStartTime = periodStartTime; /// save a single warm SLOAD if condition is false
if (cachedStartTime >= block.timestamp) { /// only accrue interest after start time
return oraclePrice;
}

uint256 cachedOraclePrice = oraclePrice; /// save a single warm SLOAD by using the stack
uint256 timeDelta = Math.min(block.timestamp - cachedStartTime, TIMEFRAME);
uint256 pricePercentageChange = cachedOraclePrice * monthlyChangeRateBasisPoints / Constants.BASIS_POINTS_GRANULARITY;
uint256 priceDelta = pricePercentageChange * timeDelta / TIMEFRAME;

return cachedOraclePrice + priceDelta;
}

/// ------------- Public State Changing API -------------

/// @notice public function that allows compounding of interest after duration has passed
/// Sets accumulator to the current accrued interest, and then resets the timer.
function compoundInterest() external override {
uint256 periodEndTime = periodStartTime + TIMEFRAME; /// save a single warm SLOAD when writing to periodStartTime
require(
block.timestamp >= periodEndTime,
"VoltSystemOracle: not past end time"
);

/// first set Oracle Price to interpolated value
oraclePrice = getCurrentOraclePrice();

/// set periodStartTime to periodStartTime + timeframe,
/// this is equivalent to init timed, which wipes out all unaccumulated compounded interest
/// and cleanly sets the start time.
periodStartTime = periodEndTime;

emit InterestCompounded(periodStartTime, oraclePrice);
}
}
Loading

0 comments on commit 8f8ead6

Please sign in to comment.