Skip to content

Add Stork Oracle Adapter #88

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
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -94,6 +94,7 @@ An adapter's parameters and acceptance logic are easily observed on-chain.
| ------------------------------------------------------------------- | -------- | ------ | ----------------------- | -------------------------------------------- |
| [ChainlinkOracle](src/adapter/chainlink/ChainlinkOracle.sol) | External | Push | Provider feeds | feed, max staleness |
| [ChronicleOracle](src/adapter/chainlink/ChronicleOracle.sol) | External | Push | Provider feeds | feed, max staleness |
| [StorkOracle](src/adapter/stork/StorkOracle.sol) | External | Pull | Provider feeds | feed, max staleness |
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

grouped the Stork Oracle with the other External Pull oracles

| [PythOracle](src/adapter/pyth/PythOracle.sol) | External | Pull | Provider feeds | feed, max staleness, max confidence interval |
| [RedstoneCoreOracle](src/adapter/redstone/RedstoneCoreOracle.sol) | External | Pull | Provider feeds | feed, max staleness, cache ttl |
| [LidoOracle](src/adapter/lido/LidoOracle.sol) | Onchain | Rate | wstETH/stETH | - |
14 changes: 14 additions & 0 deletions src/adapter/stork/IStork.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
pragma solidity ^0.8.0;

interface IStorkTemporalNumericValueUnsafeGetter {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The minimal required interface to get prices from our contract

function getTemporalNumericValueUnsafeV1(
bytes32 id
) external view returns (StorkStructs.TemporalNumericValue memory value);
}

contract StorkStructs {
struct TemporalNumericValue {
uint64 timestampNs; // nanosecond level precision timestamp of latest publisher update in batch
int192 quantizedValue;
}
}
107 changes: 107 additions & 0 deletions src/adapter/stork/StorkOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "../../lib/ScaleUtils.sol";
import "./IStork.sol";
import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol";
import {IStorkTemporalNumericValueUnsafeGetter} from "./IStork.sol";

/// @title StorkOracle
/// @custom:security-contact security@euler.xyz
/// @author Stork Labs (https://www.stork.network/)
/// @notice PriceOracle adapter for Stork price feeds.
contract StorkOracle is BaseAdapter {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modeled on the existing PythOracle adapter.

Slightly simplified since Stork doesn't have a concept of confidence intervals, and doesn't have different exponents for different feeds (every value is multiplied by 10**18)

/// @notice The maximum length of time that a price can be in the future.
uint256 internal constant MAX_AHEADNESS = 1 minutes;
/// @notice The maximum permitted value for `maxStaleness`.
uint256 internal constant MAX_STALENESS_UPPER_BOUND = 15 minutes;
// @notice The number of decimals in values returned by the Stork contract.
int8 internal constant STORK_DECIMALS = 18;
/// @inheritdoc IPriceOracle
string public constant name = "StorkOracle";
/// @notice The address of the Stork oracle proxy.
address public immutable stork;
/// @notice The address of the base asset corresponding to the feed.
address public immutable base;
/// @notice The address of the quote asset corresponding to the feed.
address public immutable quote;
/// @notice The id of the feed in the Stork network.
/// @dev See https://docs.stork.network/resources/asset-id-registry.
bytes32 public immutable feedId;
/// @notice The maximum allowed age of the price.
uint256 public immutable maxStaleness;
/// @dev Used for correcting for the decimals of base and quote.
uint8 internal immutable baseDecimals;
/// @dev Used for correcting for the decimals of base and quote.
uint8 internal immutable quoteDecimals;

/// @notice Deploy a StorkOracle.
/// @param _stork The address of the Stork oracle proxy.
/// @param _base The address of the base asset corresponding to the feed.
/// @param _quote The address of the quote asset corresponding to the feed.
/// @param _feedId The id of the feed in the Stork network.
/// @param _maxStaleness The maximum allowed age of the price.
constructor(
address _stork,
address _base,
address _quote,
bytes32 _feedId,
uint256 _maxStaleness
) {
if (_maxStaleness > MAX_STALENESS_UPPER_BOUND) {
revert Errors.PriceOracle_InvalidConfiguration();
}

stork = _stork;
base = _base;
quote = _quote;
feedId = _feedId;
maxStaleness = _maxStaleness;
baseDecimals = _getDecimals(base);
quoteDecimals = _getDecimals(quote);
}

/// @notice Fetch the latest Stork price and transform it to a quote.
/// @param inAmount The amount of `base` to convert.
/// @param _base The token that is being priced.
/// @param _quote The token that is the unit of account.
/// @return The converted amount.
function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) {
bool inverse = ScaleUtils.getDirectionOrRevert(_base, base, _quote, quote);

StorkStructs.TemporalNumericValue memory temporalNumericValue = _fetchTemporalNumericValue();

uint256 value = uint256(uint192(temporalNumericValue.quantizedValue));
int8 feedExponent = int8(baseDecimals) + STORK_DECIMALS;

Scale scale;
if (feedExponent > 0) {
scale = ScaleUtils.from(quoteDecimals, uint8(feedExponent));
} else {
scale = ScaleUtils.from(quoteDecimals + uint8(-feedExponent), 0);
}
return ScaleUtils.calcOutAmount(inAmount, value, scale, inverse);
}

/// @notice Get the latest Stork price and perform sanity checks.
/// @dev Revert conditions: update timestamp is too stale or too ahead, price is negative or zero,
/// @return The Stork price struct without modification.
function _fetchTemporalNumericValue() internal view returns (StorkStructs.TemporalNumericValue memory) {
StorkStructs.TemporalNumericValue memory v = IStorkTemporalNumericValueUnsafeGetter(stork).getTemporalNumericValueUnsafeV1(feedId);
uint256 publishTimestampSeconds = v.timestampNs / 1e9;
if (publishTimestampSeconds < block.timestamp) {
// Verify that the price is not too stale
uint256 staleness = block.timestamp - publishTimestampSeconds;
if (staleness > maxStaleness) revert Errors.PriceOracle_InvalidAnswer();
} else {
// Verify that the price is not too ahead
uint256 aheadness = publishTimestampSeconds - block.timestamp;
if (aheadness > MAX_AHEADNESS) revert Errors.PriceOracle_InvalidAnswer();
}

if (v.quantizedValue <= 0) {
revert Errors.PriceOracle_InvalidAnswer();
}
return v;
}
}
29 changes: 29 additions & 0 deletions test/adapter/stork/StorkOracle.bounds.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "./StorkOracleHelper.sol";

contract StorkOracleBoundsTest is StorkOracleHelper {
function test_Bounds(FuzzableState memory s) public {
setBounds(
Bounds({
minBaseDecimals: 0,
maxBaseDecimals: 18,
minQuoteDecimals: 0,
maxQuoteDecimals: 18,
minInAmount: 0,
maxInAmount: type(uint128).max,
minPrice: 1,
maxPrice: 1_000_000_000_000
})
);
setUpState(s);
setUpOracle(s);

uint256 outAmount = StorkOracle(oracle).getQuote(s.inAmount, s.base, s.quote);
assertEq(outAmount, calcOutAmount(s));

uint256 outAmountInverse = StorkOracle(oracle).getQuote(s.inAmount, s.quote, s.base);
assertEq(outAmountInverse, calcOutAmountInverse(s));
}
}
50 changes: 50 additions & 0 deletions test/adapter/stork/StorkOracle.fork.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;


import {ForkTest} from "test/utils/ForkTest.sol";
import {StorkOracle} from "../../../src/adapter/stork/StorkOracle.sol";
import {StorkStructs, IStorkTemporalNumericValueUnsafeGetter} from "../../../src/adapter/stork/IStork.sol";
import {BTC, USD} from "test/utils/EthereumAddresses.sol";
import {stdStorage, StdStorage} from "forge-std/StdStorage.sol";


contract StorkOracleForkTest is ForkTest {
using stdStorage for StdStorage;

StorkOracle oracle;

address storkContractAddress = 0x035B5438444f26e6Aab81E91d475b7B1Ac4Fb22b;
bytes32 feedId = 0x7404e3d104ea7841c3d9e6fd20adfe99b4ad586bc08d8f3bd3afef894cf184de; // BTCUSD
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just hardcoding this value here since feed IDs aren't used in any other tests.

Would it be helpful if we added a StorkFeeds.sol file in the test directory with all of our feeds like you have for other oracles? Or is that just used for testing?

address base = BTC;
address quote = USD;
uint256 blockNumber = 22241301;
uint256 expectedValue = 79749.8e18;

function setUp() public {
_setUpFork(blockNumber);
}

function test_GetQuote_Integrity() public {
oracle = new StorkOracle(storkContractAddress, base, quote, feedId, 15 minutes);

uint256 outAmount = oracle.getQuote(1e18, base, quote);
assertApproxEqRel(outAmount, expectedValue, 0.1e18);
uint256 outAmountInverse = oracle.getQuote(expectedValue, quote, base);
assertApproxEqRel(outAmountInverse, 1e18, 0.1e18);
}

function test_GetQuotes_Integrity() public {
oracle = new StorkOracle(storkContractAddress, base, quote, feedId, 15 minutes);

(uint256 bidOutAmount, uint256 askOutAmount) = oracle.getQuotes(1e18, base, quote);
assertApproxEqRel(bidOutAmount, expectedValue, 0.1e18);
assertApproxEqRel(askOutAmount, expectedValue, 0.1e18);
assertEq(bidOutAmount, askOutAmount);

(uint256 bidOutAmountInverse, uint256 askOutAmountInverse) = oracle.getQuotes(expectedValue, quote, base);
assertApproxEqRel(bidOutAmountInverse, 1e18, 0.1e18);
assertApproxEqRel(askOutAmountInverse, 1e18, 0.1e18);
assertEq(bidOutAmountInverse, askOutAmountInverse);
}
}
45 changes: 45 additions & 0 deletions test/adapter/stork/StorkOracle.prop.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {AdapterPropTest} from "test/adapter/AdapterPropTest.sol";
import {StorkOracleHelper} from "./StorkOracleHelper.sol";

contract StorkraclePropTest is StorkOracleHelper, AdapterPropTest {
function testProp_Bidirectional(FuzzableState memory s, Prop_Bidirectional memory p) public {
setUpPropTest(s);
checkProp(p);
}

function testProp_NoOtherPaths(FuzzableState memory s, Prop_NoOtherPaths memory p) public {
setUpPropTest(s);
checkProp(p);
}

function testProp_IdempotentQuoteAndQuotes(FuzzableState memory s, Prop_IdempotentQuoteAndQuotes memory p) public {
setUpPropTest(s);
checkProp(p);
}

function testProp_SupportsZero(FuzzableState memory s, Prop_SupportsZero memory p) public {
setUpPropTest(s);
checkProp(p);
}

function testProp_ContinuousDomain(FuzzableState memory s, Prop_ContinuousDomain memory p) public {
setUpPropTest(s);
checkProp(p);
}

function testProp_OutAmountIncreasing(FuzzableState memory s, Prop_OutAmountIncreasing memory p) public {
setUpPropTest(s);
checkProp(p);
}

function setUpPropTest(FuzzableState memory s) internal {
setUpState(s);
setUpOracle(s);
adapter = address(oracle);
base = s.base;
quote = s.quote;
}
}
108 changes: 108 additions & 0 deletions test/adapter/stork/StorkOracle.unit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "../../../src/adapter/stork/StorkOracle.sol";
import {Errors} from "src/lib/Errors.sol";
import {Test} from "forge-std/Test.sol";
import {boundAddr} from "test/utils/TestUtils.sol";
import {StorkOracle} from "../../../src/adapter/stork/StorkOracle.sol";
import {StorkOracleHelper} from "./StorkOracleHelper.sol";

contract StorkOracleTest is StorkOracleHelper {
function test_Constructor_Integrity(FuzzableState memory s) public {
setUpState(s);
setUpOracle(s);

assertEq(address(StorkOracle(oracle).stork()), STORK);
assertEq(StorkOracle(oracle).base(), s.base);
assertEq(StorkOracle(oracle).quote(), s.quote);
assertEq(StorkOracle(oracle).feedId(), s.feedId);
assertEq(StorkOracle(oracle).maxStaleness(), s.maxStaleness);
}

function test_Constructor_RevertsWhen_MaxStalenessTooHigh(FuzzableState memory s) public {
setBehavior(Behavior.Constructor_MaxStalenessTooHigh, true);
setUpState(s);
vm.expectRevert();
setUpOracle(s);
}

function test_Quote_RevertsWhen_InvalidTokens(FuzzableState memory s, address otherA, address otherB) public {
setUpState(s);
setUpOracle(s);
otherA = boundAddr(otherA);
otherB = boundAddr(otherB);
vm.assume(otherA != s.base && otherA != s.quote);
vm.assume(otherB != s.base && otherB != s.quote);
expectNotSupported(s.inAmount, s.base, s.base);
expectNotSupported(s.inAmount, s.quote, s.quote);
expectNotSupported(s.inAmount, s.base, otherA);
expectNotSupported(s.inAmount, otherA, s.base);
expectNotSupported(s.inAmount, s.quote, otherA);
expectNotSupported(s.inAmount, otherA, s.quote);
expectNotSupported(s.inAmount, otherA, otherA);
expectNotSupported(s.inAmount, otherA, otherB);
}

function test_Quote_RevertsWhen_ZeroPrice(FuzzableState memory s) public {
setBehavior(Behavior.FeedReturnsZeroPrice, true);
setUpState(s);
setUpOracle(s);

bytes memory err = abi.encodeWithSelector(Errors.PriceOracle_InvalidAnswer.selector);
expectRevertForAllQuotePermutations(s.inAmount, s.base, s.quote, err);
}

function test_Quote_RevertsWhen_NegativePrice(FuzzableState memory s) public {
setBehavior(Behavior.FeedReturnsNegativePrice, true);
setUpState(s);
setUpOracle(s);

bytes memory err = abi.encodeWithSelector(Errors.PriceOracle_InvalidAnswer.selector);
expectRevertForAllQuotePermutations(s.inAmount, s.base, s.quote, err);
}

function test_Quote_RevertsWhen_StalePrice(FuzzableState memory s) public {
setBehavior(Behavior.FeedReturnsStalePrice, true);
setUpState(s);
setUpOracle(s);

bytes memory err = abi.encodeWithSelector(Errors.PriceOracle_InvalidAnswer.selector);
expectRevertForAllQuotePermutations(s.inAmount, s.base, s.quote, err);
}

function test_Quote_RevertsWhen_AheadPrice(FuzzableState memory s) public {
setBehavior(Behavior.FeedReturnsTooAheadPrice, true);
setUpState(s);
setUpOracle(s);

bytes memory err = abi.encodeWithSelector(Errors.PriceOracle_InvalidAnswer.selector);
expectRevertForAllQuotePermutations(s.inAmount, s.base, s.quote, err);
}

function test_Quote_Integrity(FuzzableState memory s) public {
setUpState(s);
setUpOracle(s);

uint256 expectedOutAmount = calcOutAmount(s);
uint256 outAmount = StorkOracle(oracle).getQuote(s.inAmount, s.base, s.quote);
assertEq(outAmount, expectedOutAmount);

(uint256 bidOutAmount, uint256 askOutAmount) = StorkOracle(oracle).getQuotes(s.inAmount, s.base, s.quote);
assertEq(bidOutAmount, expectedOutAmount);
assertEq(askOutAmount, expectedOutAmount);
}

function test_Quote_Integrity_Inverse(FuzzableState memory s) public {
setUpState(s);
setUpOracle(s);

uint256 expectedOutAmount = calcOutAmountInverse(s);
uint256 outAmount = StorkOracle(oracle).getQuote(s.inAmount, s.quote, s.base);
assertEq(outAmount, expectedOutAmount);

(uint256 bidOutAmount, uint256 askOutAmount) = StorkOracle(oracle).getQuotes(s.inAmount, s.quote, s.base);
assertEq(bidOutAmount, expectedOutAmount);
assertEq(askOutAmount, expectedOutAmount);
}
}
141 changes: 141 additions & 0 deletions test/adapter/stork/StorkOracleHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {Test} from "forge-std/Test.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";
import {FixedPointMathLib} from "@solady/utils/FixedPointMathLib.sol";
import {AdapterHelper} from "test/adapter/AdapterHelper.sol";
import {boundAddr, distinct} from "test/utils/TestUtils.sol";
import {StubStork} from "./StubStork.sol";
import {StorkOracle} from "../../../src/adapter/stork/StorkOracle.sol";
import {StorkStructs} from "../../../src/adapter/stork/IStork.sol";

contract StorkOracleHelper is AdapterHelper {
uint256 internal constant MAX_STALENESS_UPPER_BOUND = 15 minutes;

struct Bounds {
uint8 minBaseDecimals;
uint8 maxBaseDecimals;
uint8 minQuoteDecimals;
uint8 maxQuoteDecimals;
uint256 minInAmount;
uint256 maxInAmount;
int64 minPrice;
int64 maxPrice;
}

Bounds internal DEFAULT_BOUNDS = Bounds({
minBaseDecimals: 0,
maxBaseDecimals: 18,
minQuoteDecimals: 0,
maxQuoteDecimals: 18,
minInAmount: 0,
maxInAmount: type(uint128).max,
minPrice: 1,
maxPrice: 1_000_000_000_000
});

Bounds internal bounds = DEFAULT_BOUNDS;

function setBounds(Bounds memory _bounds) internal {
bounds = _bounds;
}

address STORK;

struct FuzzableState {
// Config
address base;
address quote;
bytes32 feedId;
uint256 maxStaleness;
uint8 baseDecimals;
uint8 quoteDecimals;
// Answer
StorkStructs.TemporalNumericValue v;
// Environment
uint256 inAmount;
uint256 timestamp;
}

constructor() {
STORK = address(new StubStork());
}

function setUpState(FuzzableState memory s) internal {
s.base = boundAddr(s.base);
s.quote = boundAddr(s.quote);
vm.assume(distinct(s.base, s.quote, STORK));
vm.assume(s.feedId != 0);

if (behaviors[Behavior.Constructor_MaxStalenessTooHigh]) {
s.maxStaleness = bound(s.maxStaleness, MAX_STALENESS_UPPER_BOUND + 1, type(uint128).max);
} else {
s.maxStaleness = bound(s.maxStaleness, 0, MAX_STALENESS_UPPER_BOUND);
}

s.baseDecimals = uint8(bound(s.baseDecimals, bounds.minBaseDecimals, bounds.maxBaseDecimals));
s.quoteDecimals = uint8(bound(s.quoteDecimals, bounds.minQuoteDecimals, bounds.maxQuoteDecimals));

vm.mockCall(s.base, abi.encodeWithSelector(IERC20.decimals.selector), abi.encode(s.baseDecimals));
vm.mockCall(s.quote, abi.encodeWithSelector(IERC20.decimals.selector), abi.encode(s.quoteDecimals));

if (behaviors[Behavior.FeedReturnsNegativePrice]) {
s.v.quantizedValue = int192(bound(s.v.quantizedValue, type(int64).min, -1));
} else if (behaviors[Behavior.FeedReturnsZeroPrice]) {
s.v.quantizedValue = 0;
} else {
s.v.quantizedValue = int192(bound(s.v.quantizedValue, bounds.minPrice, bounds.maxPrice));
}

s.v.timestampNs = uint64(bound(uint256(s.v.timestampNs), (1 minutes + 1) * 1e9, type(uint64).max));
uint256 valueTimestampSeconds = uint256(s.v.timestampNs / 1e9);

if (behaviors[Behavior.FeedReturnsStalePrice]) {
s.timestamp = bound(s.timestamp, valueTimestampSeconds + s.maxStaleness + 1, type(uint144).max);
} else if (behaviors[Behavior.FeedReturnsTooAheadPrice]) {
s.timestamp = bound(s.timestamp, 0, valueTimestampSeconds - 1 minutes - 1);
} else {
s.timestamp = bound(s.timestamp, valueTimestampSeconds - 1 minutes, valueTimestampSeconds + s.maxStaleness);
}

if (behaviors[Behavior.FeedReverts]) {
StubStork(STORK).setRevert(true);
} else {
StubStork(STORK).setPrice(s.v);
}

s.inAmount = bound(s.inAmount, 1, type(uint128).max);
vm.warp(s.timestamp);
}

function setUpOracle(FuzzableState memory s) internal {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separated this out from SetUpState to get the test_Constructor_RevertsWhen_MaxStalenessTooHigh unit test to pass - when the oracle was constructed at the end of setUpState (as it was for PythOracleHelper) vm.expectRevert() wasn't catching the revert correctly.

oracle = address(new StorkOracle(STORK, s.base, s.quote, s.feedId, s.maxStaleness));
}

function calcOutAmount(FuzzableState memory s) internal pure returns (uint256) {
int8 diff = int8(s.baseDecimals) + 18;
if (diff > 0) {
return FixedPointMathLib.fullMulDiv(
s.inAmount, uint256(uint192(s.v.quantizedValue)) * 10 ** s.quoteDecimals, 10 ** (uint8(diff))
);
} else {
return FixedPointMathLib.fullMulDiv(
s.inAmount, uint256(uint192(s.v.quantizedValue)) * 10 ** (s.quoteDecimals + uint8(-diff)), 1
);
}
}

function calcOutAmountInverse(FuzzableState memory s) internal pure returns (uint256) {
int8 diff = int8(s.baseDecimals) + 18;
if (diff > 0) {
return FixedPointMathLib.fullMulDiv(
s.inAmount, 10 ** uint8(diff), uint256(uint192(s.v.quantizedValue)) * 10 ** s.quoteDecimals
);
} else {
return FixedPointMathLib.fullMulDiv(
s.inAmount, 1, uint256(uint192(s.v.quantizedValue)) * 10 ** (s.quoteDecimals + uint8(-diff))
);
}
}
}
26 changes: 26 additions & 0 deletions test/adapter/stork/StubStork.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {StorkStructs} from "../../../src/adapter/stork/IStork.sol";


contract StubStork {
StorkStructs.TemporalNumericValue value;
bool doRevert;
string revertMsg = "oops";

function setPrice(StorkStructs.TemporalNumericValue memory _value) external {
value = _value;
}

function setRevert(bool _doRevert) external {
doRevert = _doRevert;
}

function getTemporalNumericValueUnsafeV1(
bytes32
) external view returns (StorkStructs.TemporalNumericValue memory) {
if (doRevert) revert(revertMsg);
return value;
}
}