Skip to content

Commit

Permalink
Fix TWAP calc to use last price for finished timespan
Browse files Browse the repository at this point in the history
  • Loading branch information
oldchili committed Dec 5, 2023
1 parent 5d88731 commit 18e0bda
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 36 deletions.
28 changes: 13 additions & 15 deletions src/StickyOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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;

Expand All @@ -120,29 +121,26 @@ 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();
if (cap_ > 0) cap = cap_;
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) {
Expand Down
52 changes: 31 additions & 21 deletions test/StickyOracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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));
Expand Down Expand Up @@ -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));
Expand All @@ -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 {
Expand All @@ -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);
Expand All @@ -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
}
}

0 comments on commit 18e0bda

Please sign in to comment.