Skip to content

Commit

Permalink
Require that oracle is prepared before update
Browse files Browse the repository at this point in the history
gs
  • Loading branch information
haydenshively committed Sep 8, 2023
1 parent 9af75cf commit 335264d
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 29 deletions.
13 changes: 6 additions & 7 deletions core/src/VolatilityOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ contract VolatilityOracle {
unchecked {
// Read `lastWrite` info from storage
LastWrite memory lastWrite = lastWrites[pool];
require(lastWrite.time > 0);

// We need to call `Oracle.consult` even if we're going to return early, so go ahead and do it
(Oracle.PoolData memory data, uint56 metric) = Oracle.consult(pool, seed);
Expand All @@ -71,9 +72,9 @@ contract VolatilityOracle {
// Only update IV if the feeGrowthGlobals samples are approximately `FEE_GROWTH_AVG_WINDOW` hours apart
if (
_isInInterval({
min: FEE_GROWTH_AVG_WINDOW - 3 * FEE_GROWTH_SAMPLE_PERIOD,
min: FEE_GROWTH_AVG_WINDOW - FEE_GROWTH_SAMPLE_PERIOD / 2,
x: b.timestamp - a.timestamp,
max: FEE_GROWTH_AVG_WINDOW + 3 * FEE_GROWTH_SAMPLE_PERIOD
max: FEE_GROWTH_AVG_WINDOW + FEE_GROWTH_SAMPLE_PERIOD / 2
})
) {
// Estimate, then clamp so it lies within [previous - maxChange, previous + maxChange]
Expand Down Expand Up @@ -166,11 +167,9 @@ contract VolatilityOracle {
atOrAfter = arr[(i + 1) % FEE_GROWTH_ARRAY_LENGTH];

if (_isInInterval(beforeOrAt.timestamp, target, atOrAfter.timestamp)) break;
if (beforeOrAt.timestamp <= target) {
l = i + 1;
} else {
r = i - 1;
}

if (target < beforeOrAt.timestamp) r = i - 1;
else l = i + 1;
}

uint256 errorA = target - beforeOrAt.timestamp;
Expand Down
18 changes: 13 additions & 5 deletions core/src/libraries/constants/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,31 @@ uint256 constant IV_MIN = 0.01e18;
/// To avoid underflow in `BalanceSheet.computeProbePrices`, ensure that `IV_MAX * nSigma <= 1e18`
uint256 constant IV_MAX = 0.18e18;

/// @dev The timescale of implied volatility, applied to measurements and calculations. When `BalanceSheet` detects
/// that an `nSigma` event would cause insolvency in this time period, it enables liquidations. So if you squint your
/// eyes and wave your hands enough, this is (in expectation) the time liquidators have to act before the protocol
/// accrues bad debt.
uint256 constant IV_SCALE = 24 hours;

uint256 constant IV_CHANGE_PER_SECOND = 5e12;
/// @dev The maximum rate at which (reported) implied volatility can change. Raw samples in `VolatilityOracle.update`
/// are clamped (before being stored) so as not to exceed this rate.
/// Expressed in wad percentage points at `IV_SCALE` **per second**, e.g. {462962962962, 24 hours} means daily IV can
/// change by 0.0000463 percentage points per second → 4 percentage points per day.
uint256 constant IV_CHANGE_PER_SECOND = 462962962962;

/// @dev To estimate volume, we need 2 samples. One is always at the current block, the other is from
/// `FEE_GROWTH_AVG_WINDOW` seconds ago, +/- `3 * FEE_GROWTH_SAMPLE_PERIOD`. Larger values make the resulting volume
/// `FEE_GROWTH_AVG_WINDOW` seconds ago, +/- `FEE_GROWTH_SAMPLE_PERIOD / 2`. Larger values make the resulting volume
/// estimate more robust, but may cause the oracle to miss brief spikes in activity.
uint256 constant FEE_GROWTH_AVG_WINDOW = 6 hours;

/// @dev The length of the circular buffer that stores feeGrowthGlobals samples.
/// Must be in interval
/// \\( \left[ \frac{\text{FEE_GROWTH_AVG_WINDOW}}{\text{FEE_GROWTH_SAMPLE_PERIOD}}, 256 \right) \\)
uint256 constant FEE_GROWTH_ARRAY_LENGTH = 72;
uint256 constant FEE_GROWTH_ARRAY_LENGTH = 48;

/// @dev The minimum number of seconds that must elapse before a new feeGrowthGlobals sample will be stored. This also
/// @dev The minimum number of seconds that must elapse before a new feeGrowthGlobals sample will be stored. This
/// controls how often the oracle can update IV.
uint256 constant FEE_GROWTH_SAMPLE_PERIOD = 5 minutes;
uint256 constant FEE_GROWTH_SAMPLE_PERIOD = 15 minutes;

/// @dev To compute Uniswap mean price & liquidity, we need 2 samples. One is always at the current block, the other is
/// from `UNISWAP_AVG_WINDOW` seconds ago. Larger values make the resulting price/liquidity values harder to
Expand Down
35 changes: 18 additions & 17 deletions core/test/VolatilityOracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ contract VolatilityOracleTest is Test {
uint256 constant SIX_HOURS_LATER = 70_045_000;
uint256 constant TWELVE_HOURS_LATER = 70_090_000;

uint256 constant BLOCKS_PER_MINUTE = 567;
uint256 constant BLOCKS_PER_SECOND = 2;

VolatilityOracle oracle;

Expand Down Expand Up @@ -49,12 +49,8 @@ contract VolatilityOracleTest is Test {
for (uint256 i = 0; i < count; i++) {
IUniswapV3Pool pool = IUniswapV3Pool(pools[i]);

(uint56 metricConsult, uint160 priceConsult, uint256 ivConsult) = oracle.consult(pool, (1 << 32));
(uint56 metricUpdate, uint160 priceUpdate, uint256 ivUpdate) = oracle.update(pool, (1 << 32));

assertEq(metricConsult, metricUpdate);
assertEq(priceUpdate, priceConsult);
assertEqDecimal(ivUpdate, ivConsult, 18);
vm.expectRevert(bytes(""));
oracle.update(pool, (1 << 32));
}
}

Expand Down Expand Up @@ -95,7 +91,7 @@ contract VolatilityOracleTest is Test {

(uint256 index, uint256 time, uint256 ivOldExpected) = oracle.lastWrites(pool);
assertEq(index, 0);
assertGe(block.timestamp, time + 6 hours + 15 minutes);
assertGe(block.timestamp, time + 6 hours + 7.5 minutes);

(, , uint256 ivOld) = oracle.consult(pool, (1 << 32));
(, , uint256 ivNew) = oracle.update(pool, (1 << 32));
Expand Down Expand Up @@ -149,7 +145,6 @@ contract VolatilityOracleTest is Test {
(, , uint256 iv) = oracle.update(pool, (1 << 32));
(bytes32[] memory reads, bytes32[] memory writes) = vm.accesses(address(oracle));

// NOTE: When compiling without --via-ir, foundry doesn't count correctly
assertEq(reads.length, 10);
assertEq(writes.length, 4);

Expand All @@ -162,29 +157,35 @@ contract VolatilityOracleTest is Test {
}

function test_historical_updateSequenceETHUSDC() public {
uint256 currentBlock = 109019618;
vm.createSelectFork("optimism", currentBlock);

IUniswapV3Pool pool = IUniswapV3Pool(pools[1]); // WETH/USDC
oracle = new VolatilityOracle();
oracle.prepare(pool);

vm.makePersistent(address(oracle));

uint256 currentBlock = START_BLOCK;
(uint256 currentIndex, uint256 currentTime, uint256 currentIV) = oracle.lastWrites(pool);

uint256 initialTime = currentTime;

for (uint256 i = 0; i < 100; i++) {
for (uint256 i = 0; i < 48; i++) {
console2.log(currentTime, currentIV);

currentBlock += (2 + uint256(blockhash(block.number)) % 10) * BLOCKS_PER_MINUTE * 5;
vm.rollFork(currentBlock);
uint256 interval = FEE_GROWTH_SAMPLE_PERIOD * 2;
currentBlock += BLOCKS_PER_SECOND * interval;
vm.createSelectFork("optimism", currentBlock);

(, , uint256 ivWritten) = oracle.update(pool, (1 << 32));
(uint256 newIndex, uint256 newTime, uint256 ivStored) = oracle.lastWrites(pool);

assertEqDecimal(ivStored, ivWritten, 18);
assertEq(newIndex, (currentIndex + 1) % FEE_GROWTH_ARRAY_LENGTH);

assertLe(ivWritten, currentIV + (newTime - currentTime) * IV_CHANGE_PER_SECOND);
assertGe(ivWritten, currentIV - (newTime - currentTime) * IV_CHANGE_PER_SECOND);
uint256 maxChange = (newTime - currentTime) * IV_CHANGE_PER_SECOND;
assertLe(ivWritten, currentIV + maxChange);
assertGe(ivWritten + maxChange, currentIV);

currentIndex = newIndex;
currentTime = newTime;
Expand All @@ -203,8 +204,8 @@ contract VolatilityOracleTest is Test {
uint256 totalGas = 0;

for (uint256 i = 0; i < 600; i++) {
currentBlock += (1 + uint256(blockhash(block.number)) % 3) * BLOCKS_PER_MINUTE * 60;
vm.rollFork(currentBlock);
currentBlock += (1 + uint256(blockhash(block.number)) % 3) * 7200;
vm.createSelectFork("optimism", currentBlock);

uint256 g = gasleft();
(uint56 metric, uint160 sqrtPriceX96, uint256 iv) = oracle.update(pool, (1 << 32));
Expand Down

0 comments on commit 335264d

Please sign in to comment.