-
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
Add draft StickyOracle #2
base: dev
Are you sure you want to change the base?
Changes from all commits
80b1a40
ceda453
d846316
52e2b36
97b45bb
caf6d74
a610d14
433633b
8ef19a3
cdcb79c
ff62b5e
3c1639b
9f2639b
8b53dd4
dea1598
b6a1f86
6ca3e18
dc220cd
2eb8033
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 |
---|---|---|
@@ -0,0 +1,153 @@ | ||
// SPDX-FileCopyrightText: © 2023 Dai Foundation <www.daifoundation.org> | ||
// SPDX-License-Identifier: AGPL-3.0-or-later | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Affero General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Affero General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Affero General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
pragma solidity ^0.8.16; | ||
|
||
interface PipLike { | ||
function read() external view returns (uint128); | ||
function peek() external view returns (uint128, bool); | ||
} | ||
|
||
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 | ||
|
||
PipLike public immutable pip; | ||
|
||
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) | ||
|
||
uint128 val; // last poked price | ||
uint32 public age; // time of last poke | ||
|
||
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); | ||
|
||
constructor(address _pip) { | ||
pip = PipLike(_pip); | ||
|
||
wards[msg.sender] = 1; | ||
emit Rely(msg.sender); | ||
} | ||
|
||
modifier auth { | ||
require(wards[msg.sender] == 1, "StickyOracle/not-authorized"); | ||
_; | ||
} | ||
|
||
modifier toll { | ||
require(buds[msg.sender] == 1, "StickyOracle/not-whitelisted"); | ||
_; | ||
} | ||
|
||
uint256 internal constant RAY = 10 ** 27; | ||
|
||
function rely(address usr) external auth { wards[usr] = 1; emit Rely(usr); } | ||
function deny(address usr) external auth { wards[usr] = 0; emit Deny(usr); } | ||
function kiss(address usr) external auth { buds[usr] = 1; emit Kiss(usr); } | ||
function diss(address usr) external auth { buds[usr] = 0; emit Diss(usr); } | ||
|
||
function file(bytes32 what, uint256 data) external auth { | ||
if (what == "slope") slope = uint96(data); | ||
else if (what == "lo") lo = uint8(data); | ||
else if (what == "hi") hi = uint8(data); | ||
else revert("StickyOracle/file-unrecognized-param"); | ||
emit File(what, data); | ||
} | ||
|
||
function _min(uint128 a, uint128 b) internal pure returns (uint128 min) { | ||
return a < b ? a : b; | ||
} | ||
|
||
function _getCap() 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_]; | ||
|
||
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 | ||
} | ||
|
||
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; } | ||
} | ||
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); | ||
} | ||
|
||
function poke() external { | ||
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. Wouldn't it be much simpler just to store the timestamp and value of the last first-of-the-day poke? I feel like we are trying to be too smart here with the I tried to sketch how it would look like (also with saving
The above assumes we have in storage |
||
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 | ||
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. is val_ necessarily the previous day's accumulator value? not sure I get this. |
||
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 | ||
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. mmm can we do without the else case? we probably don't want twap related logic on each regular poke. |
||
newAcc = acc + cur * off - val_ * off; | ||
} | ||
accumulators[today] = newAcc; | ||
val = cur; | ||
age = uint32(block.timestamp); | ||
} | ||
|
||
function read() external view toll returns (uint128) { | ||
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. To keep consistency with the 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 also think that 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. Yes 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. Maybe that is assuming too much from the called oracle but yeah not sure, will think about this a bit more. |
||
return _min(pip.read(), _getCap()); | ||
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.
|
||
} | ||
|
||
function peek() external view toll returns (uint128, bool) { | ||
(uint128 cur,) = pip.peek(); | ||
return (_min(cur, _getCap()), cur > 0); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
// SPDX-FileCopyrightText: © 2023 Dai Foundation <www.daifoundation.org> | ||
// SPDX-License-Identifier: AGPL-3.0-or-later | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Affero General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Affero General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Affero General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
pragma solidity ^0.8.16; | ||
|
||
import "forge-std/Test.sol"; | ||
|
||
import { StickyOracle } from "src/StickyOracle.sol"; | ||
|
||
interface ChainlogLike { | ||
function getAddress(bytes32) external view returns (address); | ||
} | ||
|
||
interface PipLike { | ||
function read() external view returns (uint256); | ||
function kiss(address) external; | ||
} | ||
|
||
contract StickyOracleHarness is StickyOracle { | ||
constructor(address _pip) StickyOracle (_pip) {} | ||
function getAccumulator(uint256 day) external view returns (uint256) { | ||
return accumulators[day]; | ||
} | ||
function getVal() external view returns (uint128) { | ||
return val; | ||
} | ||
function getCap() external view returns (uint128) { | ||
return _getCap(); | ||
} | ||
} | ||
|
||
contract StickyOracleTest is Test { | ||
|
||
PipLike public medianizer; | ||
StickyOracleHarness public oracle; | ||
uint256 public initialMedianizerPrice; | ||
|
||
uint256 constant RAY = 10 ** 27; | ||
address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F; | ||
|
||
address PAUSE_PROXY; | ||
address PIP_MKR; | ||
|
||
function setMedianizerPrice(uint256 newPrice) internal { | ||
vm.store(address(medianizer), bytes32(uint256(1)), bytes32(block.timestamp << 128 | newPrice)); | ||
} | ||
|
||
function setUp() public { | ||
vm.createSelectFork(vm.envString("ETH_RPC_URL")); | ||
|
||
PAUSE_PROXY = ChainlogLike(LOG).getAddress("MCD_PAUSE_PROXY"); | ||
PIP_MKR = ChainlogLike(LOG).getAddress("PIP_MKR"); | ||
|
||
medianizer = PipLike(PIP_MKR); | ||
|
||
vm.startPrank(PAUSE_PROXY); | ||
|
||
oracle = new StickyOracleHarness(PIP_MKR); | ||
oracle.kiss(address(this)); | ||
medianizer.kiss(address(oracle)); | ||
medianizer.kiss(address(this)); | ||
|
||
oracle.file("hi", 1); | ||
oracle.file("lo", 3); | ||
oracle.file("slope", RAY * 105 / 100); | ||
|
||
vm.stopPrank(); | ||
|
||
initialMedianizerPrice = 1000 * 10**18; | ||
setMedianizerPrice(initialMedianizerPrice); | ||
assertEq(medianizer.read(), initialMedianizerPrice); | ||
} | ||
|
||
function testInit() public { | ||
vm.expectRevert("StickyOracle/not-init"); | ||
oracle.read(); | ||
|
||
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); | ||
} | ||
|
||
function testFix() external { | ||
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); | ||
|
||
vm.warp(block.timestamp + 1 days); | ||
|
||
vm.expectRevert("StickyOracle/too-soon"); | ||
oracle.fix(block.timestamp / 1 days); | ||
|
||
vm.warp(block.timestamp + 1 days); | ||
assertEq(oracle.getAccumulator(block.timestamp / 1 days - 1), 0); | ||
|
||
oracle.fix(block.timestamp / 1 days - 1); | ||
|
||
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)); | ||
} | ||
|
||
function testPoke() public { | ||
vm.prank(PAUSE_PROXY); oracle.init(3); | ||
assertEq(oracle.read(), medianizer.read()); | ||
|
||
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); | ||
} | ||
} |
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.
Also - maybe it' simpler just to use the last
val
in this case and not multiply by slope?