Repository navigation
feat: keeper-driven share price oracle for networks without Chainlink Automation #197
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
Open
zmanian
wants to merge
5
commits into
main
Choose a base branch
from
fix/keeper-driven-share-price-oracle
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c1b5d49
feat: keeper-driven share price oracle for networks without Chainlink…
zmanian 4bf6244
fix: reject a zero keeper address at construction
zmanian fdb9602
test: cover the zero-keeper guard
zmanian 0fc271b
test: derivation equivalence and TWAP window invariants
zmanian fab3fbe
test: keep invariant handler time monotonic on older forge
zmanian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| pragma solidity 0.8.21; | ||
|
|
||
| import { ERC4626SharePriceOracle } from "src/base/ERC4626SharePriceOracle.sol"; | ||
| import { Math } from "src/utils/Math.sol"; | ||
|
|
||
| /** | ||
| * @notice An ERC4626SharePriceOracle driven by a trusted keeper rather than | ||
| * Chainlink Automation, for networks where Automation is unavailable. | ||
| * | ||
| * @dev Two differences from the base contract, and no others: | ||
| * | ||
| * 1. `automationForwarder` is set in the constructor instead of by | ||
| * `initialize`. That makes `initialize` permanently unreachable, since | ||
| * it reverts once `automationForwarder` is non-zero. No Chainlink | ||
| * upkeep is ever registered and the oracle never needs LINK. | ||
| * | ||
| * 2. `performUpkeep` derives the answer from the target via | ||
| * `_getTargetSharePrice()` instead of decoding it out of `performData`. | ||
| * Chainlink documents `performData` as untrusted; the base contract | ||
| * relies on the forwarder to vouch for it. Deriving it on-chain means | ||
| * the keeper chooses only *when* an observation is taken, never *what* | ||
| * it says. | ||
| * | ||
| * The upkeep body below is copied verbatim from the base contract so it | ||
| * diffs cleanly against the audited original. Every other safety property | ||
| * is inherited untouched: the TWAP ring buffer, `deviationTrigger`, the | ||
| * kill switch, heartbeat/grace staleness, and the sequencer check. | ||
| * | ||
| * The keeper is trusted for liveness only. If it stops calling | ||
| * `performUpkeep`, `getLatest` reports `notSafeToUse` once the answer ages | ||
| * past `heartbeat + gracePeriod` - the same behaviour as Chainlink halting. | ||
| */ | ||
| contract ERC4626SharePriceOracleKeeper is ERC4626SharePriceOracle { | ||
| using Math for uint256; | ||
|
|
||
| constructor(ConstructorArgs memory args, address keeper) ERC4626SharePriceOracle(args) { | ||
| automationForwarder = keeper; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Save answer on chain, and update observations if needed. | ||
| * @dev `performData` is ignored; the answer is read from `target`. | ||
| */ | ||
| function performUpkeep(bytes calldata) external override { | ||
| if (msg.sender != automationForwarder) revert ERC4626SharePriceOracle__OnlyCallableByAutomationForwarder(); | ||
| // The only deviation from the base contract: the answer and its | ||
| // timestamp come from chain state, not from the caller. | ||
| uint216 sharePrice = _getTargetSharePrice(); | ||
| uint64 currentTime = uint64(block.timestamp); | ||
|
|
||
| // Verify atleast one of the upkeep conditions was met. | ||
| bool upkeepConditionMet; | ||
|
|
||
| // Read state from one slot. | ||
| uint256 _answer = answer; | ||
| uint16 _currentIndex = currentIndex; | ||
| uint16 _observationsLength = observationsLength; | ||
| bool _killSwitch = killSwitch; | ||
|
|
||
| if (_killSwitch) revert ERC4626SharePriceOracle__ContractKillSwitch(); | ||
|
|
||
| // See if kill switch should be activated based on change between answers. | ||
| if (_checkIfKillSwitchShouldBeTriggered(sharePrice, _answer)) return; | ||
|
|
||
| // See if we are upkeeping because of deviation. | ||
| if ( | ||
| sharePrice > uint256(_answer).mulDivDown(1e4 + deviationTrigger, 1e4) || | ||
| sharePrice < uint256(_answer).mulDivDown(1e4 - deviationTrigger, 1e4) | ||
| ) upkeepConditionMet = true; | ||
|
|
||
| // Update answer. | ||
| answer = sharePrice; | ||
|
|
||
| // Update current observation. | ||
| Observation storage currentObservation = observations[_currentIndex]; | ||
| // Make sure time is larger than previous time. | ||
| if (currentTime <= currentObservation.timestamp) revert ERC4626SharePriceOracle__StalePerformData(); | ||
|
|
||
| // Make sure time is not in the future. | ||
| if (currentTime > block.timestamp) revert ERC4626SharePriceOracle__FuturePerformData(); | ||
|
|
||
| // See if we are updating because of staleness. | ||
| uint256 timeDelta = currentTime - currentObservation.timestamp; | ||
| if (timeDelta >= heartbeat) upkeepConditionMet = true; | ||
|
|
||
| // Use the old answer to calculate cumulative. | ||
| uint256 currentCumulative = currentObservation.cumulative + (_answer * timeDelta); | ||
| if (currentCumulative > type(uint192).max) revert ERC4626SharePriceOracle__CumulativeTooLarge(); | ||
| currentObservation.cumulative = uint192(currentCumulative); | ||
| currentObservation.timestamp = currentTime; | ||
|
|
||
| uint256 timeDeltaSincePreviousObservation = currentTime - | ||
| observations[_getPreviousIndex(_currentIndex, _observationsLength)].timestamp; | ||
| // See if we need to advance to the next cumulative. | ||
| if (timeDeltaSincePreviousObservation >= heartbeat) { | ||
| uint16 nextIndex = _getNextIndex(_currentIndex, _observationsLength); | ||
| currentIndex = nextIndex; | ||
| // Update memory variable for event. | ||
| _currentIndex = nextIndex; | ||
| // Update newest cumulative. | ||
| Observation storage newObservation = observations[nextIndex]; | ||
| newObservation.cumulative = uint192(currentCumulative); | ||
| newObservation.timestamp = currentTime; | ||
| upkeepConditionMet = true; | ||
| } | ||
|
|
||
| if (!upkeepConditionMet) revert ERC4626SharePriceOracle__NoUpkeepConditionMet(); | ||
|
|
||
| (uint256 timeWeightedAverageAnswer, bool isNotSafeToUse) = _getTimeWeightedAverageAnswer( | ||
| sharePrice, | ||
| _currentIndex, | ||
| _observationsLength | ||
| ); | ||
|
|
||
| // See if kill switch should be activated based on change between proposed answer and time weighted average answer. | ||
| if (!isNotSafeToUse && _checkIfKillSwitchShouldBeTriggered(sharePrice, timeWeightedAverageAnswer)) return; | ||
| emit OracleUpdated(block.timestamp, currentTime, sharePrice, timeWeightedAverageAnswer, isNotSafeToUse); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.