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] 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 } }