Skip to content

Commit

Permalink
Add draft StickyOracle
Browse files Browse the repository at this point in the history
  • Loading branch information
telome committed Sep 25, 2023
1 parent 38a06e2 commit 80b1a40
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 0 deletions.
130 changes: 130 additions & 0 deletions src/StickyOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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);
}

contract StickyOracle {
mapping (address => uint256) public wards;
mapping (address => uint256) public buds; // Whitelisted feed readers
mapping (uint16 => uint256) accumulators; // daily (eod) sticky oracle price accumulators

PipLike public immutable pip;

uint96 public slope = uint96(RAY); // maximum allowable price growth rate from center of TWAP window to now (in 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

struct Accumulator {
uint256 acc; // accumulator value at the end of the day
}

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);
if (what == "lo") lo = uint8(data);
if (what == "hi") hi = uint8(data);
else revert("StickyOracle/file-unrecognized-param");
emit File(what, data);
}

function _getCap() internal view returns (uint256 cap) {
uint16 today = uint16(block.timestamp / 1 days);
(uint96 slope_, uint16 lo_, uint16 hi_) = (slope, lo, hi);
uint256 acc_lo = accumulators[today - lo_];
uint256 acc_hi = accumulators[today - hi_];

if (acc_lo == 0 || acc_hi == 0) return pip.read(); // TODO: do something smarter (use partial window or extrapolate missing daily accs)
return (acc_hi - acc_lo) * slope_ / (RAY * (lo_ - hi_) * 1 days);
}


function poke() public {
uint128 cur = pip.read();
uint16 today = uint16(block.timestamp / 1 days);
uint256 acc = accumulators[today];
(uint128 val_, uint32 age_) = (val, age);
uint256 newAcc;
if (acc == 0) { // first poke of the day
if (age_ > 0) {
uint16 prevDay = uint16(age_ / 1 days);
uint256 prevAcc = accumulators[prevDay];
newAcc = prevAcc + (
val_ * (today - prevDay - 1) + // account for possibly missing daily pokes
cur // optimistically assume this will be the last poke of the day
) * 1 days;
} else {
newAcc = cur * 1 days; // optimistically assume this will be the last poke of the day
}
} else { // not the first poke of the day
uint256 eod = (today + 1) * 1 days; // TODO: check if not off-by-one
uint256 off = eod - age_; // 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);
}

function read() external view toll returns (uint256) {
uint128 cur = pip.read();
uint256 cap = _getCap();
return cur < cap ? cur : cap;
}

function peek() external view toll returns (uint256, bool) {
uint128 cur = pip.read();
uint256 cap = _getCap();
return (cur < cap ? cur : cap, cur > 0);
}
}
67 changes: 67 additions & 0 deletions test/StickyOracle.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// 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 StickyOracleTest is Test {

PipLike public medianizer;
StickyOracle public oracle;
uint256 public initialMedianizerPrice;

uint256 constant RAY = 10 ** 27;
address constant LOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;

address PAUSE_PROXY;
address PIP_MKR;

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 StickyOracle(PIP_MKR);
oracle.kiss(address(this));
medianizer.kiss(address(oracle));
medianizer.kiss(address(this));

vm.stopPrank();

initialMedianizerPrice = uint256(medianizer.read());
assertGt(initialMedianizerPrice, 0);
}

function testPoke() public {
}
}

0 comments on commit 80b1a40

Please sign in to comment.