From 5d88731babd62cb57bf60db9f12d4d3436150735 Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:09:03 +0200 Subject: [PATCH 01/10] poke once a day, use struct, more changes --- src/StickyOracle.sol | 122 +++++++++++++++++++------------------ test/StickyOracle.t.sol | 130 ++++++++++++++++++++++------------------ 2 files changed, 136 insertions(+), 116 deletions(-) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index 2c41a394..dbce85ee 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -23,23 +23,30 @@ interface PipLike { contract StickyOracle { mapping (address => uint256) public wards; - mapping (address => uint256) public buds; // Whitelisted feed readers - mapping (uint256 => uint256) accumulators; // daily (eod) sticky oracle price accumulators + mapping (address => uint256) public buds; // whitelisted feed readers - PipLike public immutable pip; + mapping (uint256 => Accumulator ) accumulators; // daily sticky oracle price accumulators + Accumulator accLast; // last set accumulator + uint128 cap; // max allowed price uint96 public slope = uint96(RAY); // maximum allowable price growth factor from center of TWAP window to now (in RAY such that slope = (1 + {max growth rate}) * RAY) - uint8 public lo; // how many days ago should the TWAP window start (exclusive) - uint8 public hi; // how many days ago should the TWAP window end (inclusive) + uint8 public lo; // how many days ago should the TWAP window start (exclusive), should be more than hi + uint8 public hi; // how many days ago should the TWAP window end (inclusive), should be less than lo and more than 0 - uint128 val; // last poked price - uint32 public age; // time of last poke + PipLike public immutable pip; + + struct Accumulator { + uint256 val; + uint32 ts; + } event Rely(address indexed usr); event Deny(address indexed usr); event Kiss(address indexed usr); event Diss(address indexed usr); event File(bytes32 indexed what, uint256 data); + event Init(uint256 days_, uint128 cur); + event Poke(uint256 indexed day, uint128 cap); constructor(address _pip) { pip = PipLike(_pip); @@ -77,77 +84,76 @@ contract StickyOracle { return a < b ? a : b; } - function _getCap() internal view returns (uint128 cap) { + function _calcCap() internal view returns (uint128 cap_) { uint256 today = block.timestamp / 1 days; (uint96 slope_, uint8 lo_, uint8 hi_) = (slope, lo, hi); require(hi_ > 0 && lo_ > hi_, "StickyOracle/invalid-window"); - uint256 acc_lo = accumulators[today - lo_]; - uint256 acc_hi = accumulators[today - hi_]; + Accumulator memory acc_lo = accumulators[today - lo_]; + Accumulator memory acc_hi = accumulators[today - hi_]; - if (acc_lo > 0 && acc_hi > 0) { - return uint128((acc_hi - acc_lo) * slope_ / (RAY * (lo_ - hi_) * 1 days)); - } - - uint256 val_ = val; - require(val_ > 0, "StickyOracle/not-init"); - return uint128(val_ * slope_ / RAY); // fallback for missing accumulators + return (acc_lo.val > 0 && acc_hi.val > 0) ? + uint128((acc_hi.val - acc_lo.val) * slope_ / (RAY * (acc_hi.ts - acc_lo.ts))) : + 0; } + // days_ is the number of daily samples to initialize on top of the current one + // days_ == X will fill up a window corresponding to [lo == X, hi == 1] along with the current day + // days_ should be selected carefully as too many iterations can cause the transaction to run out of gas + // if the initiated timespan is shorter than the [lo, hi] window the initial cap will just be used for longer function init(uint256 days_) external auth { - require(val == 0, "StickyOracle/already-init"); - uint128 cur = pip.read(); - uint256 prev = block.timestamp / 1 days - days_ - 1; // day before the first initiated day - uint256 day; - for(uint256 i = 1; i <= days_ + 1;) { - unchecked { day = prev + i; } - accumulators[day] = cur * i * 1 days; - unchecked { ++i; } + require(cap == 0, "StickyOracle/already-init"); + uint128 cur = cap = pip.read(); + + uint256 currentDay = block.timestamp / 1 days; + uint256 firstDay = currentDay - days_; + + uint256 accumulatedVal = 0; + uint32 accumulatedTs = uint32(block.timestamp - days_ * 1 days); + + for (uint256 day = firstDay; day <= currentDay;) { + accumulators[day].val = accumulatedVal; + accumulators[day].ts = accumulatedTs; + + accumulatedVal += cur * 1 days; + accumulatedTs += 1 days; + unchecked { ++day; } } - val = cur; - age = uint32(block.timestamp); - } - function fix(uint256 day) external { - uint256 today = block.timestamp / 1 days; - require(day < today, "StickyOracle/too-soon"); - require(accumulators[day] == 0, "StickyOracle/nothing-to-fix"); - - uint256 acc1; uint256 acc2; - uint i; uint j; - for(i = 1; (acc1 = accumulators[day - i]) == 0; ++i) {} - for(j = i + 1; (acc2 = accumulators[day - j]) == 0; ++j) {} - - accumulators[day] = acc1 + (acc1 - acc2) * i / (j - i); + accLast = accumulators[currentDay]; + + emit Init(days_, cur); } function poke() external { - uint128 cur = _min(pip.read(), _getCap()); uint256 today = block.timestamp / 1 days; - uint256 acc = accumulators[today]; - (uint128 val_, uint32 age_) = (val, age); - uint256 newAcc; - uint256 tmrTs = (today + 1) * 1 days; // timestamp on the first second of tomorrow - if (acc == 0) { // first poke of the day - uint256 prevDay = age_ / 1 days; - uint256 bef = val_ * (block.timestamp - (prevDay + 1) * 1 days); // contribution to the accumulator from the previous value - uint256 aft = cur * (tmrTs - block.timestamp); // contribution to the accumulator from the current value, optimistically assuming this will be the last poke of the day - newAcc = accumulators[prevDay] + bef + aft; - } else { // not the first poke of the day - uint256 off = tmrTs - block.timestamp; // period during which the accumulator value needs to be adjusted - newAcc = acc + cur * off - val_ * off; - } - accumulators[today] = newAcc; - val = cur; - age = uint32(block.timestamp); + Accumulator memory accToday = accumulators[today]; + require(accToday.val == 0, "StickyOracle/already-poked-today"); + + // calculate new cap if possible, otherwise use the current one + uint128 cap_ = _calcCap(); + if (cap_ > 0) cap = cap_; + else cap_ = cap; + + // update accumulator + uint128 cur = _min(pip.read(), cap_); + + accToday.val = accLast.val + cur * (block.timestamp - accLast.ts); + accToday.ts = uint32(block.timestamp); + accumulators[today] = accLast = accToday; + + emit Poke(today, cap); } function read() external view toll returns (uint128) { - return _min(pip.read(), _getCap()); + uint128 cap_ = cap; + require(cap_ > 0, "StickyOracle/cap-not-set"); // TODO: decide if we need the cap_ require + return _min(pip.read(), cap); } function peek() external view toll returns (uint128, bool) { + uint128 cap_ = cap; (uint128 cur,) = pip.peek(); - return (_min(cur, _getCap()), cur > 0); + return (_min(cur, cap_), cur > 0 && cap_ > 0); // TODO: decide if we need the cap_ condition } } diff --git a/test/StickyOracle.t.sol b/test/StickyOracle.t.sol index d46ec86c..5db1baeb 100644 --- a/test/StickyOracle.t.sol +++ b/test/StickyOracle.t.sol @@ -31,14 +31,29 @@ interface PipLike { contract StickyOracleHarness is StickyOracle { constructor(address _pip) StickyOracle (_pip) {} - function getAccumulator(uint256 day) external view returns (uint256) { - return accumulators[day]; + + function getAccumulatorVal(uint256 day) external view returns (uint256) { + return accumulators[day].val; + } + + function getAccumulatorTs(uint256 day) external view returns (uint32) { + return accumulators[day].ts; } - function getVal() external view returns (uint128) { - return val; + + function getAccLastVal() external view returns (uint256) { + return accLast.val; + } + + function getAccLastTs() external view returns (uint32) { + return accLast.ts; } + function getCap() external view returns (uint128) { - return _getCap(); + return cap; + } + + function calcCap() external view returns (uint128) { + return _calcCap(); } } @@ -54,6 +69,8 @@ contract StickyOracleTest is Test { address PAUSE_PROXY; address PIP_MKR; + event Init(uint256 days_, uint128 cur); + function setMedianizerPrice(uint256 newPrice) internal { vm.store(address(medianizer), bytes32(uint256(1)), bytes32(block.timestamp << 128 | newPrice)); } @@ -85,75 +102,72 @@ contract StickyOracleTest is Test { } function testInit() public { - vm.expectRevert("StickyOracle/not-init"); - oracle.read(); + assertEq(oracle.read(), 0); + vm.expectEmit(true, true, true, true); + emit Init(3, uint128(initialMedianizerPrice)); vm.prank(PAUSE_PROXY); oracle.init(3); + assertEq(oracle.read(), medianizer.read()); - assertEq(oracle.getVal(), medianizer.read()); - assertEq(oracle.age(), block.timestamp); - assertEq(oracle.getAccumulator(block.timestamp / 1 days - 4), 0); - assertEq(oracle.getAccumulator(block.timestamp / 1 days - 3), initialMedianizerPrice * 1 days); - assertEq(oracle.getAccumulator(block.timestamp / 1 days - 2), initialMedianizerPrice * 2 days); - assertEq(oracle.getAccumulator(block.timestamp / 1 days - 1), initialMedianizerPrice * 3 days); - assertEq(oracle.getAccumulator(block.timestamp / 1 days ), initialMedianizerPrice * 4 days); + assertEq(oracle.getCap(), medianizer.read()); + + assertEq(oracle.getAccumulatorVal(block.timestamp / 1 days - 3), 0); + assertEq(oracle.getAccumulatorVal(block.timestamp / 1 days - 2), initialMedianizerPrice * 1 days); + assertEq(oracle.getAccumulatorVal(block.timestamp / 1 days - 1), initialMedianizerPrice * 2 days); + assertEq(oracle.getAccumulatorVal(block.timestamp / 1 days ), initialMedianizerPrice * 3 days); + assertEq(oracle.getAccLastVal(), initialMedianizerPrice * 3 days); + + assertEq(oracle.getAccumulatorTs(block.timestamp / 1 days - 3), block.timestamp - 3 days); + assertEq(oracle.getAccumulatorTs(block.timestamp / 1 days - 2), block.timestamp - 2 days); + assertEq(oracle.getAccumulatorTs(block.timestamp / 1 days - 1), block.timestamp - 1 days); + assertEq(oracle.getAccumulatorTs(block.timestamp / 1 days ), block.timestamp); + assertEq(oracle.getAccLastTs(), block.timestamp); } - function testFix() external { + function testPoke() public { vm.prank(PAUSE_PROXY); oracle.init(3); assertEq(oracle.read(), medianizer.read()); - vm.expectRevert("StickyOracle/nothing-to-fix"); - oracle.fix(block.timestamp / 1 days - 1); + setMedianizerPrice(initialMedianizerPrice * 110 / 100); + + vm.expectRevert("StickyOracle/already-poked-today"); + oracle.poke(); vm.warp(block.timestamp + 1 days); + oracle.poke(); // before: [-,100,100] + assertEq(oracle.getCap(), initialMedianizerPrice * 105 / 100); // (100+100)/2 * 1.05 = 105 + assertEq(oracle.read(), initialMedianizerPrice * 105 / 100); - vm.expectRevert("StickyOracle/too-soon"); - oracle.fix(block.timestamp / 1 days); + vm.warp(block.timestamp + 1 days); + oracle.poke(); // before: // [-,100,105] + assertEq(oracle.getCap(), initialMedianizerPrice * 107625 / 100000); // (100+105)/2 * 1.05 = 107.625 + assertEq(oracle.read(), initialMedianizerPrice * 107625 / 100000); vm.warp(block.timestamp + 1 days); - assertEq(oracle.getAccumulator(block.timestamp / 1 days - 1), 0); + oracle.poke(); // before: [-,105,107.625] + assertEq(oracle.getCap(), initialMedianizerPrice * 111628125 / 100000000); // (105+107.625)/2 * 1.05 = 111.628125 + assertEq(oracle.read(), initialMedianizerPrice * 110 / 100); // blocked by current price of 110 - oracle.fix(block.timestamp / 1 days - 1); + vm.warp(block.timestamp + 2 days); // missing poke for 1 day + oracle.poke(); // before: [-,110,Miss] + assertEq(oracle.getCap(), initialMedianizerPrice * 111628125 / 100000000); // cannot calc twap, cap will stay the same + assertEq(oracle.read(), initialMedianizerPrice * 110 / 100); // still blocked by current price of 110 - uint256 acc1 = oracle.getAccumulator(block.timestamp / 1 days - 2); - uint256 acc2 = oracle.getAccumulator(block.timestamp / 1 days - 3); - assertGt(oracle.getAccumulator(block.timestamp / 1 days - 1), 0); - assertEq(oracle.getAccumulator(block.timestamp / 1 days - 1), acc1 + (acc1 - acc2)); - } + setMedianizerPrice(initialMedianizerPrice * 111 / 100); // price goes up a bit - function testPoke() public { - vm.prank(PAUSE_PROXY); oracle.init(3); - assertEq(oracle.read(), medianizer.read()); + vm.warp(block.timestamp + 1 days); + oracle.poke(); // before: [-,Miss,110] + assertEq(oracle.getCap(), initialMedianizerPrice * 1155 / 1000); // (110*2)/2 * 1.05 = 115.5 + assertEq(oracle.read(), initialMedianizerPrice * 111 / 100); // blocked by current price of 111 - uint256 medianizerPrice1 = initialMedianizerPrice * 110 / 100; - setMedianizerPrice(medianizerPrice1); - vm.warp((block.timestamp / 1 days) * 1 days + 1 days + 8 hours); // warping to 8am on the next day - uint256 prevVal = oracle.getVal(); - - oracle.poke(); // first poke of the day - - uint256 oraclePrice1 = 105 * initialMedianizerPrice / 100; - assertEq(oracle.getCap(), oraclePrice1); - assertEq(oracle.getVal(), oraclePrice1); - assertEq(oracle.age(), block.timestamp); - assertEq(oracle.read(), oraclePrice1); - uint256 bef = prevVal * 8 hours; - uint256 aft = oraclePrice1 * 16 hours; - assertEq(oracle.getAccumulator(block.timestamp / 1 days), oracle.getAccumulator(block.timestamp / 1 days - 1) + bef + aft); - - uint256 prevAcc = oracle.getAccumulator(block.timestamp / 1 days); - vm.warp(block.timestamp + 8 hours); // warping to 4pm on the same day - uint256 medianizerPrice2 = initialMedianizerPrice * 104 / 100; - setMedianizerPrice(medianizerPrice2); - - oracle.poke(); // second poke of the day - - uint256 oraclePrice2 = 104 * initialMedianizerPrice / 100; - assertEq(oracle.getCap(), 105 * initialMedianizerPrice / 100); - assertEq(oracle.getVal(), oraclePrice2); - assertEq(oracle.age(), block.timestamp); - assertEq(oracle.read(), oraclePrice2); - assertEq(oracle.getAccumulator(block.timestamp / 1 days), prevAcc + 8 hours * oraclePrice2 - 8 hours * oraclePrice1); + vm.warp(block.timestamp + 1 days); + oracle.poke(); // before: [Miss,110,111]; + assertEq(oracle.getCap(), initialMedianizerPrice * 1155 / 1000); // cannot calc twap, cap will stay the same + assertEq(oracle.read(), initialMedianizerPrice * 111 / 100); // still blocked by current price of 111 + + vm.warp(block.timestamp + 1 days); + oracle.poke(); // before: [-,111,111]; + assertEq(oracle.getCap(), initialMedianizerPrice * 11655 / 10000); // (111 + 111)/2 * 1.05 = 116.55 + assertEq(oracle.read(), initialMedianizerPrice * 111 / 100); // still blocked by current price of 111 } } From 18e0bdaac3b6a2ed053a9d9cda1926f4928cb1b9 Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:24:16 +0200 Subject: [PATCH 02/10] Fix TWAP calc to use last price for finished timespan --- src/StickyOracle.sol | 28 +++++++++++----------- test/StickyOracle.t.sol | 52 ++++++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index dbce85ee..76b82ea6 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -26,8 +26,9 @@ contract StickyOracle { mapping (address => uint256) public buds; // whitelisted feed readers mapping (uint256 => Accumulator ) accumulators; // daily sticky oracle price accumulators - Accumulator accLast; // last set accumulator uint128 cap; // max allowed price + uint128 pokePrice; // last price at which poke() was called + uint256 pokeDay; // last day at which poke() was called uint96 public slope = uint96(RAY); // maximum allowable price growth factor from center of TWAP window to now (in RAY such that slope = (1 + {max growth rate}) * RAY) uint8 public lo; // how many days ago should the TWAP window start (exclusive), should be more than hi @@ -46,7 +47,7 @@ contract StickyOracle { event Diss(address indexed usr); event File(bytes32 indexed what, uint256 data); event Init(uint256 days_, uint128 cur); - event Poke(uint256 indexed day, uint128 cap); + event Poke(uint256 indexed day, uint128 cap, uint128 pokePrice); constructor(address _pip) { pip = PipLike(_pip); @@ -103,15 +104,15 @@ contract StickyOracle { // if the initiated timespan is shorter than the [lo, hi] window the initial cap will just be used for longer function init(uint256 days_) external auth { require(cap == 0, "StickyOracle/already-init"); - uint128 cur = cap = pip.read(); + uint128 cur = cap = pokePrice = pip.read(); - uint256 currentDay = block.timestamp / 1 days; - uint256 firstDay = currentDay - days_; + uint256 pokeDay_ = pokeDay = block.timestamp / 1 days; + uint256 firstDay = pokeDay_ - days_; uint256 accumulatedVal = 0; uint32 accumulatedTs = uint32(block.timestamp - days_ * 1 days); - for (uint256 day = firstDay; day <= currentDay;) { + for (uint256 day = firstDay; day <= pokeDay_;) { accumulators[day].val = accumulatedVal; accumulators[day].ts = accumulatedTs; @@ -120,15 +121,12 @@ contract StickyOracle { unchecked { ++day; } } - accLast = accumulators[currentDay]; - emit Init(days_, cur); } function poke() external { uint256 today = block.timestamp / 1 days; - Accumulator memory accToday = accumulators[today]; - require(accToday.val == 0, "StickyOracle/already-poked-today"); + require(accumulators[today].val == 0, "StickyOracle/already-poked-today"); // calculate new cap if possible, otherwise use the current one uint128 cap_ = _calcCap(); @@ -136,13 +134,13 @@ contract StickyOracle { else cap_ = cap; // update accumulator - uint128 cur = _min(pip.read(), cap_); + accumulators[today].val = accumulators[pokeDay].val + pokePrice * (block.timestamp - accumulators[pokeDay].ts); + accumulators[today].ts = uint32(block.timestamp); - accToday.val = accLast.val + cur * (block.timestamp - accLast.ts); - accToday.ts = uint32(block.timestamp); - accumulators[today] = accLast = accToday; + uint128 pokePrice_ = pokePrice = _min(pip.read(), cap_); + pokeDay = today; - emit Poke(today, cap); + emit Poke(today, cap, pokePrice_); } function read() external view toll returns (uint128) { diff --git a/test/StickyOracle.t.sol b/test/StickyOracle.t.sol index 5db1baeb..514ffa16 100644 --- a/test/StickyOracle.t.sol +++ b/test/StickyOracle.t.sol @@ -40,12 +40,12 @@ contract StickyOracleHarness is StickyOracle { return accumulators[day].ts; } - function getAccLastVal() external view returns (uint256) { - return accLast.val; + function getPokePrice() external view returns (uint256) { + return pokePrice; } - function getAccLastTs() external view returns (uint32) { - return accLast.ts; + function getPokeDay() external view returns (uint256) { + return pokeDay; } function getCap() external view returns (uint128) { @@ -70,6 +70,7 @@ contract StickyOracleTest is Test { address PIP_MKR; event Init(uint256 days_, uint128 cur); + event Poke(uint256 indexed day, uint128 cap, uint128 pokePrice); function setMedianizerPrice(uint256 newPrice) internal { vm.store(address(medianizer), bytes32(uint256(1)), bytes32(block.timestamp << 128 | newPrice)); @@ -102,7 +103,8 @@ contract StickyOracleTest is Test { } function testInit() public { - assertEq(oracle.read(), 0); + vm.expectRevert("StickyOracle/cap-not-set"); + oracle.read(); vm.expectEmit(true, true, true, true); emit Init(3, uint128(initialMedianizerPrice)); @@ -115,13 +117,14 @@ contract StickyOracleTest is Test { assertEq(oracle.getAccumulatorVal(block.timestamp / 1 days - 2), initialMedianizerPrice * 1 days); assertEq(oracle.getAccumulatorVal(block.timestamp / 1 days - 1), initialMedianizerPrice * 2 days); assertEq(oracle.getAccumulatorVal(block.timestamp / 1 days ), initialMedianizerPrice * 3 days); - assertEq(oracle.getAccLastVal(), initialMedianizerPrice * 3 days); assertEq(oracle.getAccumulatorTs(block.timestamp / 1 days - 3), block.timestamp - 3 days); assertEq(oracle.getAccumulatorTs(block.timestamp / 1 days - 2), block.timestamp - 2 days); assertEq(oracle.getAccumulatorTs(block.timestamp / 1 days - 1), block.timestamp - 1 days); assertEq(oracle.getAccumulatorTs(block.timestamp / 1 days ), block.timestamp); - assertEq(oracle.getAccLastTs(), block.timestamp); + + assertEq(oracle.getPokePrice(), initialMedianizerPrice); + assertEq(oracle.getPokeDay(), block.timestamp / 1 days); } function testPoke() public { @@ -134,30 +137,37 @@ contract StickyOracleTest is Test { oracle.poke(); vm.warp(block.timestamp + 1 days); - oracle.poke(); // before: [-,100,100] - assertEq(oracle.getCap(), initialMedianizerPrice * 105 / 100); // (100+100)/2 * 1.05 = 105 + vm.expectEmit(true, true, true, true); + emit Poke(block.timestamp / 1 days, uint128(initialMedianizerPrice * 105 / 100), uint128(initialMedianizerPrice * 105 / 100)); + oracle.poke(); // before: [100,100,100] + assertEq(oracle.getCap(), initialMedianizerPrice * 105 / 100); // (100 + 100) / 2 * 1.05 = 105 + assertEq(oracle.read(), initialMedianizerPrice * 105 / 100); + + vm.warp(block.timestamp + 1 days); + oracle.poke(); // before: // [100,100,105] + assertEq(oracle.getCap(), initialMedianizerPrice * 105 / 100 ); // (100 + 100) / 2 * 1.05 = 105 assertEq(oracle.read(), initialMedianizerPrice * 105 / 100); vm.warp(block.timestamp + 1 days); - oracle.poke(); // before: // [-,100,105] - assertEq(oracle.getCap(), initialMedianizerPrice * 107625 / 100000); // (100+105)/2 * 1.05 = 107.625 + oracle.poke(); // before: [100,105,105] + assertEq(oracle.getCap(), initialMedianizerPrice * 107625 / 100000); // (100 + 105) /2 * 1.05 = 107.625 assertEq(oracle.read(), initialMedianizerPrice * 107625 / 100000); vm.warp(block.timestamp + 1 days); - oracle.poke(); // before: [-,105,107.625] - assertEq(oracle.getCap(), initialMedianizerPrice * 111628125 / 100000000); // (105+107.625)/2 * 1.05 = 111.628125 - assertEq(oracle.read(), initialMedianizerPrice * 110 / 100); // blocked by current price of 110 + oracle.poke(); // before: [105,105,107.625] + assertEq(oracle.getCap(), initialMedianizerPrice * 11025 / 10000); // (105 + 105) / 2 * 1.05 = 110.25 + assertEq(oracle.read(), initialMedianizerPrice * 110 / 100); // blocked by current price of 110 - vm.warp(block.timestamp + 2 days); // missing poke for 1 day - oracle.poke(); // before: [-,110,Miss] - assertEq(oracle.getCap(), initialMedianizerPrice * 111628125 / 100000000); // cannot calc twap, cap will stay the same + vm.warp(block.timestamp + 2 days); // missing a poke + oracle.poke(); // before: [107.625,110,Miss] + assertEq(oracle.getCap(), initialMedianizerPrice * 11025 / 10000); // cannot calc twap, cap will stay the same assertEq(oracle.read(), initialMedianizerPrice * 110 / 100); // still blocked by current price of 110 setMedianizerPrice(initialMedianizerPrice * 111 / 100); // price goes up a bit vm.warp(block.timestamp + 1 days); - oracle.poke(); // before: [-,Miss,110] - assertEq(oracle.getCap(), initialMedianizerPrice * 1155 / 1000); // (110*2)/2 * 1.05 = 115.5 + oracle.poke(); // before: [110,Miss,110] + assertEq(oracle.getCap(), initialMedianizerPrice * 1155 / 1000); // (110 * 2) / 2 * 1.05 = 115.5 assertEq(oracle.read(), initialMedianizerPrice * 111 / 100); // blocked by current price of 111 vm.warp(block.timestamp + 1 days); @@ -166,8 +176,8 @@ contract StickyOracleTest is Test { assertEq(oracle.read(), initialMedianizerPrice * 111 / 100); // still blocked by current price of 111 vm.warp(block.timestamp + 1 days); - oracle.poke(); // before: [-,111,111]; - assertEq(oracle.getCap(), initialMedianizerPrice * 11655 / 10000); // (111 + 111)/2 * 1.05 = 116.55 + oracle.poke(); // before: [110,111,111]; + assertEq(oracle.getCap(), initialMedianizerPrice * 116025 / 100000); // (110 + 111)/2 * 1.05 = 116.025 assertEq(oracle.read(), initialMedianizerPrice * 111 / 100); // still blocked by current price of 111 } } From 3793b3e83f2cba1bce95160dba1becb440178e12 Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:40:44 +0200 Subject: [PATCH 03/10] Cosmetic changes --- lib/dss-test | 2 +- src/StickyOracle.sol | 15 +++++++-------- test/StickyOracle.t.sol | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/dss-test b/lib/dss-test index df7b13ea..28000a0e 160000 --- a/lib/dss-test +++ b/lib/dss-test @@ -1 +1 @@ -Subproject commit df7b13ead253f4b831df2464f7d74f28a091a790 +Subproject commit 28000a0ea3602c887f2fdc22dea591789d1b4971 diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index 76b82ea6..b182ba27 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -46,8 +46,8 @@ contract StickyOracle { event Kiss(address indexed usr); event Diss(address indexed usr); event File(bytes32 indexed what, uint256 data); - event Init(uint256 days_, uint128 cur); - event Poke(uint256 indexed day, uint128 cap, uint128 pokePrice); + event Init(uint256 days_, uint128 pokePrice_); + event Poke(uint256 indexed day, uint128 cap, uint128 pokePrice_); constructor(address _pip) { pip = PipLike(_pip); @@ -104,24 +104,22 @@ contract StickyOracle { // if the initiated timespan is shorter than the [lo, hi] window the initial cap will just be used for longer function init(uint256 days_) external auth { require(cap == 0, "StickyOracle/already-init"); - uint128 cur = cap = pokePrice = pip.read(); + uint128 pokePrice_ = pokePrice = cap = pip.read(); uint256 pokeDay_ = pokeDay = block.timestamp / 1 days; - uint256 firstDay = pokeDay_ - days_; - uint256 accumulatedVal = 0; uint32 accumulatedTs = uint32(block.timestamp - days_ * 1 days); - for (uint256 day = firstDay; day <= pokeDay_;) { + for (uint256 day = pokeDay_ - days_; day <= pokeDay_;) { accumulators[day].val = accumulatedVal; accumulators[day].ts = accumulatedTs; - accumulatedVal += cur * 1 days; + accumulatedVal += pokePrice_ * 1 days; accumulatedTs += 1 days; unchecked { ++day; } } - emit Init(days_, cur); + emit Init(days_, pokePrice_); } function poke() external { @@ -137,6 +135,7 @@ contract StickyOracle { accumulators[today].val = accumulators[pokeDay].val + pokePrice * (block.timestamp - accumulators[pokeDay].ts); accumulators[today].ts = uint32(block.timestamp); + // store for next accumulator calc uint128 pokePrice_ = pokePrice = _min(pip.read(), cap_); pokeDay = today; diff --git a/test/StickyOracle.t.sol b/test/StickyOracle.t.sol index 514ffa16..42d89b3e 100644 --- a/test/StickyOracle.t.sol +++ b/test/StickyOracle.t.sol @@ -69,8 +69,8 @@ contract StickyOracleTest is Test { address PAUSE_PROXY; address PIP_MKR; - event Init(uint256 days_, uint128 cur); - event Poke(uint256 indexed day, uint128 cap, uint128 pokePrice); + event Init(uint256 days_, uint128 pokePrice_); + event Poke(uint256 indexed day, uint128 cap, uint128 pokePrice_); function setMedianizerPrice(uint256 newPrice) internal { vm.store(address(medianizer), bytes32(uint256(1)), bytes32(block.timestamp << 128 | newPrice)); From 76a377e08ae5bf07ca8b0cbb413087e5e328000f Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:43:32 +0200 Subject: [PATCH 04/10] X => N --- src/StickyOracle.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index b182ba27..ff5c2018 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -99,7 +99,7 @@ contract StickyOracle { } // days_ is the number of daily samples to initialize on top of the current one - // days_ == X will fill up a window corresponding to [lo == X, hi == 1] along with the current day + // days_ == N will fill up a window corresponding to [lo == N, hi == 1] along with the current day // days_ should be selected carefully as too many iterations can cause the transaction to run out of gas // if the initiated timespan is shorter than the [lo, hi] window the initial cap will just be used for longer function init(uint256 days_) external auth { From 2c5f9b31bd042a98f2982c2ae94ca25063230d6c Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Wed, 6 Dec 2023 08:25:59 +0200 Subject: [PATCH 05/10] Add some TODOs --- src/StickyOracle.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index ff5c2018..89684218 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -17,9 +17,10 @@ pragma solidity ^0.8.16; interface PipLike { - function read() external view returns (uint128); - function peek() external view returns (uint128, bool); + function read() external view returns (uint128); // TODO: shouldn't this (and our function) return bytes32? https://github.com/makerdao/osm/blob/e36c874b4e14fba860e48c0cf99cd600c0c59efa/src/osm.sol#L150C49-L150C56 + function peek() external view returns (uint128, bool); // TODO: shouldn't this (and our function) return (bytes32, bool)? https://github.com/makerdao/osm/blob/e36c874b4e14fba860e48c0cf99cd600c0c59efa/src/osm.sol#L142 } +// TODO: should we implement peep as well? (even if a dummy implementation) Scribe does - https://github.com/chronicleprotocol/scribe/blob/41f25a8a40f1a1d2ef62d6a073f98a3c57d23579/src/Scribe.sol#L276. contract StickyOracle { mapping (address => uint256) public wards; @@ -105,7 +106,7 @@ contract StickyOracle { function init(uint256 days_) external auth { require(cap == 0, "StickyOracle/already-init"); - uint128 pokePrice_ = pokePrice = cap = pip.read(); + uint128 pokePrice_ = pokePrice = cap = pip.read(); // TODO: should this use peek() and return true/false instead of reverting? it will be called from a spell so we don't want it to revert uint256 pokeDay_ = pokeDay = block.timestamp / 1 days; uint256 accumulatedVal = 0; uint32 accumulatedTs = uint32(block.timestamp - days_ * 1 days); From 765a0d7c467bc5f0a784724bc6ec8e9ba69dcc6f Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Wed, 6 Dec 2023 10:34:36 +0200 Subject: [PATCH 06/10] Add another TODO --- src/StickyOracle.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index 89684218..55a6ce5f 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -143,6 +143,8 @@ contract StickyOracle { emit Poke(today, cap, pokePrice_); } + // TODO: should we add stop functionality? the stop can set the cap to 0 and then we need to make sure poke() doesn't ovreride it + function read() external view toll returns (uint128) { uint128 cap_ = cap; require(cap_ > 0, "StickyOracle/cap-not-set"); // TODO: decide if we need the cap_ require From 80f401000820b497b8b13273390e16e0d1d81e34 Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:15:15 +0200 Subject: [PATCH 07/10] Add void() --- src/StickyOracle.sol | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index 55a6ce5f..169d315f 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -48,6 +48,7 @@ contract StickyOracle { event Diss(address indexed usr); event File(bytes32 indexed what, uint256 data); event Init(uint256 days_, uint128 pokePrice_); + event Void(); event Poke(uint256 indexed day, uint128 cap, uint128 pokePrice_); constructor(address _pip) { @@ -123,14 +124,20 @@ contract StickyOracle { emit Init(days_, pokePrice_); } + function void() external auth { + cap = 0; + emit Void(); + } + function poke() external { uint256 today = block.timestamp / 1 days; require(accumulators[today].val == 0, "StickyOracle/already-poked-today"); // calculate new cap if possible, otherwise use the current one - uint128 cap_ = _calcCap(); - if (cap_ > 0) cap = cap_; - else cap_ = cap; + uint128 cap_ = cap; + require(cap_ > 0, "StickyOracle/cap-not-set"); + uint128 newCap = _calcCap(); + if (newCap > 0) cap = cap_ = newCap; // update accumulator accumulators[today].val = accumulators[pokeDay].val + pokePrice * (block.timestamp - accumulators[pokeDay].ts); @@ -143,17 +150,15 @@ contract StickyOracle { emit Poke(today, cap, pokePrice_); } - // TODO: should we add stop functionality? the stop can set the cap to 0 and then we need to make sure poke() doesn't ovreride it - function read() external view toll returns (uint128) { uint128 cap_ = cap; - require(cap_ > 0, "StickyOracle/cap-not-set"); // TODO: decide if we need the cap_ require + require(cap_ > 0, "StickyOracle/cap-not-set"); return _min(pip.read(), cap); } function peek() external view toll returns (uint128, bool) { uint128 cap_ = cap; (uint128 cur,) = pip.peek(); - return (_min(cur, cap_), cur > 0 && cap_ > 0); // TODO: decide if we need the cap_ condition + return (_min(cur, cap_), cur > 0 && cap_ > 0); } } From d10f0de89c232bb78417c8a5fd79182cf861cbd5 Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:35:51 +0200 Subject: [PATCH 08/10] Return false on init if no price --- src/StickyOracle.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index 169d315f..a58bf40b 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -104,10 +104,13 @@ contract StickyOracle { // days_ == N will fill up a window corresponding to [lo == N, hi == 1] along with the current day // days_ should be selected carefully as too many iterations can cause the transaction to run out of gas // if the initiated timespan is shorter than the [lo, hi] window the initial cap will just be used for longer - function init(uint256 days_) external auth { + function init(uint256 days_) external auth returns(bool) { require(cap == 0, "StickyOracle/already-init"); - uint128 pokePrice_ = pokePrice = cap = pip.read(); // TODO: should this use peek() and return true/false instead of reverting? it will be called from a spell so we don't want it to revert + (uint128 pokePrice_, bool has) = pip.peek(); // non-reverting to support calling from a spell + if (!has) return false; + + pokePrice = cap = pokePrice_; uint256 pokeDay_ = pokeDay = block.timestamp / 1 days; uint256 accumulatedVal = 0; uint32 accumulatedTs = uint32(block.timestamp - days_ * 1 days); @@ -122,6 +125,7 @@ contract StickyOracle { } emit Init(days_, pokePrice_); + return true; } function void() external auth { From 1ed6d987b3ed4bdfa378f3f35f18a01a064a2a43 Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:41:58 +0200 Subject: [PATCH 09/10] peek() to check has explicitly --- src/StickyOracle.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index a58bf40b..2e84b5fc 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -162,7 +162,7 @@ contract StickyOracle { function peek() external view toll returns (uint128, bool) { uint128 cap_ = cap; - (uint128 cur,) = pip.peek(); - return (_min(cur, cap_), cur > 0 && cap_ > 0); + (uint128 cur, bool has) = pip.peek(); + return (_min(cur, cap_), has && cap_ > 0); } } From 5ea56a5a6e2fa89d4fcb48fbaf053b37f4460343 Mon Sep 17 00:00:00 2001 From: oldchili <130549691+oldchili@users.noreply.github.com> Date: Sun, 4 Feb 2024 12:02:04 +0200 Subject: [PATCH 10/10] Apply suggestions from code review Co-authored-by: sunbreak1211 <129470872+sunbreak1211@users.noreply.github.com> --- src/StickyOracle.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StickyOracle.sol b/src/StickyOracle.sol index 2e84b5fc..4ef3faf3 100644 --- a/src/StickyOracle.sol +++ b/src/StickyOracle.sol @@ -26,12 +26,12 @@ contract StickyOracle { mapping (address => uint256) public wards; mapping (address => uint256) public buds; // whitelisted feed readers - mapping (uint256 => Accumulator ) accumulators; // daily sticky oracle price accumulators + mapping (uint256 => Accumulator) accumulators; // daily sticky oracle price accumulators uint128 cap; // max allowed price uint128 pokePrice; // last price at which poke() was called uint256 pokeDay; // last day at which poke() was called - uint96 public slope = uint96(RAY); // maximum allowable price growth factor from center of TWAP window to now (in RAY such that slope = (1 + {max growth rate}) * RAY) + uint96 public slope = uint96(RAY); // maximum allowable price growth factor from the average value of a TWAP window (in RAY such that slope = (1 + {max growth rate}) * RAY) uint8 public lo; // how many days ago should the TWAP window start (exclusive), should be more than hi uint8 public hi; // how many days ago should the TWAP window end (inclusive), should be less than lo and more than 0 @@ -157,7 +157,7 @@ contract StickyOracle { function read() external view toll returns (uint128) { uint128 cap_ = cap; require(cap_ > 0, "StickyOracle/cap-not-set"); - return _min(pip.read(), cap); + return _min(pip.read(), cap_); } function peek() external view toll returns (uint128, bool) {