Skip to content

Commit

Permalink
poke once a day, use struct, more changes
Browse files Browse the repository at this point in the history
  • Loading branch information
oldchili committed Dec 5, 2023
1 parent dea1598 commit 5d88731
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 116 deletions.
122 changes: 64 additions & 58 deletions src/StickyOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
}
}
130 changes: 72 additions & 58 deletions test/StickyOracle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Expand All @@ -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));
}
Expand Down Expand Up @@ -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
}
}

0 comments on commit 5d88731

Please sign in to comment.