-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
poke once a day, use struct, more changes #4
base: sticky
Are you sure you want to change the base?
Changes from all commits
5d88731
18e0bda
3793b3e
76a377e
2c5f9b3
765a0d7
80f4010
d10f0de
1ed6d98
aab30ce
7e6fb31
3765ee5
5ea56a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -17,29 +17,39 @@ | |||||
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; | ||||||
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 | ||||||
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) | ||||||
uint8 public hi; // how many days ago should the TWAP window end (inclusive) | ||||||
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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I was trying to say here is that, given that the TWAP is an unbiased smoothed estimate of the price at the time in the middle of the TWAP window, the
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh ok got it. Although I think it is easier to describe it as the multiplier of the TWAP. |
||||||
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; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure tracking the exact poke ts is worth the added poke cost (vs just using the eod ts). Generally if we expect the window to be much larger than 1 day (e.g. 30 days) the gain in precision is likely to be negligible (I did some quick approximation math to convince myself of it but might be good to test with actual values). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well the poke is only done once a day, so even if it's a matter of a few storage operations (probably less) I think it's negligible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, though note that in the Uniswap example the timestamp is necessary as the accumulators are not indexed by days. That said I don't mind storing the ts here given the frequency of |
||||||
} | ||||||
|
||||||
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 pokePrice_); | ||||||
event Void(); | ||||||
event Poke(uint256 indexed day, uint128 cap, uint128 pokePrice_); | ||||||
|
||||||
constructor(address _pip) { | ||||||
pip = PipLike(_pip); | ||||||
|
@@ -77,77 +87,82 @@ 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; | ||||||
} | ||||||
|
||||||
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; } | ||||||
// days_ is the number of daily samples to initialize on top of the current one | ||||||
// 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 returns(bool) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
require(cap == 0, "StickyOracle/already-init"); | ||||||
|
||||||
(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); | ||||||
|
||||||
for (uint256 day = pokeDay_ - days_; day <= pokeDay_;) { | ||||||
accumulators[day].val = accumulatedVal; | ||||||
accumulators[day].ts = accumulatedTs; | ||||||
|
||||||
accumulatedVal += pokePrice_ * 1 days; | ||||||
accumulatedTs += 1 days; | ||||||
unchecked { ++day; } | ||||||
} | ||||||
val = cur; | ||||||
age = uint32(block.timestamp); | ||||||
|
||||||
emit Init(days_, pokePrice_); | ||||||
return true; | ||||||
} | ||||||
|
||||||
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); | ||||||
function void() external auth { | ||||||
cap = 0; | ||||||
emit Void(); | ||||||
} | ||||||
|
||||||
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); | ||||||
require(accumulators[today].val == 0, "StickyOracle/already-poked-today"); | ||||||
|
||||||
// calculate new cap if possible, otherwise use the current one | ||||||
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); | ||||||
accumulators[today].ts = uint32(block.timestamp); | ||||||
|
||||||
// store for next accumulator calc | ||||||
uint128 pokePrice_ = pokePrice = _min(pip.read(), cap_); | ||||||
pokeDay = today; | ||||||
|
||||||
emit Poke(today, cap, pokePrice_); | ||||||
} | ||||||
|
||||||
function read() external view toll returns (uint128) { | ||||||
return _min(pip.read(), _getCap()); | ||||||
uint128 cap_ = cap; | ||||||
require(cap_ > 0, "StickyOracle/cap-not-set"); | ||||||
return _min(pip.read(), cap_); | ||||||
} | ||||||
|
||||||
function peek() external view toll returns (uint128, bool) { | ||||||
(uint128 cur,) = pip.peek(); | ||||||
return (_min(cur, _getCap()), cur > 0); | ||||||
uint128 cap_ = cap; | ||||||
(uint128 cur, bool has) = pip.peek(); | ||||||
return (_min(cur, cap_), has && cap_ > 0); | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main reason for using a
uint128
type was to be able to packval
andage
in the same slot (and avoid an explicit cast for a value which under the hood is anyway auint128
in the pip/osm). But since these variables have been removed, feel free to use a bytes32 (like in the osm) or a uint256 (like in the pip).